Merge remote-tracking branches 'origin/CODEX' and 'origin/main_laptop_1' into sub_laptop_5
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user