Phase I 실행 완료 + 프로세스 재설계 (Stage 2.5 → Stage 5)

Phase I: 전수 정합성 복구 + 넘침 처리 패러다임 전환 (14개 항목)
- I-14: SSE 유틸 공통 추출 (src/sse_utils.py 신규, 3개 파일 중복 제거)
- I-13: dead code 3건 삭제 (_call_anthropic_direct, _extract_sse_text x2) + import anthropic 제거
- I-1: STEP_B_PROMPT purpose 가이드 미존재 블록 3개 → 실존 블록 교체
- I-2: catalog.yaml not_for 13건 미존재 블록 참조 교체/제거
- I-12: BLOCK_SLOTS 주석 개수 수정 (cards 9, visuals 6, emphasis 10)
- I-10: INDEX.md 38개 동기화 (삭제된 8개 블록 행 제거)
- I-11: README.md 38개 동기화 (_legacy 제거, 트리/개수 정리)
- I-3: PURPOSE_FALLBACK 상수 + purpose 기반 미등록 블록 교체
- I-7: compare-pill-pair 단독 사용 금지 검증
- I-4: 38개 블록 전체에 slot_desc 추가
- I-5: 편집자 프롬프트에 slot_desc 전달 로직
- I-6: 제목 유사도 70% 초과 시 자동 교정
- I-9: 넘침 판단 Kei API 호출 (KEI_OVERFLOW_PROMPT, call_kei_overflow_judgment)
- I-8: 대형 콘텐츠 정보 Kei overflow 프롬프트에 포함

프로세스 재설계:
- Stage 2.5 제거 → Stage 5에서 Sonnet 감지 + Kei 판단 통합
- _review_balance() 확장: zone 예산 + overflow_detected action 추가
- Stage 5 루프에 Kei 넘침 판단 호출 통합
- _apply_adjustments()에 kei_trim/kei_restructure action 추가
- _build_overflow_context(), _convert_kei_judgment() 헬퍼 함수 추가
- DOWNGRADE_MAP은 Kei API 실패 시 비상용으로만 잔존

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-26 13:06:21 +09:00
co-authored by Claude Opus 4.6
parent 1c65255f04
commit ffad1ba82a
11 changed files with 1982 additions and 535 deletions
+8 -60
View File
@@ -19,6 +19,7 @@ import httpx
from src.config import settings
from src.design_director import BLOCK_SLOTS
from src.sse_utils import stream_sse_tokens
logger = logging.getLogger(__name__)
@@ -92,6 +93,12 @@ async def fill_content(
f" 선택 슬롯: {slots.get('optional', [])}"
)
# I-5: 슬롯 의미 설명 전달 (slot_desc가 있으면)
slot_desc = slots.get("slot_desc", {})
if slot_desc:
desc_lines = [f" {k}: {v}" for k, v in slot_desc.items()]
req_text += "\n 슬롯 설명:\n" + "\n".join(desc_lines)
if char_guide:
guide_lines = [f" {k}: ~{v}" for k, v in char_guide.items()]
req_text += "\n 글자 수 가이드 (참고, 의미 우선):\n" + "\n".join(guide_lines)
@@ -188,7 +195,7 @@ async def _call_kei_editor(prompt: str) -> str | None:
logger.warning(f"Kei API (editor) HTTP {response.status_code}")
return None
full_text = await _stream_sse_tokens(response)
full_text = await stream_sse_tokens(response)
if full_text:
return full_text
@@ -201,65 +208,6 @@ 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
tokens = []
events = _re.split(r'\r?\n\r?\n', raw)
for event in events:
if not event.strip():
continue
event_type = ""
event_data = ""
for line in event.split('\n'):
line = line.strip('\r')
if line.startswith('event:'):
event_type = line[6:].strip()
elif line.startswith('data:'):
event_data = line[5:].strip()
if not event_data:
continue
if event_type == 'token':
try:
token = json.loads(event_data)
if isinstance(token, str):
tokens.append(token)
except json.JSONDecodeError:
tokens.append(event_data)
elif event_type == 'done':
break
return "".join(tokens)
def _apply_defaults(blocks: list[dict[str, Any]]) -> None:
"""실패 시 기본 데이터 적용."""