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:
@@ -63,10 +63,9 @@ KEI_PROMPT = (
|
||||
async def classify_content(content: str) -> dict[str, Any] | None:
|
||||
"""1단계: Kei API를 통해 꼭지를 추출하고 분석한다.
|
||||
|
||||
1차: Kei API (persona + RAG + 사고)
|
||||
fallback: Anthropic API 직접 호출
|
||||
Kei API만 사용. Sonnet fallback 없음.
|
||||
Kei API 실패 시 None 반환 → pipeline.py에서 manual_classify() 안전망.
|
||||
"""
|
||||
# 1차: Kei API
|
||||
result = await _call_kei_api(content)
|
||||
if result:
|
||||
logger.info(
|
||||
@@ -75,47 +74,36 @@ async def classify_content(content: str) -> dict[str, Any] | None:
|
||||
)
|
||||
return result
|
||||
|
||||
# fallback: Anthropic 직접
|
||||
logger.warning("Kei API 실패. Anthropic 직접 호출로 fallback.")
|
||||
result = await _call_anthropic_direct(content)
|
||||
if result:
|
||||
logger.info(
|
||||
f"[Anthropic] 꼭지 추출 완료: {result.get('title', '')}, "
|
||||
f"{len(result.get('topics', []))}개 꼭지"
|
||||
)
|
||||
return result
|
||||
|
||||
logger.warning("[Kei API] 꼭지 추출 실패. manual_classify로 안전망 적용.")
|
||||
return None
|
||||
|
||||
|
||||
async def _call_kei_api(content: str) -> dict[str, Any] | None:
|
||||
"""Kei API를 통해 꼭지 추출. SSE 스트리밍 응답을 파싱."""
|
||||
"""Kei API를 통해 꼭지 추출. SSE 스트리밍으로 실시간 수신."""
|
||||
kei_url = getattr(settings, "kei_api_url", "http://localhost:8000")
|
||||
|
||||
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": KEI_PROMPT + content,
|
||||
"session_id": "design-agent",
|
||||
"mode": "chat",
|
||||
"mode_hint": "chat",
|
||||
},
|
||||
timeout=None,
|
||||
)
|
||||
) as response:
|
||||
if response.status_code != 200:
|
||||
logger.warning(f"Kei API HTTP {response.status_code}")
|
||||
return None
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.warning(f"Kei API HTTP {response.status_code}")
|
||||
return None
|
||||
|
||||
# SSE 응답에서 토큰 수집
|
||||
full_text = _extract_sse_text(response.text)
|
||||
full_text = await _stream_sse_tokens(response)
|
||||
|
||||
if not full_text:
|
||||
logger.warning("Kei API 응답에서 텍스트 추출 실패")
|
||||
return None
|
||||
|
||||
# JSON 추출
|
||||
result = _parse_json(full_text)
|
||||
if result and "topics" in result:
|
||||
return result
|
||||
@@ -128,6 +116,43 @@ async def _call_kei_api(content: str) -> dict[str, Any] | None:
|
||||
return None
|
||||
|
||||
|
||||
async def _stream_sse_tokens(response: httpx.Response) -> str:
|
||||
"""SSE 스트리밍 응답에서 토큰을 실시간 수집한다.
|
||||
|
||||
persona_agent의 SSE 이벤트:
|
||||
- token: 텍스트 토큰 수집
|
||||
- done: 완료, 중단
|
||||
- error: 에러, 즉시 중단
|
||||
- planning/planning_done/research_progress/warning: 스킵
|
||||
"""
|
||||
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 응답에서 토큰 텍스트를 수집한다. CRLF/LF 모두 처리."""
|
||||
tokens = []
|
||||
|
||||
Reference in New Issue
Block a user