Phase G: Kei API 통신 정상화 — streaming 전환 + Sonnet fallback 제거
G-1: httpx non-streaming → streaming 전환 (3개 파일)
- client.post() → client.stream("POST") + response.aiter_lines()
- SSE 토큰을 실시간 수신 (30분+ 무응답 해소)
G-2: Sonnet fallback 완전 제거
- kei_client.py: classify_content()에서 _call_anthropic_direct() 호출 제거
- content_editor.py: fill_content()에서 Sonnet fallback 분기 제거
- Kei API만 사용. 실패 시 manual_classify() 또는 _apply_defaults() 안전망
G-3: _parse_json() 마크다운 제거 3파일 동기화
- content_editor.py, design_director.py에 kei_client.py와 동일한 전처리 추가
G-4: FAISS를 CPU로 전환 (GPU 메모리 경쟁 해소)
- block_search.py + build_block_index.py: device="cpu"
G-5: streaming 파서에 event:error 처리
- persona_agent 에러 시 무한 대기 방지. 즉시 중단.
G-6: content_editor.py None 가드
- Kei API 실패 시 _parse_json(None) TypeError 방지
G-7: "mode" → "mode_hint" 필드명 수정 (3개 파일)
- persona_agent의 실제 필드명에 맞춤
persona_agent 수정: 0건
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+75
-33
@@ -111,20 +111,14 @@ async def fill_content(
|
||||
)
|
||||
|
||||
try:
|
||||
# 1차: Kei API (도메인 전문가 + RAG)
|
||||
# Kei API만 사용. Sonnet fallback 없음.
|
||||
result_text = await _call_kei_editor(user_prompt)
|
||||
|
||||
# fallback: Anthropic 직접
|
||||
# G-6: Kei API 실패 시 None 가드
|
||||
if result_text is None:
|
||||
logger.warning("Kei API 편집 실패. Anthropic 직접 호출로 fallback.")
|
||||
client = anthropic.AsyncAnthropic(api_key=settings.anthropic_api_key)
|
||||
response = await client.messages.create(
|
||||
model="claude-sonnet-4-20250514",
|
||||
max_tokens=4096,
|
||||
system=EDITOR_PROMPT,
|
||||
messages=[{"role": "user", "content": user_prompt}],
|
||||
)
|
||||
result_text = response.content[0].text
|
||||
logger.warning("Kei API 편집 실패. 기본값 적용.")
|
||||
_apply_defaults(blocks)
|
||||
continue
|
||||
|
||||
filled = _parse_json(result_text)
|
||||
|
||||
@@ -165,7 +159,7 @@ async def fill_content(
|
||||
|
||||
|
||||
async def _call_kei_editor(prompt: str) -> str | None:
|
||||
"""Kei API를 통해 텍스트 편집을 요청한다.
|
||||
"""Kei API를 통해 텍스트 편집을 요청한다. SSE 스트리밍으로 실시간 수신.
|
||||
|
||||
Kei persona의 도메인 지식 + RAG를 활용하여
|
||||
건설/DX 분야 전문 용어를 정확하게 유지하면서 편집.
|
||||
@@ -176,22 +170,22 @@ async def _call_kei_editor(prompt: str) -> str | None:
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=None) as client:
|
||||
response = await client.post(
|
||||
async with client.stream(
|
||||
"POST",
|
||||
f"{kei_url}/api/message",
|
||||
json={
|
||||
"message": full_prompt,
|
||||
"session_id": "design-agent-editor",
|
||||
"mode": "chat",
|
||||
"mode_hint": "chat",
|
||||
},
|
||||
timeout=None,
|
||||
)
|
||||
) as response:
|
||||
if response.status_code != 200:
|
||||
logger.warning(f"Kei API (editor) HTTP {response.status_code}")
|
||||
return None
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.warning(f"Kei API (editor) HTTP {response.status_code}")
|
||||
return None
|
||||
full_text = await _stream_sse_tokens(response)
|
||||
|
||||
# SSE 응답에서 텍스트 수집
|
||||
full_text = _extract_sse_text(response.text)
|
||||
if full_text:
|
||||
return full_text
|
||||
|
||||
@@ -203,6 +197,36 @@ async def _call_kei_editor(prompt: str) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
async def _stream_sse_tokens(response: httpx.Response) -> str:
|
||||
"""SSE 스트리밍 응답에서 토큰을 실시간 수집한다."""
|
||||
tokens: list[str] = []
|
||||
event_type = ""
|
||||
|
||||
async for line in response.aiter_lines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
event_type = ""
|
||||
continue
|
||||
if line.startswith("event:"):
|
||||
event_type = line[6:].strip()
|
||||
elif line.startswith("data:"):
|
||||
data = line[5:].strip()
|
||||
if event_type == "token" and data:
|
||||
try:
|
||||
token = json.loads(data)
|
||||
if isinstance(token, str):
|
||||
tokens.append(token)
|
||||
except json.JSONDecodeError:
|
||||
tokens.append(data)
|
||||
elif event_type == "done":
|
||||
break
|
||||
elif event_type == "error":
|
||||
logger.warning(f"Kei API SSE 에러: {data}")
|
||||
break
|
||||
|
||||
return "".join(tokens)
|
||||
|
||||
|
||||
def _extract_sse_text(raw: str) -> str:
|
||||
"""SSE 응답에서 토큰 텍스트를 수집한다."""
|
||||
import re as _re
|
||||
@@ -287,17 +311,35 @@ def _apply_defaults(blocks: list[dict[str, Any]]) -> None:
|
||||
|
||||
|
||||
def _parse_json(text: str) -> dict[str, Any] | None:
|
||||
"""텍스트에서 JSON을 추출한다."""
|
||||
patterns = [
|
||||
r"```json\s*(.*?)```",
|
||||
r"```\s*(.*?)```",
|
||||
r"(\{.*\})",
|
||||
]
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, text, re.DOTALL)
|
||||
if match:
|
||||
try:
|
||||
return json.loads(match.group(1).strip())
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
"""텍스트에서 JSON을 추출한다.
|
||||
|
||||
Kei API가 마크다운 리스트 접두사(- )를 붙여 응답하는 경우에도 처리.
|
||||
"""
|
||||
# 전처리: 각 줄 앞의 마크다운 리스트 접두사(- ) 제거
|
||||
lines = text.split("\n")
|
||||
cleaned_lines = []
|
||||
for line in lines:
|
||||
stripped = line.lstrip()
|
||||
if stripped.startswith("- "):
|
||||
cleaned_lines.append(stripped[2:])
|
||||
elif stripped.startswith("* "):
|
||||
cleaned_lines.append(stripped[2:])
|
||||
else:
|
||||
cleaned_lines.append(stripped)
|
||||
cleaned = "\n".join(cleaned_lines)
|
||||
|
||||
# 원본 먼저 시도 → 클린 버전 시도
|
||||
for target in [text, cleaned]:
|
||||
patterns = [
|
||||
r"```json\s*(.*?)```",
|
||||
r"```\s*(.*?)```",
|
||||
r"(\{.*\})",
|
||||
]
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, target, re.DOTALL)
|
||||
if match:
|
||||
try:
|
||||
return json.loads(match.group(1).strip())
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user