Merge remote-tracking branch 'origin/CODEX' into main_laptop_1 — 챗봇 출처 보기 · 여러 줄 입력(전역 깃 싱크)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LLCk2y5xE8YGVgjbTntWsY
This commit is contained in:
@@ -8,7 +8,7 @@ from B01_Dashboard import B01_Dashboard_Knowledge_Chat as chat
|
||||
|
||||
|
||||
def test_greeting_answers_without_search_or_api_key(monkeypatch):
|
||||
monkeypatch.delenv("GEMINI_API_KEY", raising=False)
|
||||
monkeypatch.delenv("NVIDIA_API_KEY", raising=False)
|
||||
monkeypatch.setattr(
|
||||
chat, "search", lambda _question: (_ for _ in ()).throw(AssertionError("searched"))
|
||||
)
|
||||
@@ -28,16 +28,55 @@ def test_natural_question_finds_term_without_particle(monkeypatch):
|
||||
assert chat.search("일위대가가 뭐야")[0]["path"] == "original/원가계산/품셈.md"
|
||||
|
||||
|
||||
def test_source_endpoint_only_reads_allowed_original_markdown(monkeypatch, tmp_path):
|
||||
original = tmp_path / "original"
|
||||
allowed = original / "법령" / "현행.md"
|
||||
excluded = original / "실무문서" / "사례.md"
|
||||
outside = tmp_path / "technical_info" / "가이드.md"
|
||||
allowed.parent.mkdir(parents=True)
|
||||
excluded.parent.mkdir()
|
||||
outside.parent.mkdir()
|
||||
allowed.write_text("# 허용 원문", encoding="utf-8")
|
||||
excluded.write_text("비공개", encoding="utf-8")
|
||||
outside.write_text("중복 자료", encoding="utf-8")
|
||||
monkeypatch.setattr(chat, "KNOWLEDGE", tmp_path)
|
||||
|
||||
result = asyncio.run(chat.read_source(chat.SourceRequest(path="original/법령/현행.md"), {}))
|
||||
assert result["content"] == "# 허용 원문"
|
||||
for path in (
|
||||
"original/실무문서/사례.md",
|
||||
"technical_info/가이드.md",
|
||||
"../technical_info/가이드.md",
|
||||
):
|
||||
with pytest.raises(HTTPException) as error:
|
||||
asyncio.run(chat.read_source(chat.SourceRequest(path=path), {}))
|
||||
assert error.value.status_code == 404
|
||||
|
||||
|
||||
def test_source_request_words_do_not_displace_matching_rule(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
chat,
|
||||
"_sections",
|
||||
lambda: [
|
||||
("original/법률/일반기준/현행_2026.md", "임도 문서 근거로 " * 10),
|
||||
("original/행정규칙/임도시설기준/별표7.md", "임도 종단기울기 기준 7%"),
|
||||
],
|
||||
)
|
||||
assert chat.search("임도 종단기울기 기준을 문서 근거로 설명해줘")[0]["path"].endswith(
|
||||
"별표7.md"
|
||||
)
|
||||
|
||||
|
||||
def test_missing_api_key_is_reported_before_search(monkeypatch):
|
||||
monkeypatch.delenv("GEMINI_API_KEY", raising=False)
|
||||
monkeypatch.delenv("NVIDIA_API_KEY", raising=False)
|
||||
monkeypatch.setattr(chat, "search", lambda _question: [])
|
||||
with pytest.raises(HTTPException) as error:
|
||||
asyncio.run(chat.ask_knowledge_chat(chat.ChatRequest(question="일위대가가 뭐야"), {}))
|
||||
assert error.value.status_code == 503
|
||||
|
||||
|
||||
def test_busy_gemini_is_retried_and_reported(monkeypatch, caplog):
|
||||
monkeypatch.setenv("GEMINI_API_KEY", "test-key")
|
||||
def test_busy_nvidia_is_retried_and_reported(monkeypatch, caplog):
|
||||
monkeypatch.setenv("NVIDIA_API_KEY", "test-key")
|
||||
monkeypatch.setattr(chat, "search", lambda _question: [])
|
||||
calls = []
|
||||
|
||||
@@ -69,8 +108,8 @@ def test_busy_gemini_is_retried_and_reported(monkeypatch, caplog):
|
||||
assert "일위대가가 뭐야" not in caplog.text
|
||||
|
||||
|
||||
def test_gemini_timeout_is_reported(monkeypatch):
|
||||
monkeypatch.setenv("GEMINI_API_KEY", "test-key")
|
||||
def test_nvidia_timeout_is_reported(monkeypatch):
|
||||
monkeypatch.setenv("NVIDIA_API_KEY", "test-key")
|
||||
monkeypatch.setattr(chat, "search", lambda _question: [])
|
||||
|
||||
class Client:
|
||||
@@ -99,21 +138,26 @@ def test_chat_reads_only_original_except_excluded_folders(monkeypatch, tmp_path)
|
||||
outside = tmp_path / "별도자료"
|
||||
private = tmp_path / "original" / "실무문서"
|
||||
stmate = tmp_path / "original" / "원가계산" / "STmate"
|
||||
stmate_analysis = tmp_path / "original" / "경쟁사 프로그램" / "STmate 분석"
|
||||
dasan = tmp_path / "original" / "원가계산" / "다산소프트"
|
||||
public.mkdir(parents=True)
|
||||
guide.mkdir()
|
||||
outside.mkdir()
|
||||
private.mkdir()
|
||||
stmate.mkdir(parents=True)
|
||||
stmate_analysis.mkdir(parents=True)
|
||||
dasan.mkdir()
|
||||
(public / "현행.md").write_text("# 임도 기준\n" + "임도 설치 기준 내용 " * 10, encoding="utf-8")
|
||||
(guide / "_검토.md").write_text("# 임도 기준\n기술정보 안내 " * 10, encoding="utf-8")
|
||||
(outside / "자료.md").write_text("# 임도 기준\n별도 자료 " * 10, encoding="utf-8")
|
||||
(private / "사례.md").write_text("# 임도 기준\n비공개 사례 " * 10, encoding="utf-8")
|
||||
(stmate / "자료.md").write_text("# 임도 기준\nSTmate 자료 " * 10, encoding="utf-8")
|
||||
(stmate_analysis / "자료.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")
|
||||
monkeypatch.setenv("NVIDIA_API_KEY", "test-key")
|
||||
chat._sections.cache_clear()
|
||||
sent = {}
|
||||
|
||||
@@ -124,7 +168,7 @@ def test_chat_reads_only_original_except_excluded_folders(monkeypatch, tmp_path)
|
||||
pass
|
||||
|
||||
def json(self):
|
||||
return {"candidates": [{"content": {"parts": [{"text": "설명 [1]"}]}}]}
|
||||
return {"choices": [{"message": {"content": "설명 [1]"}}]}
|
||||
|
||||
class Client:
|
||||
def __init__(self, timeout):
|
||||
@@ -137,14 +181,35 @@ def test_chat_reads_only_original_except_excluded_folders(monkeypatch, tmp_path)
|
||||
pass
|
||||
|
||||
async def post(self, url, *, headers, json):
|
||||
sent["prompt"] = json["contents"][0]["parts"][0]["text"]
|
||||
sent["thinking_level"] = json["generationConfig"]["thinkingConfig"]["thinkingLevel"]
|
||||
sent["url"] = url
|
||||
sent["authorization"] = headers["Authorization"]
|
||||
sent["model"] = json["model"]
|
||||
sent["prompt"] = json["messages"][0]["content"]
|
||||
sent["reasoning_effort"] = json["reasoning_effort"]
|
||||
sent["stream"] = json["stream"]
|
||||
return Response()
|
||||
|
||||
monkeypatch.setattr(chat.httpx, "AsyncClient", Client)
|
||||
result = asyncio.run(chat.ask_knowledge_chat(chat.ChatRequest(question="임도 기준"), {}))
|
||||
result = asyncio.run(
|
||||
chat.ask_knowledge_chat(
|
||||
chat.ChatRequest(
|
||||
question="그중 몇 퍼센트야?",
|
||||
history=[
|
||||
chat.ChatTurn(role="user", content="임도 기준 알려줘"),
|
||||
chat.ChatTurn(role="assistant", content="임도 기준입니다."),
|
||||
],
|
||||
),
|
||||
{},
|
||||
)
|
||||
)
|
||||
assert result["answer"] == "설명 [1]"
|
||||
assert sent["thinking_level"] == "low"
|
||||
assert sent["url"] == "https://integrate.api.nvidia.com/v1/chat/completions"
|
||||
assert sent["authorization"] == "Bearer test-key"
|
||||
assert sent["model"] == "nvidia/nemotron-3-super-120b-a12b"
|
||||
assert sent["reasoning_effort"] == "none"
|
||||
assert sent["stream"] is False
|
||||
assert "이전 대화" in sent["prompt"]
|
||||
assert "사용자: 임도 기준 알려줘" in sent["prompt"]
|
||||
paths = {source["path"] for source in result["sources"]}
|
||||
assert paths == {"original/법률/현행.md"}
|
||||
assert "실무문서" not in sent["prompt"]
|
||||
|
||||
Reference in New Issue
Block a user