import os import gradio as gr import bs4 from langchain_community.document_loaders import WebBaseLoader from langchain.text_splitter import CharacterTextSplitter from langchain_community.embeddings import HuggingFaceEmbeddings from langchain.vectorstores import FAISS from langchain.chains import RetrievalQA from langchain_groq import ChatGroq from langchain_community.document_loaders import UnstructuredExcelLoader # 환경 변수로부터 Groq API Key 불러오기 groq_api_key = os.environ.get("GROQ_API_KEY", "") # 국가기록원 웹 문서 목록 urls = [ "https://archives.go.kr/next/newsearch/listSubjectContent.do?subjectFieldId=000011", "https://archives.go.kr/next/newsearch/listSubjectDescription.do?id=003140&pageFlag=A&sitePage=1-2-1", "https://archives.go.kr/next/newsearch/listSubjectDescription.do?id=003288&pageFlag=A&sitePage=1-2-1", "https://archives.go.kr/next/newsearch/listSubjectDescription.do?id=003290&pageFlag=A&sitePage=1-2-1", "https://archives.go.kr/next/newsearch/listSubjectDescription.do?id=003292&pageFlag=A&sitePage=1-2-1", "https://archives.go.kr/next/newsearch/listSubjectDescription.do?id=008757&pageFlag=A&sitePage=1-2-1", "https://archives.go.kr/next/newsearch/listSubjectDescription.do?id=003293&pageFlag=A&sitePage=1-2-1", "https://archives.go.kr/next/newsearch/listSubjectDescription.do?id=003294&pageFlag=A&sitePage=1-2-1", "https://archives.go.kr/next/newsearch/listSubjectDescription.do?id=003295&pageFlag=A&sitePage=1-2-1", "https://archives.go.kr/next/newsearch/listSubjectDescription.do?id=003289&pageFlag=A&sitePage=1-2-1", "https://archives.go.kr/next/newsearch/listSubjectDescription.do?id=010816&pageFlag=A&sitePage=1-2-1", "https://archives.go.kr/next/newsearch/listSubjectDescription.do?id=010817&pageFlag=A&sitePage=1-2-1", "https://archives.go.kr/next/newsearch/listSubjectDescription.do?id=009154&pageFlag=A&sitePage=1-2-1", "https://archives.go.kr/next/newsearch/listSubjectDescription.do?id=003260&pageFlag=A&sitePage=1-2-1", "https://archives.go.kr/next/newsearch/listSubjectDescription.do?id=003278&pageFlag=A&sitePage=1-2-1", "https://archives.go.kr/next/newsearch/listSubjectDescription.do?id=003281&pageFlag=A&sitePage=1-2-1", "https://archives.go.kr/next/newsearch/listSubjectDescription.do?id=003283&pageFlag=A&sitePage=1-2-1", "https://archives.go.kr/next/newsearch/listSubjectDescription.do?id=003284&pageFlag=A&sitePage=1-2-1", "https://archives.go.kr/next/newsearch/listSubjectDescription.do?id=003280&pageFlag=A&sitePage=1-2-1", "https://archives.go.kr/next/newsearch/listSubjectDescription.do?id=003282&pageFlag=A&sitePage=1-2-1", "https://archives.go.kr/next/newsearch/listSubjectDescription.do?id=003287&pageFlag=A&sitePage=1-2-1", "https://archives.go.kr/next/newsearch/listSubjectDescription.do?id=003286&pageFlag=A&sitePage=1-2-1", "https://archives.go.kr/next/newsearch/listSubjectDescription.do?id=003285&pageFlag=A&sitePage=1-2-1", "https://archives.go.kr/next/newsearch/listSubjectDescription.do?id=003279&pageFlag=A&sitePage=1-2-1", "https://archives.go.kr/next/newsearch/listSubjectDescription.do?id=003141&pageFlag=A&sitePage=1-2-1", "https://archives.go.kr/next/newsearch/listSubjectDescription.do?id=003143&pageFlag=A&sitePage=1-2-1", "https://archives.go.kr/next/newsearch/listSubjectDescription.do?id=003144&pageFlag=A&sitePage=1-2-1", "https://archives.go.kr/next/newsearch/listSubjectDescription.do?id=003142&pageFlag=A&sitePage=1-2-1", "https://archives.go.kr/next/newsearch/listSubjectDescription.do?id=008653&pageFlag=A&sitePage=1-2-1", "https://archives.go.kr/next/newsearch/listSubjectDescription.do?id=010827&pageFlag=A&sitePage=1-2-1", "https://archives.go.kr/next/newsearch/listSubjectDescription.do?id=008582&pageFlag=A&sitePage=1-2-1", "https://archives.go.kr/next/newsearch/listSubjectDescription.do?id=008663&pageFlag=A&sitePage=1-2-1", "https://archives.go.kr/next/newsearch/listSubjectDescription.do?id=008581&pageFlag=A&sitePage=1-2-1", "https://archives.go.kr/next/newsearch/listSubjectDescription.do?id=010828&pageFlag=A&sitePage=1-2-1", "https://archives.go.kr/next/newsearch/listSubjectDescription.do?id=010830&pageFlag=A&sitePage=1-2-1", "https://archives.go.kr/next/newsearch/listSubjectDescription.do?id=010831&pageFlag=A&sitePage=1-2-1", "https://archives.go.kr/next/newsearch/listSubjectDescription.do?id=003145&pageFlag=A&sitePage=1-2-1", "https://archives.go.kr/next/newsearch/listSubjectDescription.do?id=009425&pageFlag=A&sitePage=1-2-1", "https://archives.go.kr/next/newsearch/listSubjectDescription.do?id=003146&pageFlag=A&sitePage=1-2-1", "https://archives.go.kr/next/newsearch/listSubjectDescription.do?id=010821&pageFlag=A&sitePage=1-2-1", "https://archives.go.kr/next/newsearch/listSubjectDescription.do?id=003151&pageFlag=A&sitePage=1-2-1", "https://archives.go.kr/next/newsearch/listSubjectDescription.do?id=003149&pageFlag=A&sitePage=1-2-1", "https://archives.go.kr/next/newsearch/listSubjectDescription.do?id=003148&pageFlag=A&sitePage=1-2-1", "https://archives.go.kr/next/newsearch/listSubjectDescription.do?id=008655&pageFlag=A&sitePage=1-2-1", "https://archives.go.kr/next/newsearch/listSubjectDescription.do?id=008654&pageFlag=A&sitePage=1-2-1", "https://archives.go.kr/next/newsearch/listSubjectDescription.do?id=003150&pageFlag=A&sitePage=1-2-1", "https://archives.go.kr/next/newmanager/recodeRegister.do", "https://archives.go.kr/next/newtour/tourCourse.do", "https://archives.go.kr/next/newrecordsMngPro/recordsDonateInfo.do", "https://archives.go.kr/next/newdata/pepoleRecodPresentIntro.do", "https://archives.go.kr/next/newsearch/searchGuideList.do", "https://archives.go.kr/next/newsearch/searchGuideList.do?page=2", "https://archives.go.kr/next/newsearch/searchGuideDetail.do?guideSeq=441", "https://archives.go.kr/next/newsearch/searchGuideDetail.do?guideSeq=381", "https://archives.go.kr/next/newsearch/searchGuideDetail.do?guideSeq=341", "https://archives.go.kr/next/newsearch/searchGuideDetail.do?guideSeq=261", "https://archives.go.kr/next/newsearch/searchGuideDetail.do?guideSeq=227", "https://archives.go.kr/next/newsearch/searchGuideDetail.do?guideSeq=59", "https://archives.go.kr/next/newsearch/searchGuideDetail.do?guideSeq=30", "https://archives.go.kr/next/newsearch/searchGuideDetail.do?guideSeq=64", "https://archives.go.kr/next/newsearch/searchGuideDetail.do?guideSeq=321", "https://archives.go.kr/next/newsearch/searchGuideDetail.do?guideSeq=124", "https://archives.go.kr/next/newsearch/searchGuideDetail.do?guideSeq=267", "https://archives.go.kr/next/newsearch/searchGuideDetail.do?guideSeq=141", "https://archives.go.kr/next/newsearch/searchGuideDetail.do?guideSeq=149", "https://archives.go.kr/next/newsearch/searchGuideDetail.do?guideSeq=22" ] # 웹문서 로딩 loader = WebBaseLoader(web_paths=urls, bs_kwargs=dict(parse_only=bs4.SoupStrainer())) docs = loader.load() # 기록물 목록 엑셀 파일 excel_files = [ "교육 전반 관련 기록물 목록1.xls", "교육 전반 관련 기록물 목록2.xls", "교육 전반 관련 기록물 목록3.xls" ] # 엑셀 문서 로딩 excel_docs = [] for file in excel_files: loader = UnstructuredExcelLoader(file) excel_docs.extend(loader.load()) # 웹문서 + 엑셀문서 결합 docs.extend(excel_docs) # 문서 분할 splitter = CharacterTextSplitter(separator="\n", chunk_size=500, chunk_overlap=50) split_docs = splitter.split_documents(docs) # 임베딩 및 벡터 저장 및 리트리버 설정 embedding_model = HuggingFaceEmbeddings(model_name="snunlp/KR-SBERT-V40K-klueNLI-augSTS") vectorstore = FAISS.from_documents(split_docs, embedding_model) retriever = vectorstore.as_retriever() # LLM + QA 체인 llm = ChatGroq(groq_api_key=groq_api_key, model_name="llama3-70b-8192") qa_chain = RetrievalQA.from_chain_type(llm=llm, retriever=retriever, chain_type="stuff") # 예시 질문 example_questions = [ "기록물 열람 방법은 어떻게 되나요?", "견학신청에 대해 알 수 있나요?", "기록물 기증 절차는 어떻게 되나요?", "기록물 검색 길잡이가 무엇인가요?", "민중교육지 사건의 주제 유형은 무엇인가요?", "교육개혁시민운동연대의 배경은 무엇인가요?", "참교육 운동의 역사적 의의는 무엇인가요?", "학도호국단의 기록물들은 공개구분이 어떻게 되나요?", "AI 디지털 교과서의 기록물 중에 정부간행물은 몇건인가요?", "교육개혁심의회의 기록물은 몇건인가요?", "국민교육헌장의 기록물들이 어떤 제목인지 알 수 있나요?", "세계박람회(EXPO) 기록물의 개요는 무엇인가요?", "대학수학능력시험 관련 기록물의 기록물 생산정보를 알 수 있나요?", "한미동맹 기록물을 검색하고 싶은데, 키워드를 알 수 있나요?", "동학농민혁명의 기록물 수집 현황이 어떻게 되나요?" ] # 키워드 계층 구조 keyword_tree = { "교육 전반": { "교육 민주화운동": { "교원운동": { "교육 민주화선언": {}, "민중교육지 사건": {}, "참교육 운동": {} }, "학부모 운동": { "교육개혁시민운동연대": {} }, "학생운동": {} }, "교육 정보화 정책": { "AI 디지털 교과서": {}, "e러닝활성화": {}, "교육행정정보시스템(NEIS)": {} }, "교육개혁": { "교육개혁심의회": {}, "교육개혁위원회": { "5·31 교육개혁": {} }, "교육정책심의회": {}, "교육정책자문회의": {}, "교육혁신위원회": {}, "새교육공동체위원회": {}, "인력자원개발회의": {}, "장기종합계획심의회": {} }, "교육이념": { "국민교육헌장": { "학도호국단": {} }, "홍익인간 교육이념": { "일민주의": {} } }, "교육정책 관련 기관": { "한국교육개발원": {}, "한국교육과정평가원": {}, "한국교육방송공사": {} }, "학술진흥 정책": { "KERIS": {}, "대한민국학술원": {} }, "학제": { "학령인구 감소": {}, "학제 확정": {} }, "헌법의 교육조항과 변천": { "고등교육법": {}, "교육기본법": {}, "교육법 제정": {}, "교육에 관한 임시특례법": {}, "사립학교법": {}, "초·중등교육법": {} } }, "기록물 검색 길잡이": { "동학농민혁명": {}, "우편행정": {}, "대학수학능력시험": {}, "도시철도": { "도시철도 1호선": {} }, "재외동포": { "재외동포재단": {}, "남미 한인": {}, "중국 한인": {}, "고려인": {}, "재외동포": {}, "한미동맹": {}, "미국 한인": {}, "파독 광부 및 간호사": {}, "조선기술자": {} }, "한미동맹": {}, "공기업": { "한국전력공사": {} }, "박람회": { "세계박람회(EXPO)": {} } }, "검색 방향": { "교육 전반 관련": { "기록물 목록": { "관리번호": {}, "기록물 철 제목": {}, "기록물 건 제목": {}, "생산기관명": {}, "생산년도": {}, "기록물형태": { "일반문서류": {}, "정부간행물류": {}, "사진,필름류": {}, "녹음,동영상류": {} }, "공개구분": {} }, "주제 설명": { "주제유형": {}, "근거": {}, "배경(발생배경)": {}, "경과": {}, "내용": {}, "역사적 의의": {}, "참고자료": {}, "집필자": {} } }, "기록물 검색 길잡이 관련": { "개요": {}, "생산정보": {}, "이관 현황": {}, "소장 현황": {}, "정리 현황": {} } } } # 경로에서 하위 키워드 반환 def get_keywords(path): node = keyword_tree for key in path: node = node.get(key, {}) return list(node.keys()) def format_path(path): return " > ".join(path) if path else "교육 전반" def on_keyword_select(selected, path): new_path = path + [selected] next_keywords = get_keywords(new_path) formatted = format_path(new_path) return formatted, new_path, gr.update(choices=next_keywords) # Gradio 채팅 함수 def chat_with_history(user_input, history): if history is None: history = [] query = user_input.strip() + " 한국어로 답해주세요." result = qa_chain({"query": query}) answer = result.get("result", "답변을 찾을 수 없습니다.") # 메시지 포맷 맞추기 (딕셔너리 형태) history.append({"role": "user", "content": user_input}) history.append({"role": "assistant", "content": answer}) return "", history, history # Gradio 인터페이스 구성 with gr.Blocks() as demo: gr.Markdown("## 📚 국가기록원 챗봇") gr.Markdown( """### **국가기록원 정보 챗봇에 오신 것을 환영합니다!** 이 챗봇은 국가기록원에 보관된 다양한 기록물을 바탕으로 여러분의 궁금증을 쉽고 빠르게 해결해 드립니다. 국가기록원의 역할, 기록물 열람 방법, 견학 신청, 기증 절차 등의 공식 정보를 확인할 수 있으며, 교육 전반 분야에 관한 정보를 안내해 드립니다. 아래 입력창에 궁금한 내용을 자유롭게 입력해 보세요. 💡 질문을 어떻게 시작할지 고민 중이신가요? - **예시 질문 보기**에서 질문을 선택해 보세요. 기록물 정보 탐색이 처음이라도 쉽게 시작할 수 있습니다. - **검색 키워드 탐색** 기능을 이용해 보세요. 키워드를 참고하여 나만의 검색 방향을 정해보세요! """ ) chatbot = gr.Chatbot(label="기록원 챗봇", type="messages") with gr.Row(): dropdown = gr.Dropdown(choices=example_questions, label="📝 예시 질문 보기") msg = gr.Textbox(placeholder="질문을 입력하세요", label="💬 질문 입력", lines=1) state = gr.State([]) # 채팅 기록 path_state = gr.State([]) # 키워드 경로 상태 dropdown.change(lambda q: q, inputs=dropdown, outputs=msg) msg.submit(chat_with_history, inputs=[msg, state], outputs=[msg, chatbot, state]) with gr.Column(): gr.Markdown("### 🔍 검색 키워드 탐색") gr.Markdown( """ **검색 키워드 탐색 안내** 국가기록원의 다양한 기록물을 주제별로 분류한 키워드를 따라가며, 관심 있는 분야의 기록을 **계층적으로 쉽게 탐색**할 수 있습니다. """ ) keyword_path_display = gr.Textbox(label="현재 키워드 경로", interactive=False) keyword_selector = gr.Radio(choices=get_keywords([]), label="키워드 선택", value=None) keyword_selector.change( fn=on_keyword_select, inputs=[keyword_selector, path_state], outputs=[keyword_path_display, path_state, keyword_selector] ) # 뒤로가기 버튼 추가 def on_back_click(path): if path: path = path[:-1] next_keywords = get_keywords(path) formatted = format_path(path) return formatted, path, gr.update(choices=next_keywords) back_btn = gr.Button("🔙 한 단계 뒤로가기") back_btn.click( fn=on_back_click, inputs=[path_state], outputs=[keyword_path_display, path_state, keyword_selector] ) demo.launch()