메뉴 여닫기
개인 메뉴 토글
로그인하지 않음
만약 지금 편집한다면 당신의 IP 주소가 공개될 수 있습니다.

개인 PC 텍스트 검색 챗봇 웹 화면

데브카페
Devcafe (토론 | 기여)님의 2025년 6월 24일 (화) 01:48 판 (새 문서: 알겠습니다. 요청하신 내용을 반영하여 1. **문단(Paragraph) 단위로 텍스트 파일을 섹션 분할 및 임베딩/검색** 2. **더 강화된 확장 UI (파일 업로드, 검색 결과 하이라이트, 검색 범위 설명, 대화 이력 다운로드 등)** 기능이 적용된 Streamlit 예제 코드를 미디어위키 양식으로 정리해 드립니다. --- = Streamlit 기반 문단 단위 검색 및 확장 UI 챗봇 = == 주요 기능 == * 텍스...)
(차이) ← 이전 판 | 최신판 (차이) | 다음 판 → (차이)

알겠습니다. 요청하신 내용을 반영하여 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 커스터마이징 등 추가 확장 검토

Comments