다른 명령
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
# client = chromadb.Client()
# 문단 분리 (빈 줄 기준)
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"))
chroma_client = chromadb.Client()
# client = chromadb.PersistentClient(path="E:\\DEV\\streamlit\\")
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 커스터마이징 등 추가 확장 검토