Merge remote-tracking branch 'origin/CODEX' into sub_laptop_3
This commit is contained in:
@@ -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 서버에 연결하지 못했습니다. 잠시 후 다시 시도해 주세요.",
|
||||
|
||||
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user