File size: 16,980 Bytes
99d0976
9973d70
99d0976
 
 
 
 
 
 
 
 
 
d3bbfc6
99d0976
e8bd7a8
99d0976
71d7cd2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c428f9f
71d7cd2
 
 
 
 
f25b56f
3e228de
d3bbfc6
f25b56f
99d0976
f25b56f
3e228de
 
 
 
 
 
d3bbfc6
f25b56f
d3bbfc6
f25b56f
d3bbfc6
3e228de
 
 
 
 
 
 
 
 
d3bbfc6
 
f25b56f
 
 
 
 
3e228de
 
 
f25b56f
3e228de
 
 
 
 
 
c428f9f
d3bbfc6
3e228de
d3bbfc6
f25b56f
 
 
 
99d0976
f25b56f
 
3e228de
 
 
 
 
f25b56f
 
d3bbfc6
f25b56f
3f4986e
 
3e228de
e8bd7a8
d3bbfc6
3e228de
 
 
c428f9f
3e228de
 
 
6b9a753
3e228de
 
 
 
 
3f4986e
f25b56f
 
d3bbfc6
3e228de
 
 
 
e8bd7a8
 
f25b56f
3e228de
e8bd7a8
3e228de
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e8bd7a8
3e228de
 
 
e8bd7a8
3e228de
e8bd7a8
3e228de
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c428f9f
3e228de
 
d3bbfc6
3e228de
99d0976
 
f25b56f
 
99d0976
f25b56f
 
 
 
 
 
99d0976
f25b56f
 
 
 
99d0976
f25b56f
 
 
 
99d0976
 
3e228de
99d0976
f25b56f
99d0976
 
 
 
f25b56f
 
 
 
 
 
99d0976
 
 
 
 
9973d70
 
e8bd7a8
3e228de
99d0976
 
 
 
 
 
 
 
 
3e228de
99d0976
3e228de
e8bd7a8
3e228de
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d3bbfc6
3f4986e
3e228de
c428f9f
99d0976
 
 
 
 
 
 
 
 
3e228de
 
 
 
 
 
 
 
 
99d0976
 
 
 
e8bd7a8
99d0976
 
 
 
 
 
 
 
 
d3bbfc6
 
f25b56f
3e228de
 
 
 
 
 
 
 
99d0976
 
f25b56f
99d0976
 
 
 
d3bbfc6
3e228de
e8bd7a8
d3bbfc6
3e228de
 
 
 
 
9126d17
3e228de
 
99d0976
 
3e228de
99d0976
f25b56f
4eeab21
99d0976
 
3e228de
 
 
 
 
 
 
99d0976
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f25b56f
99d0976
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f25b56f
 
e8bd7a8
f25b56f
 
 
 
 
 
 
 
9973d70
99d0976
9973d70
d3bbfc6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
import os
import gradio as gr
from langchain_groq import ChatGroq
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate
from langchain.vectorstores import FAISS
from langchain.embeddings import HuggingFaceEmbeddings

# Groq API 키 (Hugging Face Secrets에서 가져옴)
GROQ_API_KEY = os.environ.get("GROQ_API_KEY")

# 전역 변수들
vectorstores = {}
embeddings = None
combined_vectorstore = None

def debug_file_system():
    """파일 시스템 상태를 자세히 확인하는 함수"""
    import os
    print("=" * 50)
    print("🔍 파일 시스템 디버깅 시작")
    print("=" * 50)
    
    # 현재 디렉토리
    current_dir = os.getcwd()
    print(f"📂 현재 작업 디렉토리: {current_dir}")
    
    # 루트 디렉토리의 모든 항목
    try:
        all_items = os.listdir('.')
        print(f"📋 루트 디렉토리 내용: {all_items}")
        
        # 각 항목의 타입 확인
        for item in all_items:
            item_path = os.path.join('.', item)
            if os.path.isdir(item_path):
                print(f"📁 {item} (디렉토리)")
                try:
                    sub_items = os.listdir(item_path)
                    print(f"   └── 내용: {sub_items}")
                except Exception as e:
                    print(f"   └── 접근 불가: {e}")
            else:
                print(f"📄 {item} (파일)")
                
    except Exception as e:
        print(f"❌ 디렉토리 읽기 오류: {e}")
    
    # 환경 변수 확인
    print(f"🔑 GROQ_API_KEY 설정됨: {'GROQ_API_KEY' in os.environ}")
    
    print("=" * 50)

# 앱 시작 시 디버깅 실행
debug_file_system()

def find_vectorstore_folders():
    """현재 디렉토리에서 벡터스토어 폴더들을 찾는 함수 - 개선 버전"""
    current_dir = os.getcwd()
    print(f"현재 디렉토리: {current_dir}")
    
    # 모든 파일과 폴더 확인
    try:
        all_items = os.listdir(current_dir)
        print(f"현재 디렉토리 내 모든 항목들: {all_items}")
    except Exception as e:
        print(f"디렉토리 읽기 오류: {e}")
        return []
    
    # 예상되는 벡터스토어 폴더들
    expected_folders = ['vectorstore1', 'vectorstore2', 'vectorstore3']
    vectorstore_folders = []
    
    # 실제로 존재하는 벡터스토어 관련 폴더들도 찾기
    for item in all_items:
        if os.path.isdir(item) and ('vectorstore' in item.lower() or 'vector' in item.lower()):
            expected_folders.append(item)
    
    # 중복 제거
    expected_folders = list(set(expected_folders))
    print(f"확인할 폴더들: {expected_folders}")
    
    for folder_name in expected_folders:
        folder_path = os.path.join(current_dir, folder_name)
        if os.path.exists(folder_path) and os.path.isdir(folder_path):
            try:
                folder_contents = os.listdir(folder_path)
                print(f"📁 {folder_name} 폴더 내용: {folder_contents}")
                
                # FAISS 파일들 확인 (더 유연하게)
                has_faiss = any('.faiss' in file for file in folder_contents)
                has_pkl = any('.pkl' in file for file in folder_contents)
                
                if has_faiss and has_pkl:
                    vectorstore_folders.append(folder_name)
                    print(f"✅ {folder_name} - FAISS 파일들 존재")
                elif has_faiss or has_pkl:
                    print(f"⚠️ {folder_name} - 일부 파일만 존재 (faiss: {has_faiss}, pkl: {has_pkl})")
                    # 일부만 있어도 시도해보기
                    vectorstore_folders.append(folder_name)
                else:
                    print(f"❌ {folder_name} - FAISS 파일들 없음")
                    
            except Exception as e:
                print(f"❌ {folder_name} 폴더 확인 중 오류: {e}")
        else:
            print(f"❌ {folder_name} 폴더가 존재하지 않음")
    
    if not vectorstore_folders:
        print("❌ 사용 가능한 벡터스토어 폴더를 찾을 수 없습니다")
        # 디버깅을 위해 현재 디렉토리의 모든 하위 폴더 출력
        print("📋 현재 디렉토리의 모든 폴더들:")
        for item in all_items:
            if os.path.isdir(item):
                print(f"  📁 {item}")
    else:
        print(f"✅ 총 {len(vectorstore_folders)}개의 벡터스토어 폴더를 찾았습니다: {vectorstore_folders}")
    
    return vectorstore_folders

def load_all_vectorstores():
    """모든 벡터스토어를 로드하고 통합하는 함수 - 개선 버전"""
    global vectorstores, embeddings, combined_vectorstore
    
    print("🔄 벡터스토어 로딩 시작...")
    
    # 임베딩 모델 초기화
    if not embeddings:
        try:
            print("🤖 임베딩 모델 로딩 중...")
            embeddings = HuggingFaceEmbeddings(
                model_name="jhgan/ko-sbert-nli"
            )
            print("✅ 임베딩 모델 로딩 완료")
        except Exception as e:
            print(f"❌ 임베딩 모델 로딩 실패: {e}")
            return False
    
    # 벡터스토어 폴더들 찾기
    folders = find_vectorstore_folders()
    
    if not folders:
        print("❌ 벡터스토어 폴더를 찾을 수 없습니다")
        return False
    
    loaded_vectorstores = []
    
    for folder_name in folders:
        folder_path = f"./{folder_name}"
        try:
            print(f"📂 {folder_name} 로딩 시도 중...")
            
            # 다양한 방법으로 로딩 시도
            vectorstore = None
            
            # 방법 1: 기본 로딩
            try:
                vectorstore = FAISS.load_local(
                    folder_path, 
                    embeddings, 
                    allow_dangerous_deserialization=True
                )
                print(f"✅ {folder_name} 로드 완료 (방법 1)")
            except Exception as e1:
                print(f"⚠️ 방법 1 실패: {e1}")
                
                # 방법 2: 절대 경로로 시도
                try:
                    abs_path = os.path.abspath(folder_name)
                    vectorstore = FAISS.load_local(
                        abs_path, 
                        embeddings, 
                        allow_dangerous_deserialization=True
                    )
                    print(f"✅ {folder_name} 로드 완료 (방법 2)")
                except Exception as e2:
                    print(f"⚠️ 방법 2 실패: {e2}")
                    
                    # 방법 3: 직접 파일 지정
                    try:
                        index_file = os.path.join(folder_name, "index.faiss")
                        pkl_file = os.path.join(folder_name, "index.pkl")
                        
                        if os.path.exists(index_file) and os.path.exists(pkl_file):
                            vectorstore = FAISS.load_local(
                                folder_name, 
                                embeddings, 
                                allow_dangerous_deserialization=True
                            )
                            print(f"✅ {folder_name} 로드 완료 (방법 3)")
                        else:
                            print(f"❌ 필수 파일 없음: {index_file}, {pkl_file}")
                    except Exception as e3:
                        print(f"❌ 방법 3 실패: {e3}")
            
            if vectorstore:
                vectorstores[folder_name] = vectorstore
                loaded_vectorstores.append(vectorstore)
                
                # 벡터스토어 정보 출력
                try:
                    doc_count = vectorstore.index.ntotal
                    print(f"📊 {folder_name}: {doc_count}개 문서")
                except:
                    print(f"📊 {folder_name}: 문서 수 확인 불가")
            else:
                print(f"❌ {folder_name} 로드 실패")
                
        except Exception as e:
            print(f"❌ {folder_name} 로드 중 예외 발생: {e}")
            import traceback
            traceback.print_exc()
    
    # 벡터스토어들을 통합
    if loaded_vectorstores:
        try:
            print("🔗 벡터스토어 통합 시작...")
            combined_vectorstore = loaded_vectorstores[0]
            
            for i, vs in enumerate(loaded_vectorstores[1:], 1):
                try:
                    combined_vectorstore.merge_from(vs)
                    print(f"✅ 벡터스토어 {i+1} 통합 완료")
                except Exception as e:
                    print(f"❌ 벡터스토어 {i+1} 통합 실패: {e}")
            
            print(f"🎉 총 {len(loaded_vectorstores)}개의 벡터스토어가 로드되고 통합되었습니다")
            return True
            
        except Exception as e:
            print(f"❌ 벡터스토어 통합 중 오류: {e}")
            # 통합 실패시 첫 번째 것만 사용
            if loaded_vectorstores:
                combined_vectorstore = loaded_vectorstores[0]
                print("⚠️ 첫 번째 벡터스토어만 사용합니다")
                return True
    
    print("❌ 사용 가능한 벡터스토어가 없습니다")
    return False

# 질문 리스트
suggested_questions = [
    '교원 신규 임용은 어떻게 하나요?',
    '교원 연구년 기간은 어떻게 되나요?',
    '조교 신규 임용 기준은 무엇인가요?',
    '교직원의 평일 근무시간은 어떻게 되나요?',
    '직원 신규 임용 원칙은 무엇인가요?',
    '직원 임용시 가산점이 있나요?',
    '교원 업적의 심사 내용은 무엇인가요?',
    '외국인 교원의 임기는 어떻게 되나요?',
    '외국인 교원의 면직 기준은 무엇인가요?',
    '기간제 계약직의 임기는 얼마정도인가요?',
    '등록금 납부 방법은 무엇인가요?',
    '교직 이수는 언제 신청이 가능한가요?',
    '해외교류유학 지원자격은 어떻게 되나요?',
    '만족도 조사 실행 대상은 누구인가요?',
    '마이크로디그리의 유형은 무엇이 있나요?',
    '장학금 관리 기관은 어디인가요?',
    '학생 단체는 어떻게 등록하나요?',
    '학생 설치물 중 금지된 설치물이 있나요?',
    '비교과 교육과정의 종류는 무엇이 있나요?',
    '안전사고예방계획은 어디에 제출해야 하나요?'
]

# 프롬프트 템플릿
prompt_template = """당신은 한남대학교 규정집 도우미입니다.
반드시 한국어로만 답변해주세요. 영어나 다른 언어는 절대 사용하지 마세요.
주어진 문서 내용을 바탕으로 질문에 대해 정확하고 친절하게 한국어로 답변해주세요.
참고 문서:
{context}
질문: {question}
답변 지침:
- 이용자를 반기는 인사로 시작하세요
- 반드시 한국어로만 답변하세요
- 정중하고 친근한 말투를 사용하세요
- 구체적이고 도움이 되는 정보를 제공하세요
- 문서에서 답을 찾을 수 없으면 "죄송하지만 해당 정보를 규정집에서 찾을 수 없습니다"라고 답변하세요
한국어 답변:"""

prompt = PromptTemplate(
    template=prompt_template,
    input_variables=["context", "question"]
)

def respond_with_groq(question, selected_q, model):
    """질문에 대한 답변을 생성하는 함수 - 개선 버전"""
    
    # 선택된 질문이 있으면 그것을 사용
    if selected_q != "직접 입력":
        question = selected_q

    if not question.strip():
        return "질문을 입력해주세요."

    if not GROQ_API_KEY:
        return "❌ GROQ API 키가 설정되지 않았습니다. Hugging Face Spaces의 Settings에서 GROQ_API_KEY를 설정해주세요."

    # 통합된 벡터스토어가 로드되지 않은 경우 재시도
    if not combined_vectorstore:
        print("⚠️ 벡터스토어가 로드되지 않음. 재로딩 시도...")
        success = load_all_vectorstores()
        if not success:
            # 디버깅 정보 출력
            debug_file_system()
            return """❌ 벡터스토어를 로드할 수 없습니다. 
            
가능한 원인:
1. 벡터스토어 파일이 올바르게 업로드되지 않음
2. 파일 권한 문제
3. Git LFS 설정 필요 (큰 파일의 경우)

해결 방법:
1. vectorstore 폴더들이 제대로 업로드되었는지 확인
2. 각 폴더에 index.faiss와 index.pkl 파일이 있는지 확인
3. Git LFS를 사용해 큰 파일들을 관리해보세요"""

    try:
        print(f"🔍 질문: {question}")
        print(f"✅ 통합된 벡터스토어를 사용하여 검색 중...")

        # LLM 설정
        llm = ChatGroq(
            groq_api_key=GROQ_API_KEY,
            model_name=model,
            temperature=0.1,
            max_tokens=1000
        )

        # 검색 테스트
        try:
            retriever = combined_vectorstore.as_retriever(search_kwargs={"k": 5})
            docs = retriever.get_relevant_documents(question)
            print(f"🔍 검색된 문서 수: {len(docs)}")
        except Exception as e:
            print(f"❌ 검색 오류: {e}")
            return f"❌ 문서 검색 중 오류가 발생했습니다: {str(e)}"

        # QA 체인 생성
        qa_chain = RetrievalQA.from_chain_type(
            llm=llm,
            chain_type="stuff",
            retriever=combined_vectorstore.as_retriever(search_kwargs={"k": 5}),
            chain_type_kwargs={"prompt": prompt},
            return_source_documents=True
        )

        # 답변 생성
        result = qa_chain({"query": question})
        return result['result']

    except Exception as e:
        import traceback
        error_details = traceback.format_exc()
        print(f"❌ 상세 오류 정보:\n{error_details}")
        return f"""❌ 답변 생성 중 오류가 발생했습니다: {str(e)}

디버깅 정보:
- 벡터스토어 로드됨: {combined_vectorstore is not None}
- API 키 설정됨: {GROQ_API_KEY is not None}
- 모델: {model}

관리자에게 위 정보와 함께 문의해주세요."""

def update_question(selected):
    """드롭다운 선택 시 질문을 업데이트하는 함수"""
    if selected != "직접 입력":
        return selected
    return ""

# 앱 시작시 벡터스토어들 로드
print("🚀 앱 시작 - 벡터스토어 로딩 중...")
vectorstores_loaded = load_all_vectorstores()

# 상태 메시지 생성
if vectorstores_loaded:
    status_message = "✅ 벡터스토어가 성공적으로 로드되었습니다!"
    status_color = "green"
else:
    status_message = "❌ 벡터스토어 로딩에 실패했습니다."
    status_color = "red"

# Gradio 인터페이스 생성
with gr.Blocks(title="한남대학교 Q&A") as interface:
    gr.HTML(f"""
    <div style="text-align: center; padding: 20px; background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 10px; margin-bottom: 20px;">
        <h1>🏫 한남대학교 규정집 Q&A</h1>
        <p>한남대학교 규정집에 대한 질문에 답변해드립니다.</p>
    </div>
    """)
    
    # 상태 표시 추가
    gr.HTML(f"""
    <div style="text-align: center; padding: 10px; background-color: {status_color}; color: white; border-radius: 5px; margin-bottom: 10px;">
        <strong>{status_message}</strong>
    </div>
    """)

    with gr.Row():
        with gr.Column(scale=1):
            question_dropdown = gr.Dropdown(
                choices=["직접 입력"] + suggested_questions,
                label="💡 자주 묻는 질문",
                value="직접 입력"
            )

            question_input = gr.Textbox(
                label="❓ 질문을 입력하세요",
                placeholder="예: 졸업 요건은 무엇인가요?",
                lines=3
            )

            submit_btn = gr.Button("답변 받기", variant="primary", size="lg")

            model_choice = gr.Radio(
                choices=["llama3-70b-8192", "llama3-8b-8192"],
                label="🤖 AI 모델 선택",
                value="llama3-70b-8192"
            )

        with gr.Column(scale=2):
            output = gr.Textbox(
                label="💬 답변",
                lines=15,
                max_lines=20,
                show_copy_button=True
            )

    # 이벤트 연결
    submit_btn.click(
        fn=respond_with_groq,
        inputs=[question_input, question_dropdown, model_choice],
        outputs=output
    )

    question_dropdown.change(
        fn=update_question,
        inputs=question_dropdown,
        outputs=question_input
    )

# 앱 실행
if __name__ == "__main__":
    interface.launch()