다른 명령
알겠습니다. 요청하신 내용을 반영하여 1. **문단(Paragraph) 단위로 텍스트 파일을 섹션 분할 및 임베딩/검색** 2. **더 강화된 확장 UI (파일 업로드, 검색 결과 하이라이트, 검색 범위 설명, 대화 이력 다운로드 등)** 기능이 적용된 Streamlit 예제 코드를 미디어위키 양식으로 정리해 드립니다.
---
Streamlit 기반 문단 단위 검색 및 확장 UI 챗봇
주요 기능
- 텍스트 문서를 문단(빈 줄 기준)으로 분할하여 정교한 검색
- 파일 업로드 UI 지원 (업로드 또는 폴더 선택 중 선택)
- 챗봇 대화 이력 다운로드
- 검색 결과 내 문단/문서 정보, 유사도, 하이라이트 표시
- 검색 범위 안내/설명
- 다크모드에 친화적인 UI 요소
필수 패키지 설치
pip install streamlit sentence-transformers chromadb
전체 코드
import os import streamlit as st from sentence_transformers import SentenceTransformer import chromadb from chromadb.config import Settings import tempfile import re # 문단 분리 (빈 줄 기준) def split_paragraphs(text): paragraphs = re.split(r'\n\s*\n', text.strip()) return [para.strip().replace('\n', ' ') for para in paragraphs if para.strip()] # 파일 로딩 및 문단 분할 def load_docs_and_paragraphs(file_paths): doc_data = [] for path in file_paths: with open(path, 'r', encoding='utf-8') as f: text = f.read() paragraphs = split_paragraphs(text) file_name = os.path.basename(path) for i, p in enumerate(paragraphs): doc_data.append({ 'file': file_name, 'paragraph': p, 'idx': i }) return doc_data # 업로드 파일 임시저장 def save_uploaded_files(uploaded_files): tmp_dir = tempfile.mkdtemp() file_paths = [] for file in uploaded_files: path = os.path.join(tmp_dir, file.name) with open(path, 'wb') as out: out.write(file.read()) file_paths.append(path) return file_paths # 벡터 임베딩/DB 구축 (문단 단위) @st.cache_resource def build_vector_db(doc_data): model = SentenceTransformer("paraphrase-MiniLM-L6-v2") chroma_client = chromadb.Client(Settings(chroma_db_impl="mem")) collection = chroma_client.create_collection(name="paragraphs") paragraphs = [d["paragraph"] for d in doc_data] metadatas = [{"file": d["file"], "paragraph_idx": d["idx"]} for d in doc_data] ids = [f"{d['file']}#{d['idx']}" for d in doc_data] if paragraphs: embeddings = model.encode(paragraphs, show_progress_bar=True).tolist() collection.add( documents=paragraphs, metadatas=metadatas, ids=ids, embeddings=embeddings ) else: embeddings = [] return collection, model # 검색 및 하이라이트 def search_paragraphs(user_query, collection, model, top_k): query_emb = model.encode([user_query]).tolist()[0] results = collection.query(query_embeddings=[query_emb], n_results=top_k) response = [] for i in range(len(results["ids"][0])): para = results["documents"][0][i] meta = results["metadatas"][0][i] score = results["distances"][0][i] file = meta["file"] idx = meta["paragraph_idx"] response.append((file, idx, para, score)) return response # 하이라이트 (검색어 주요 단어만) def highlight_keywords(text, query): # 주요 단어 추출(간단 버전) key_terms = [w for w in re.findall(r'\w+', query) if len(w) > 1] for t in set(key_terms): text = re.sub(f"({re.escape(t)})", r"<mark>\1</mark>", text, flags=re.IGNORECASE) return text # 대화 이력 다운로드용 텍스트 변환 def chat_history_to_text(chat_history): log = "" for q, resparas in chat_history: log += f"[질문]\n{q}\n" for idx, (fname, para_idx, answer, sim) in enumerate(resparas): log += f"- ({fname}) {para_idx+1}번째 문단, 유사도: {round(1-sim, 3)}\n{answer}\n" log += "\n" return log # UI 시작 st.set_page_config(page_title="문단 기반 챗봇", layout="wide") st.title("📚 문단 단위 검색 & 확장UI 챗봇") # --- 검색 데이터 소스 선택 UI --- with st.sidebar: st.header("데이터 업로드/폴더 선택") method = st.radio("데이터 소스", ['폴더 경로', '파일 업로드']) file_paths = [] if method == '폴더 경로': folder = st.text_input("텍스트 파일 폴더 경로", "./textfiles") if folder and os.path.exists(folder): file_paths = [os.path.join(folder, f) for f in os.listdir(folder) if f.endswith('.txt')] else: st.warning("유효한 폴더 경로를 입력하세요.") else: uploaded_files = st.file_uploader("txt 파일 1개 이상 업로드", type=['txt'], accept_multiple_files=True) if uploaded_files: file_paths = save_uploaded_files(uploaded_files) st.markdown("---") st.info("문단은 txt 파일 내에서 빈 줄(Enter 2번)로 구분됩니다.\n문단 단위로 검색이 이뤄집니다.\n검색 결과 하이라이트 제공.") top_k = st.slider("검색결과 개수(top_k)", 1, 10, 3) st.caption("챗봇이 보여주는 답변 개수 조절") # 데이터 로딩 및 벡터화 if not file_paths: st.warning("🛑 사용할 .txt 파일이 없습니다. 좌측에서 파일을 올리거나 경로를 선택해 주세요.") st.stop() # 문단 데이터 준비 with st.spinner("문단 분할/벡터 임베딩 중..."): doc_data = load_docs_and_paragraphs(file_paths) if not doc_data: st.error("문단 데이터가 없습니다. 파일 내용을 확인하세요.") st.stop() collection, model = build_vector_db(doc_data) # 컬럼 UI col1, col2 = st.columns([1,2]) # 왼쪽 : 문서 리스트 및 정보 with col1: st.subheader("📄 문서/문단 요약") doc_set = set([d['file'] for d in doc_data]) st.markdown(f"- **문서 수:** {len(doc_set)}개") st.markdown(f"- **전체 문단 수:** {len(doc_data)}개") fselect = st.selectbox("문서별 보기", ["전체 보기"] + sorted(doc_set)) # 원하는 문서만 필터링 filtered_data = doc_data if fselect=="전체 보기" else [d for d in doc_data if d['file']==fselect] for d in filtered_data[:20]: # 최대 20개만 미리보기 st.markdown(f"<div style='margin-bottom:8px;'><span style='color:#888'>[{d['file']},{d['idx']+1}번문단]</span><br><span style='font-size:13.5px;color:#444'>{d['paragraph'][:110]}{'...' if len(d['paragraph'])>110 else ''}</span></div>", unsafe_allow_html=True) if len(filtered_data) > 20: st.caption("※ 문단이 많아 미리보기는 20건까지만 표시합니다.") # 오른쪽 : 챗봇 인터페이스, 답변, 대화 이력 with col2: st.subheader("💬 검색질문 & 문단 단위 답변") if "chat_history" not in st.session_state: st.session_state.chat_history = [] with st.form(key="ask_form", clear_on_submit=True): user_query = st.text_area("당신의 질문을 입력하세요") submitted = st.form_submit_button("질문 및 검색") if submitted and user_query.strip(): answers = search_paragraphs(user_query, collection, model, top_k=top_k) st.session_state.chat_history.append((user_query, answers)) if st.session_state.chat_history: st.markdown("---") for question, resparas in reversed(st.session_state.chat_history[-6:]): # 최근 6개만 st.markdown(f"<div style='background:#eaf7ff;border-radius:8px;padding:8px 10px 4px 10px;margin-bottom:8px;'><b>🙋 질문:</b> {question}</div>", unsafe_allow_html=True) for idx, (fname, para_idx, answer, sim) in enumerate(resparas): # 하이라이트 적용 highlighted = highlight_keywords(answer, question) st.markdown( f"<div style='background:#f8faff;border-radius:6px;padding:10px;margin-bottom:6px;'>" f"<b>🔎 관련문단 {idx+1}:</b> <span style='color:#2a7af3'>{fname}</span> " f"<span style='color:#888'>{para_idx+1}번문단</span> (유사도: <b>{round(1-sim,3)}</b>)" f"<br><div style='max-height:120px;overflow:auto;font-size:15px'>{highlighted} ...</div>" f"</div>", unsafe_allow_html=True) with st.expander("⬇️ 대화 이력 다운로드"): chat_log = chat_history_to_text(st.session_state.chat_history) st.download_button("대화로그 TXT 내려받기", data=chat_log, file_name="chatbot_log.txt") else: st.info("질문을 입력하면 문단별로 가장 유사한 답변을 찾아줍니다.") st.caption("ⓒ 예시 코드. 문의/기능 개선 요청 환영합니다.")
추가 설명
- 업로드 또는 폴더선택 중 하나만 사용
- 문단 단위 검색으로 Q&A 품질이 향상됨
- 결과에 검색 질의어와 매칭되는 부분이 하이라이트됨
- 대화 이력 다운로드 버튼으로 전체 세션의 Q&A 저장 가능
- 검색결과 수(top_k) 슬라이더로 조절
실행 방법
1. 위 코드를 예시로 `paragraphchat_ui.py`로 저장 2. 터미널에서 실행
streamlit run paragraphchat_ui.py
---
심층 검색, 답변 요약, 다중 파일 업로드 병행, 기타 UI 커스터마이징 등 추가 확장 검토