Merge remote-tracking branches 'origin/CODEX' and 'origin/main_laptop_1' into sub_laptop_5

This commit is contained in:
2026-10-06 16:03:14 +09:00
3 changed files with 99 additions and 7 deletions
+31 -7
View File
@@ -1,5 +1,6 @@
"""지식DB Markdown 검색과 Gemini 설명. 제외 폴더는 읽지 않는다."""
import asyncio
import os
import re
from functools import lru_cache
@@ -150,21 +151,44 @@ async def ask_knowledge_chat(payload: ChatRequest, _session=Depends(verify_sessi
)
try:
async with httpx.AsyncClient(timeout=30) as client:
response = await client.post(
f"https://generativelanguage.googleapis.com/v1beta/models/{MODEL}:generateContent",
headers={"x-goog-api-key": key},
json={"contents": [{"parts": [{"text": prompt}]}]},
)
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}]}]},
)
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 모델에 요청이 몰려 있습니다. 잠시 후 다시 질문해 주세요.",
)
response.raise_for_status()
answer = "".join(
part.get("text", "")
for candidate in response.json().get("candidates", [])
for part in candidate.get("content", {}).get("parts", [])
).strip()
except (httpx.HTTPError, ValueError) as exc:
except httpx.HTTPStatusError as exc:
detail = {
400: "Gemini 요청 형식이 올바르지 않습니다.",
401: "Gemini API 키를 확인해 주세요.",
403: "Gemini API 키의 사용 권한을 확인해 주세요.",
429: "Gemini API 사용 한도에 도달했습니다. 잠시 후 다시 시도해 주세요.",
}.get(exc.response.status_code, "Gemini 요청에 실패했습니다. 잠시 후 다시 시도해 주세요.")
raise HTTPException(status_code=502, detail=detail) from exc
except httpx.TimeoutException as exc:
raise HTTPException(
status_code=504,
detail="Gemini 3.8 Flash 응답이 지연되어 시간 초과됐습니다. "
"잠시 후 다시 시도해 주세요.",
) from exc
except (httpx.RequestError, ValueError) as exc:
raise HTTPException(
status_code=502,
detail="Gemini 응답을 받지 못했습니다. API 키와 사용 한도를 확인해 주세요.",
detail="Gemini 서버에 연결하지 못했습니다. 잠시 후 다시 시도해 주세요.",
) from exc
if not answer:
raise HTTPException(status_code=502, detail="Gemini가 답변을 생성하지 못했습니다.")
@@ -310,3 +310,14 @@ B 방향이 맞겠네. 새 프로젝트를 만드는건 문제없어보여. 이
(10-6 사용자)
다음으로 좌측 패널에서 서브 컨테이너의 항목을 선택/변경/생성 할 경우에 횡단도에서 선택한것처럼 이동되어야할것같아.
(10-6 사용자)
추가로 배관의 도수로/돌붙임의 경우에는 복수 값이 아니야. 생성이 되었다면 추가 버튼이 없어져야함.
(10-6 사용자)
1. 종단도의 구조물 알약의 이름이 구조물군의 종류 값과 매칭이 안됨. 같은 값을 보게 개선.
2. 종단도에서 배관이 선택한 상태에서 다른 지점에 옹벽을 추가하기 위해 마우스 오른쪽 버튼을 클릭후 사면안정을 눌렀더니 하위 리스트는 안보이고 리셋된 상태가 됨. 확인
(10-6 사용자)
1. 도수로 돌붙임에는 컨테이너 제목에 1과 2 같은 정보는 필요없어보임.
2. 구조물의 높이 선택에 따라 제약이 발생한 경우 토스트 메세지 발생. 이것도 흙막이, 집수정, 옹벽, 관 보호공에서 높이 제약을 풀어줘. 사용자가 선택하게 할거야. 이건 이전에 요청했던 내용이야.
@@ -1,5 +1,6 @@
import asyncio
import httpx
import pytest
from fastapi import HTTPException
@@ -35,6 +36,60 @@ 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):
monkeypatch.setenv("GEMINI_API_KEY", "test-key")
monkeypatch.setattr(chat, "search", lambda _question: [])
calls = []
class Client:
def __init__(self, timeout):
pass
async def __aenter__(self):
return self
async def __aexit__(self, *_args):
pass
async def post(self, *_args, **_kwargs):
calls.append(1)
return type("Response", (), {"status_code": 503})()
async def no_wait(_seconds):
pass
monkeypatch.setattr(chat.httpx, "AsyncClient", Client)
monkeypatch.setattr(chat.asyncio, "sleep", no_wait)
with pytest.raises(HTTPException) as error:
asyncio.run(chat.ask_knowledge_chat(chat.ChatRequest(question="일위대가가 뭐야"), {}))
assert len(calls) == 3
assert "요청이 몰려" in error.value.detail
def test_gemini_timeout_is_reported(monkeypatch):
monkeypatch.setenv("GEMINI_API_KEY", "test-key")
monkeypatch.setattr(chat, "search", lambda _question: [])
class Client:
def __init__(self, timeout):
pass
async def __aenter__(self):
return self
async def __aexit__(self, *_args):
pass
async def post(self, *_args, **_kwargs):
raise httpx.ReadTimeout("timed out")
monkeypatch.setattr(chat.httpx, "AsyncClient", Client)
with pytest.raises(HTTPException) as error:
asyncio.run(chat.ask_knowledge_chat(chat.ChatRequest(question="일위대가가 뭐야"), {}))
assert error.value.status_code == 504
assert "시간 초과" in error.value.detail
def test_chat_searches_all_knowledge_markdown_except_excluded_folders(monkeypatch, tmp_path):
public = tmp_path / "original" / "법률"
guide = tmp_path / "technical_info"
@@ -57,6 +112,8 @@ def test_chat_searches_all_knowledge_markdown_except_excluded_folders(monkeypatc
sent = {}
class Response:
status_code = 200
def raise_for_status(self):
pass