Spaces:
Sleeping
Sleeping
| import os | |
| import gradio as gr | |
| import urllib3 | |
| 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 | |
| # SSL 경고 제거 | |
| urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) | |
| # 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}") | |
| print("현재 작업 디렉토리:", os.getcwd()) | |
| print("폴더 경로 존재 여부:", os.path.exists(folder_path)) | |
| # 방법 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}") | |
| print("index.faiss 존재 여부:", os.path.exists(os.path.join(folder_path, "index.faiss"))) | |
| print("index.pkl 존재 여부:", os.path.exists(os.path.join(folder_path, "index.pkl"))) | |
| # 방법 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: | |
| print(f"🔍 질문: {question}") | |
| retriever = combined_vectorstore.as_retriever( | |
| search_type="mmr", | |
| search_kwargs={"k": 10, "lambda_mult": 0.5} | |
| ) | |
| print(f"🔍 질문: {question}") | |
| docs = retriever.invoke(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() |