diff --git a/B01_Dashboard/B01_Dashboard_Knowledge_Chat.py b/B01_Dashboard/B01_Dashboard_Knowledge_Chat.py index 7438a4910..26be22a0e 100644 --- a/B01_Dashboard/B01_Dashboard_Knowledge_Chat.py +++ b/B01_Dashboard/B01_Dashboard_Knowledge_Chat.py @@ -1,4 +1,4 @@ -"""원문 Markdown 검색과 Gemini 설명. 실무문서는 어떤 경로에서도 읽지 않는다.""" +"""지식DB Markdown 검색과 Gemini 설명. 제외 폴더는 읽지 않는다.""" import os import re @@ -12,7 +12,8 @@ from pydantic import BaseModel, Field from common_util.common_util_auth import verify_session router = APIRouter(prefix="/knowledge-chat", tags=["B01_KnowledgeChat"]) -ORIGINAL = Path(__file__).resolve().parents[1] / "resources" / "knowledge" / "original" +KNOWLEDGE = Path(__file__).resolve().parents[1] / "resources" / "knowledge" +EXCLUDED = ("실무문서", "stmate", "다산소프트") MODEL = "gemini-3.8-flash" @@ -21,24 +22,26 @@ class ChatRequest(BaseModel): def _allowed(path: Path) -> bool: - return path.suffix.lower() == ".md" and "실무문서" not in path.relative_to(ORIGINAL).parts + return path.suffix.lower() == ".md" and not any( + excluded in part.casefold() + for part in path.relative_to(KNOWLEDGE).parts + for excluded in EXCLUDED + ) @lru_cache(maxsize=1) def _sections() -> list[tuple[str, str]]: sections = [] - for path in ORIGINAL.rglob("*.md"): + for path in KNOWLEDGE.rglob("*.md"): if not _allowed(path): continue - relative = path.relative_to(ORIGINAL).as_posix() - if path.name in {"CHANGELOG.md", "_index.md"} or path.name.startswith("_"): - continue + relative = path.relative_to(KNOWLEDGE).as_posix() content = path.read_text(encoding="utf-8", errors="replace") for part in re.split(r"(?=^#{1,4} )", content, flags=re.MULTILINE): part = part.strip() for start in range(0, len(part), 1800): snippet = part[start : start + 1800].strip() - if len(snippet) >= 40: + if snippet: sections.append((relative, snippet)) return sections diff --git a/resources/tester/test_b01_knowledge_chat.py b/resources/tester/test_b01_knowledge_chat.py index 3ae7cb687..237e79bb1 100644 --- a/resources/tester/test_b01_knowledge_chat.py +++ b/resources/tester/test_b01_knowledge_chat.py @@ -3,14 +3,23 @@ import asyncio from B01_Dashboard import B01_Dashboard_Knowledge_Chat as chat -def test_chat_excludes_practice_documents_from_gemini(monkeypatch, tmp_path): - public = tmp_path / "법률" - private = tmp_path / "실무문서" - public.mkdir() +def test_chat_searches_all_knowledge_markdown_except_excluded_folders(monkeypatch, tmp_path): + public = tmp_path / "original" / "법률" + guide = tmp_path / "technical_info" + private = tmp_path / "original" / "실무문서" + stmate = tmp_path / "original" / "원가계산" / "STmate" + dasan = tmp_path / "original" / "원가계산" / "다산소프트" + public.mkdir(parents=True) + guide.mkdir() private.mkdir() + stmate.mkdir(parents=True) + dasan.mkdir() (public / "현행.md").write_text("# 임도 기준\n" + "임도 설치 기준 내용 " * 10, encoding="utf-8") + (guide / "_검토.md").write_text("# 임도 기준\n기술정보 안내 " * 10, encoding="utf-8") (private / "사례.md").write_text("# 임도 기준\n비공개 사례 " * 10, encoding="utf-8") - monkeypatch.setattr(chat, "ORIGINAL", tmp_path) + (stmate / "자료.md").write_text("# 임도 기준\nSTmate 자료 " * 10, encoding="utf-8") + (dasan / "자료.md").write_text("# 임도 기준\n다산소프트 자료 " * 10, encoding="utf-8") + monkeypatch.setattr(chat, "KNOWLEDGE", tmp_path) monkeypatch.setenv("GEMINI_API_KEY", "test-key") chat._sections.cache_clear() sent = {} @@ -39,7 +48,10 @@ def test_chat_excludes_practice_documents_from_gemini(monkeypatch, tmp_path): monkeypatch.setattr(chat.httpx, "AsyncClient", Client) result = asyncio.run(chat.ask_knowledge_chat(chat.ChatRequest(question="임도 기준"), {})) assert result["answer"] == "설명 [1]" - assert result["sources"][0]["path"] == "법률/현행.md" + paths = {source["path"] for source in result["sources"]} + assert paths == {"original/법률/현행.md", "technical_info/_검토.md"} assert "실무문서" not in sent["prompt"] assert "비공개 사례" not in sent["prompt"] + assert "STmate" not in sent["prompt"] + assert "다산소프트" not in sent["prompt"] chat._sections.cache_clear()