Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -6,6 +6,8 @@ from langchain.chains import RetrievalQA
|
|
6 |
from langchain.prompts import PromptTemplate
|
7 |
from langchain.vectorstores import FAISS
|
8 |
from langchain.embeddings import HuggingFaceEmbeddings
|
|
|
|
|
9 |
|
10 |
# SSL 경고 제거
|
11 |
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
@@ -18,6 +20,94 @@ vectorstores = {}
|
|
18 |
embeddings = None
|
19 |
combined_vectorstore = None
|
20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
21 |
def debug_file_system():
|
22 |
"""파일 시스템 상태를 자세히 확인하는 함수"""
|
23 |
import os
|
@@ -326,17 +416,19 @@ prompt = PromptTemplate(
|
|
326 |
)
|
327 |
|
328 |
def respond_with_groq(question, selected_q, model):
|
329 |
-
"""질문에 대한 답변을 생성하는 함수 - 개선 버전"""
|
330 |
|
331 |
# 선택된 질문이 있으면 그것을 사용
|
332 |
if selected_q != "직접 입력":
|
333 |
question = selected_q
|
334 |
|
335 |
if not question.strip():
|
336 |
-
return "질문을 입력해주세요."
|
337 |
|
338 |
if not GROQ_API_KEY:
|
339 |
-
|
|
|
|
|
340 |
|
341 |
# 통합된 벡터스토어가 로드되지 않은 경우 재시도
|
342 |
if not combined_vectorstore:
|
@@ -345,7 +437,7 @@ def respond_with_groq(question, selected_q, model):
|
|
345 |
if not success:
|
346 |
# 디버깅 정보 출력
|
347 |
debug_file_system()
|
348 |
-
|
349 |
|
350 |
가능한 원인:
|
351 |
1. 벡터스토어 파일이 올바르게 업로드되지 않음
|
@@ -355,6 +447,8 @@ def respond_with_groq(question, selected_q, model):
|
|
355 |
1. vectorstore 폴더들이 제대로 업로드되었는지 확인
|
356 |
2. 각 폴더에 index.faiss와 index.pkl 파일이 있는지 확인
|
357 |
3. Git LFS를 사용해 큰 파일들을 관리해보세요"""
|
|
|
|
|
358 |
|
359 |
try:
|
360 |
print(f"🔍 질문: {question}")
|
@@ -380,7 +474,9 @@ def respond_with_groq(question, selected_q, model):
|
|
380 |
print(f"🔍 검색된 문서 수: {len(docs)}")
|
381 |
except Exception as e:
|
382 |
print(f"❌ 검색 오류: {e}")
|
383 |
-
|
|
|
|
|
384 |
|
385 |
# QA 체인 생성
|
386 |
qa_chain = RetrievalQA.from_chain_type(
|
@@ -393,18 +489,38 @@ def respond_with_groq(question, selected_q, model):
|
|
393 |
|
394 |
# 답변 생성
|
395 |
result = qa_chain({"query": question})
|
396 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
397 |
|
398 |
except Exception as e:
|
399 |
import traceback
|
400 |
error_details = traceback.format_exc()
|
401 |
print(f"❌ 상세 오류 정보:\n{error_details}")
|
402 |
-
|
403 |
디버깅 정보:
|
404 |
- 벡터스토어 로드됨: {combined_vectorstore is not None}
|
405 |
- API 키 설정됨: {GROQ_API_KEY is not None}
|
406 |
- 모델: {model}
|
407 |
관리자에게 위 정보와 함께 문의해주세요."""
|
|
|
|
|
|
|
408 |
|
409 |
def update_question(selected):
|
410 |
"""드롭다운 선택 시 질문을 업데이트하는 함수"""
|
@@ -412,6 +528,11 @@ def update_question(selected):
|
|
412 |
return selected
|
413 |
return ""
|
414 |
|
|
|
|
|
|
|
|
|
|
|
415 |
# 앱 시작시 벡터스토어들 로드
|
416 |
print("🚀 앱 시작 - 벡터스토어 로딩 중...")
|
417 |
vectorstores_loaded = load_all_vectorstores()
|
@@ -440,41 +561,64 @@ with gr.Blocks(title="한남대학교 Q&A") as interface:
|
|
440 |
</div>
|
441 |
""")
|
442 |
|
443 |
-
|
444 |
-
|
445 |
-
|
446 |
-
|
447 |
-
|
448 |
-
|
449 |
-
|
|
|
|
|
|
|
|
|
450 |
|
451 |
-
|
452 |
-
|
453 |
-
|
454 |
-
|
455 |
-
|
456 |
|
457 |
-
|
458 |
|
459 |
-
|
460 |
-
|
461 |
-
|
462 |
-
|
463 |
-
|
464 |
|
465 |
-
|
466 |
-
|
467 |
-
|
468 |
-
|
469 |
-
|
470 |
-
|
471 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
472 |
|
473 |
# 이벤트 연결
|
474 |
submit_btn.click(
|
475 |
fn=respond_with_groq,
|
476 |
inputs=[question_input, question_dropdown, model_choice],
|
477 |
-
outputs=output
|
478 |
)
|
479 |
|
480 |
question_dropdown.change(
|
@@ -482,6 +626,16 @@ with gr.Blocks(title="한남대학교 Q&A") as interface:
|
|
482 |
inputs=question_dropdown,
|
483 |
outputs=question_input
|
484 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
485 |
|
486 |
# 앱 실행
|
487 |
if __name__ == "__main__":
|
|
|
6 |
from langchain.prompts import PromptTemplate
|
7 |
from langchain.vectorstores import FAISS
|
8 |
from langchain.embeddings import HuggingFaceEmbeddings
|
9 |
+
from datetime import datetime
|
10 |
+
import json
|
11 |
|
12 |
# SSL 경고 제거
|
13 |
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
|
20 |
embeddings = None
|
21 |
combined_vectorstore = None
|
22 |
|
23 |
+
# 대화 기록 저장을 위한 전역 변수 (메모리 기반)
|
24 |
+
conversation_history = []
|
25 |
+
|
26 |
+
def save_conversation(question, answer, sources=None):
|
27 |
+
"""대화 기록을 저장하는 함수"""
|
28 |
+
global conversation_history
|
29 |
+
|
30 |
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
31 |
+
|
32 |
+
conversation_entry = {
|
33 |
+
"timestamp": timestamp,
|
34 |
+
"question": question,
|
35 |
+
"answer": answer,
|
36 |
+
"sources": sources if sources else []
|
37 |
+
}
|
38 |
+
|
39 |
+
conversation_history.append(conversation_entry)
|
40 |
+
|
41 |
+
# 최대 100개까지만 저장 (메모리 절약)
|
42 |
+
if len(conversation_history) > 100:
|
43 |
+
conversation_history = conversation_history[-100:]
|
44 |
+
|
45 |
+
def get_conversation_history():
|
46 |
+
"""대화 기록을 반환하는 함수"""
|
47 |
+
global conversation_history
|
48 |
+
|
49 |
+
if not conversation_history:
|
50 |
+
return "아직 대화 기록이 없습니다."
|
51 |
+
|
52 |
+
history_text = "## 🕐 대화 기록\n\n"
|
53 |
+
|
54 |
+
# 최근 기록부터 표시
|
55 |
+
for i, entry in enumerate(reversed(conversation_history), 1):
|
56 |
+
history_text += f"### {i}. {entry['timestamp']}\n"
|
57 |
+
history_text += f"**❓ 질문:** {entry['question']}\n\n"
|
58 |
+
history_text += f"**💬 답변:** {entry['answer']}\n\n"
|
59 |
+
|
60 |
+
if entry['sources']:
|
61 |
+
history_text += f"**📚 출처:** {', '.join(entry['sources'])}\n\n"
|
62 |
+
|
63 |
+
history_text += "---\n\n"
|
64 |
+
|
65 |
+
return history_text
|
66 |
+
|
67 |
+
def clear_history():
|
68 |
+
"""대화 기록을 삭제하는 함수"""
|
69 |
+
global conversation_history
|
70 |
+
conversation_history = []
|
71 |
+
return "대화 기록이 삭제되었습니다."
|
72 |
+
|
73 |
+
def extract_source_info(source_documents):
|
74 |
+
"""소스 문서에서 출처 정보를 추출하는 함수"""
|
75 |
+
sources = []
|
76 |
+
|
77 |
+
for doc in source_documents:
|
78 |
+
# 메타데이터에서 소스 정보 추출
|
79 |
+
if hasattr(doc, 'metadata') and doc.metadata:
|
80 |
+
source = doc.metadata.get('source', '')
|
81 |
+
if source:
|
82 |
+
# 파일명에서 규정명 추출 (예: "학사규정.pdf" -> "학사규정")
|
83 |
+
source_name = os.path.basename(source).replace('.pdf', '').replace('.txt', '')
|
84 |
+
if source_name and source_name not in sources:
|
85 |
+
sources.append(source_name)
|
86 |
+
|
87 |
+
# 문서 내용에서 규정명 패턴 찾기
|
88 |
+
if hasattr(doc, 'page_content'):
|
89 |
+
content = doc.page_content
|
90 |
+
# 일반적인 규정명 패턴들
|
91 |
+
import re
|
92 |
+
patterns = [
|
93 |
+
r'([가-힣\s]+규정)',
|
94 |
+
r'([가-힣\s]+규칙)',
|
95 |
+
r'([가-힣\s]+세칙)',
|
96 |
+
r'([가-힣\s]+지침)',
|
97 |
+
r'([가-힣\s]+운영요강)',
|
98 |
+
r'([가-힣\s]+관리요령)'
|
99 |
+
]
|
100 |
+
|
101 |
+
for pattern in patterns:
|
102 |
+
matches = re.findall(pattern, content)
|
103 |
+
for match in matches:
|
104 |
+
clean_match = match.strip()
|
105 |
+
if len(clean_match) > 2 and clean_match not in sources:
|
106 |
+
sources.append(clean_match)
|
107 |
+
break # 첫 번째 매치만 사용
|
108 |
+
|
109 |
+
return sources[:3] # 최대 3개까지만 반환
|
110 |
+
|
111 |
def debug_file_system():
|
112 |
"""파일 시스템 상태를 자세히 확인하는 함수"""
|
113 |
import os
|
|
|
416 |
)
|
417 |
|
418 |
def respond_with_groq(question, selected_q, model):
|
419 |
+
"""질문에 대한 답변을 생성하는 함수 - 개선 버전 (출처 정보 포함)"""
|
420 |
|
421 |
# 선택된 질문이 있으면 그것을 사용
|
422 |
if selected_q != "직접 입력":
|
423 |
question = selected_q
|
424 |
|
425 |
if not question.strip():
|
426 |
+
return "질문을 입력해주세요.", ""
|
427 |
|
428 |
if not GROQ_API_KEY:
|
429 |
+
error_msg = "❌ GROQ API 키가 설정되지 않았습니다. Hugging Face Spaces의 Settings에서 GROQ_API_KEY를 설정해주세요."
|
430 |
+
save_conversation(question, error_msg)
|
431 |
+
return error_msg, get_conversation_history()
|
432 |
|
433 |
# 통합된 벡터스토어가 로드되지 않은 경우 재시도
|
434 |
if not combined_vectorstore:
|
|
|
437 |
if not success:
|
438 |
# 디버깅 정보 출력
|
439 |
debug_file_system()
|
440 |
+
error_msg = """❌ 벡터스토어를 로드할 수 없습니다.
|
441 |
|
442 |
가능한 원인:
|
443 |
1. 벡터스토어 파일이 올바르게 업로드되지 않음
|
|
|
447 |
1. vectorstore 폴더들이 제대로 업로드되었는지 확인
|
448 |
2. 각 폴더에 index.faiss와 index.pkl 파일이 있는지 확인
|
449 |
3. Git LFS를 사용해 큰 파일들을 관리해보세요"""
|
450 |
+
save_conversation(question, error_msg)
|
451 |
+
return error_msg, get_conversation_history()
|
452 |
|
453 |
try:
|
454 |
print(f"🔍 질문: {question}")
|
|
|
474 |
print(f"🔍 검색된 문서 수: {len(docs)}")
|
475 |
except Exception as e:
|
476 |
print(f"❌ 검색 오류: {e}")
|
477 |
+
error_msg = f"❌ 문서 검색 중 오류가 발생했습니다: {str(e)}"
|
478 |
+
save_conversation(question, error_msg)
|
479 |
+
return error_msg, get_conversation_history()
|
480 |
|
481 |
# QA 체인 생성
|
482 |
qa_chain = RetrievalQA.from_chain_type(
|
|
|
489 |
|
490 |
# 답변 생성
|
491 |
result = qa_chain({"query": question})
|
492 |
+
answer = result['result']
|
493 |
+
source_documents = result.get('source_documents', [])
|
494 |
+
|
495 |
+
# 출처 정보 추출
|
496 |
+
sources = extract_source_info(source_documents)
|
497 |
+
|
498 |
+
# 답변에 출처 정보 추가
|
499 |
+
if sources:
|
500 |
+
source_text = f"\n\n📚 **출처:** {', '.join(sources)}"
|
501 |
+
final_answer = answer + source_text
|
502 |
+
else:
|
503 |
+
final_answer = answer
|
504 |
+
sources = []
|
505 |
+
|
506 |
+
# 대화 기록 저장
|
507 |
+
save_conversation(question, final_answer, sources)
|
508 |
+
|
509 |
+
return final_answer, get_conversation_history()
|
510 |
|
511 |
except Exception as e:
|
512 |
import traceback
|
513 |
error_details = traceback.format_exc()
|
514 |
print(f"❌ 상세 오류 정보:\n{error_details}")
|
515 |
+
error_msg = f"""❌ 답변 생성 중 오류가 발생했습니다: {str(e)}
|
516 |
디버깅 정보:
|
517 |
- 벡터스토어 로드됨: {combined_vectorstore is not None}
|
518 |
- API 키 설정됨: {GROQ_API_KEY is not None}
|
519 |
- 모델: {model}
|
520 |
관리자에게 위 정보와 함께 문의해주세요."""
|
521 |
+
|
522 |
+
save_conversation(question, error_msg)
|
523 |
+
return error_msg, get_conversation_history()
|
524 |
|
525 |
def update_question(selected):
|
526 |
"""드롭다운 선택 시 질문을 업데이트하는 함수"""
|
|
|
528 |
return selected
|
529 |
return ""
|
530 |
|
531 |
+
def handle_clear_history():
|
532 |
+
"""기록 삭제 버튼 핸들러"""
|
533 |
+
clear_msg = clear_history()
|
534 |
+
return clear_msg, ""
|
535 |
+
|
536 |
# 앱 시작시 벡터스토어들 로드
|
537 |
print("🚀 앱 시작 - 벡터스토어 로딩 중...")
|
538 |
vectorstores_loaded = load_all_vectorstores()
|
|
|
561 |
</div>
|
562 |
""")
|
563 |
|
564 |
+
# 탭 생성
|
565 |
+
with gr.Tabs():
|
566 |
+
# 메인 Q&A 탭
|
567 |
+
with gr.TabItem("💬 질문하기"):
|
568 |
+
with gr.Row():
|
569 |
+
with gr.Column(scale=1):
|
570 |
+
question_dropdown = gr.Dropdown(
|
571 |
+
choices=["직접 입력"] + suggested_questions,
|
572 |
+
label="💡 자주 묻는 질문",
|
573 |
+
value="직접 입력"
|
574 |
+
)
|
575 |
|
576 |
+
question_input = gr.Textbox(
|
577 |
+
label="❓ 질문을 입력하세요",
|
578 |
+
placeholder="예: 졸업 요건은 무엇인가요?",
|
579 |
+
lines=3
|
580 |
+
)
|
581 |
|
582 |
+
submit_btn = gr.Button("답변 받기", variant="primary", size="lg")
|
583 |
|
584 |
+
model_choice = gr.Radio(
|
585 |
+
choices=["llama3-70b-8192", "llama3-8b-8192"],
|
586 |
+
label="🤖 AI 모델 선택",
|
587 |
+
value="llama3-70b-8192"
|
588 |
+
)
|
589 |
|
590 |
+
with gr.Column(scale=2):
|
591 |
+
output = gr.Textbox(
|
592 |
+
label="💬 답변",
|
593 |
+
lines=15,
|
594 |
+
max_lines=20,
|
595 |
+
show_copy_button=True
|
596 |
+
)
|
597 |
+
|
598 |
+
# 대화 기록 탭
|
599 |
+
with gr.TabItem("📋 대화 기록"):
|
600 |
+
with gr.Row():
|
601 |
+
with gr.Column():
|
602 |
+
gr.HTML("""
|
603 |
+
<div style="text-align: center; padding: 15px; background-color: #f8f9fa; border-radius: 8px; margin-bottom: 15px;">
|
604 |
+
<h3>🕐 이전 대화 기록</h3>
|
605 |
+
<p>지금까지의 질문과 답변 내역을 확인할 수 있습니다.</p>
|
606 |
+
</div>
|
607 |
+
""")
|
608 |
+
|
609 |
+
refresh_btn = gr.Button("🔄 기록 새로고침", variant="secondary")
|
610 |
+
clear_history_btn = gr.Button("🗑️ 기록 삭제", variant="stop")
|
611 |
+
|
612 |
+
history_output = gr.Markdown(
|
613 |
+
value="아직 대화 기록이 없습니다.",
|
614 |
+
label="대화 기록"
|
615 |
+
)
|
616 |
|
617 |
# 이벤트 연결
|
618 |
submit_btn.click(
|
619 |
fn=respond_with_groq,
|
620 |
inputs=[question_input, question_dropdown, model_choice],
|
621 |
+
outputs=[output, history_output]
|
622 |
)
|
623 |
|
624 |
question_dropdown.change(
|
|
|
626 |
inputs=question_dropdown,
|
627 |
outputs=question_input
|
628 |
)
|
629 |
+
|
630 |
+
refresh_btn.click(
|
631 |
+
fn=get_conversation_history,
|
632 |
+
outputs=history_output
|
633 |
+
)
|
634 |
+
|
635 |
+
clear_history_btn.click(
|
636 |
+
fn=handle_clear_history,
|
637 |
+
outputs=[history_output, output]
|
638 |
+
)
|
639 |
|
640 |
# 앱 실행
|
641 |
if __name__ == "__main__":
|