Merge remote-tracking branch 'origin/CODEX' into sub_laptop_1

This commit is contained in:
2026-10-09 14:41:31 +09:00
2 changed files with 36 additions and 3 deletions
+30 -2
View File
@@ -1,10 +1,13 @@
"""지식DB Markdown 검색과 Gemini 설명. 제외 폴더는 읽지 않는다."""
import asyncio
import logging
import os
import re
import time
from functools import lru_cache
from pathlib import Path
from uuid import uuid4
import httpx
from fastapi import APIRouter, Depends, HTTPException
@@ -16,6 +19,7 @@ router = APIRouter(prefix="/knowledge-chat", tags=["B01_KnowledgeChat"])
KNOWLEDGE = Path(__file__).resolve().parents[1] / "resources" / "knowledge"
EXCLUDED = ("실무문서", "stmate", "다산소프트", "technical_info")
MODEL = "gemini-3.8-flash"
logger = logging.getLogger(__name__)
class ChatRequest(BaseModel):
@@ -149,21 +153,36 @@ async def ask_knowledge_chat(payload: ChatRequest, _session=Depends(verify_sessi
f"질문: {payload.question}"
)
)
request_id = uuid4().hex[:8]
started = time.monotonic()
try:
async with httpx.AsyncClient(timeout=30) as client:
for attempt in range(3):
response = await client.post(
f"https://generativelanguage.googleapis.com/v1beta/models/{MODEL}:generateContent",
headers={"x-goog-api-key": key},
json={"contents": [{"parts": [{"text": prompt}]}]},
json={
"contents": [{"parts": [{"text": prompt}]}],
"generationConfig": {"thinkingConfig": {"thinkingLevel": "low"}},
},
)
if response.status_code >= 400:
logger.warning(
"Gemini request failed id=%s model=%s attempt=%d status=%d elapsed=%.1fs prompt_chars=%d",
request_id,
MODEL,
attempt + 1,
response.status_code,
time.monotonic() - started,
len(prompt),
)
if response.status_code != 503 or attempt == 2:
break
await asyncio.sleep(attempt + 1)
if response.status_code == 503:
raise HTTPException(
status_code=503,
detail="Gemini 모델에 요청이 몰려 있습니다. 잠시 후 다시 질문해 주세요.",
detail=f"Gemini 모델에 요청이 몰려 있습니다. 잠시 후 다시 질문해 주세요. (문의번호 {request_id})",
)
response.raise_for_status()
answer = "".join(
@@ -180,12 +199,21 @@ async def ask_knowledge_chat(payload: ChatRequest, _session=Depends(verify_sessi
}.get(exc.response.status_code, "Gemini 요청에 실패했습니다. 잠시 후 다시 시도해 주세요.")
raise HTTPException(status_code=502, detail=detail) from exc
except httpx.TimeoutException as exc:
logger.warning(
"Gemini request timed out id=%s elapsed=%.1fs", request_id, time.monotonic() - started
)
raise HTTPException(
status_code=504,
detail="Gemini 3.8 Flash 응답이 지연되어 시간 초과됐습니다. "
"잠시 후 다시 시도해 주세요.",
) from exc
except (httpx.RequestError, ValueError) as exc:
logger.warning(
"Gemini request error id=%s type=%s elapsed=%.1fs",
request_id,
type(exc).__name__,
time.monotonic() - started,
)
raise HTTPException(
status_code=502,
detail="Gemini 서버에 연결하지 못했습니다. 잠시 후 다시 시도해 주세요.",
+6 -1
View File
@@ -36,7 +36,7 @@ def test_missing_api_key_is_reported_before_search(monkeypatch):
assert error.value.status_code == 503
def test_busy_gemini_is_retried_and_reported(monkeypatch):
def test_busy_gemini_is_retried_and_reported(monkeypatch, caplog):
monkeypatch.setenv("GEMINI_API_KEY", "test-key")
monkeypatch.setattr(chat, "search", lambda _question: [])
calls = []
@@ -64,6 +64,9 @@ def test_busy_gemini_is_retried_and_reported(monkeypatch):
asyncio.run(chat.ask_knowledge_chat(chat.ChatRequest(question="일위대가가 뭐야"), {}))
assert len(calls) == 3
assert "요청이 몰려" in error.value.detail
assert [f"attempt={n} status=503" in caplog.text for n in (1, 2, 3)] == [True] * 3
assert "test-key" not in caplog.text
assert "일위대가가 뭐야" not in caplog.text
def test_gemini_timeout_is_reported(monkeypatch):
@@ -132,11 +135,13 @@ def test_chat_searches_all_knowledge_markdown_except_excluded_folders(monkeypatc
async def post(self, url, *, headers, json):
sent["prompt"] = json["contents"][0]["parts"][0]["text"]
sent["thinking_level"] = json["generationConfig"]["thinkingConfig"]["thinkingLevel"]
return Response()
monkeypatch.setattr(chat.httpx, "AsyncClient", Client)
result = asyncio.run(chat.ask_knowledge_chat(chat.ChatRequest(question="임도 기준"), {}))
assert result["answer"] == "설명 [1]"
assert sent["thinking_level"] == "low"
paths = {source["path"] for source in result["sources"]}
assert paths == {"original/법률/현행.md"}
assert "실무문서" not in sent["prompt"]