Phase P~S 전체 작업물: 검증 스크립트, 블록 템플릿, 설계 문서, 코드 수정

포함 내용:
- Phase P/Q/R/S 설계 문서 (IMPROVEMENT-PHASE-*.md)
- 영역별 검증 스크립트 (scripts/verify_*.py, test_*.py)
- 블록 템플릿 추가 (cards, emphasis 변형)
- 코드 수정: block_search, content_editor, design_director, slide_measurer
- catalog.yaml 블록 목록 업데이트
- CLAUDE.md, PROGRESS.md, README.md 업데이트

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-31 08:38:06 +09:00
parent 0e4b8c091c
commit 29f56187c0
44 changed files with 9431 additions and 313 deletions

View File

@@ -450,6 +450,99 @@ def _load_catalog() -> str:
# Step B(Sonnet) 제거됨 — Phase O에서 Kei 확정 + 코드 검증으로 대체.
async def _opus_batch_recommend(
analysis: dict[str, Any],
faiss_candidates: dict[int, list[dict]],
container_specs: dict | None = None,
) -> dict[int, str]:
"""Phase P: 전체 topic을 한꺼번에 보여주고 topic별 Opus 추천 1개씩 받는다.
FAISS 후보 2개를 함께 보여주고, Opus가 도메인 지식으로 다른 1개를 추천.
1회 Kei API 호출로 전체 topic 처리.
Returns:
{topic_id: block_type} — 각 topic별 Opus 추천 블록
"""
import httpx
from src.sse_utils import stream_sse_tokens
kei_url = getattr(settings, "kei_api_url", "http://localhost:8000")
# 각 topic의 정보 + FAISS 후보 정리
topic_sections = []
for topic in analysis.get("topics", []):
tid = topic.get("id")
faiss_blocks = faiss_candidates.get(tid, [])
faiss_ids = [b["id"] for b in faiss_blocks]
# 컨테이너 제약 정보
container_info = ""
if container_specs:
from src.space_allocator import find_container_for_topic
spec = find_container_for_topic(tid, container_specs)
if spec:
per_topic = spec.height_px // max(1, len(spec.topic_ids))
container_info = f"컨테이너: {per_topic}px, 허용 height_cost: {spec.max_height_cost} 이하"
topic_sections.append(
f"- 꼭지 {tid}: {topic.get('title', '')}\n"
f" purpose: {topic.get('purpose', '')}\n"
f" relation_type: {topic.get('relation_type', '')}\n"
f" expression_hint: {topic.get('expression_hint', '')}\n"
f" FAISS 후보: {faiss_ids}\n"
f" {container_info}"
)
prompt = (
"아래 각 꼭지에 대해 FAISS가 추천한 블록 2개를 참고하되,\n"
"도메인 지식을 활용하여 **FAISS 후보에 없는 다른 블록 1개**를 추천해줘.\n"
"FAISS 후보와 중복되면 안 된다.\n"
"각 꼭지의 purpose, relation_type, expression_hint를 보고\n"
"**콘텐츠의 의미와 목적에 가장 적합한** 블록을 추천하라.\n"
"컨테이너 크기 제약도 반드시 고려하라.\n\n"
f"## 꼭지 목록\n" + "\n".join(topic_sections) +
"\n\n## 출력 (JSON만)\n"
'{"recommendations": [{"topic_id": 1, "block_type": "...", "reason": "..."}]}'
)
try:
async with httpx.AsyncClient(timeout=None) as client:
async with client.stream(
"POST",
f"{kei_url}/api/message",
json={
"message": prompt,
"session_id": "design-agent-p-recommend",
"mode_hint": "chat",
},
timeout=None,
) as response:
if response.status_code != 200:
logger.warning(f"[Phase P] Opus 배치 추천 HTTP {response.status_code}")
return {}
full_text = await stream_sse_tokens(response)
if not full_text:
return {}
result = _parse_json(full_text)
if result and "recommendations" in result:
mapping = {}
for rec in result["recommendations"]:
tid = rec.get("topic_id") or rec.get("id")
if tid is not None:
mapping[tid] = rec.get("block_type", "")
logger.info(f"[Phase P] Opus 배치 추천: {mapping}")
return mapping
logger.warning(f"[Phase P] Opus 배치 추천 JSON 파싱 실패: {full_text[:200]}")
return {}
except Exception as e:
logger.warning(f"[Phase P] Opus 배치 추천 실패: {e}")
return {}
async def _opus_block_recommendation(
analysis: dict[str, Any],
block_candidates: str,