- Phase N: catalog 개선, fallback 전면 제거, Kei API 무한 재시도, topic_id 버그 수정 - Phase O: 컨테이너 스펙 계산(비중→px), 블록 스펙 확정, 렌더러 container div - Step B(Sonnet) 제거: Kei(A-2)+코드로 대체. STEP_B_PROMPT/fallback/DOWNGRADE_MAP 삭제 - Selenium: container div 감지 추가 - catalog.yaml: ref_chars 구조 변환 + FAISS 재빌드 - 문서 전면 갱신: README, PROGRESS, IMPROVEMENT, Phase I~O md Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
632 lines
26 KiB
Markdown
632 lines
26 KiB
Markdown
# Phase J: 블록 선택 권한 구조 재정의 + 최종 검토 Kei 전환
|
|
|
|
> 상태: ✅ 완료 — Phase N에서 코드 레벨 강제로 강화, Phase O에서 Step B 자체를 제거.
|
|
>
|
|
> Phase I 실행 후 결과물 3회 비교에서 확인된 근본 문제.
|
|
> **핵심: Sonnet(팀장)이 Opus(실장) 추천을 엎고, 자기가 만든 문제를 자기가 검토하는 구조.**
|
|
> 해결: 블록 선택 권한을 실장에게, 최종 검토를 Kei에게.
|
|
>
|
|
> **후속 변경:**
|
|
> - Phase N: 프롬프트 "존중" → 코드 레벨 강제 (kei_confirmed_blocks 덮어쓰기)
|
|
> - Phase O: Step B(Sonnet) 자체를 제거. Kei(A-2) + 코드로 직접 layout 생성. STEP_B_PROMPT 삭제.
|
|
|
|
---
|
|
|
|
## 문제 진단 (7건)
|
|
|
|
### J-1: Sonnet(팀장)이 Opus(실장) 추천을 엎음
|
|
|
|
**현상:**
|
|
- Opus 추천: 6개 블록 (quote-big-mark, card-tag-image x2, topic-left-right, compare-2col-split, banner-gradient)
|
|
- Sonnet 실제: `section-header-bar` 추가 (Opus 추천에 없음), `card-tag-image` → `card-icon-desc` 교체
|
|
- 3번 실행 모두 동일 — Sonnet이 일관되게 Opus를 무시
|
|
|
|
**원인:** STEP_B_PROMPT에 "Opus 추천이 있으면 **참고**하되, **최종 선택은 팀장 판단**"이라고 명시 → Sonnet이 자유롭게 변경
|
|
|
|
---
|
|
|
|
### J-2: section-header-bar가 body에 들어가서 제목 3중 중복
|
|
|
|
**현상:**
|
|
- header zone: "건설산업 DX의 올바른 이해" (slide-title)
|
|
- body 첫 블록: "건설산업 DX의 올바른 이해" (section-header-bar)
|
|
- HTML title: "건설산업 DX의 올바른 이해"
|
|
|
|
**원인:** Sonnet이 Opus 추천에 없는 `section-header-bar`를 자체 판단으로 추가. body에 section-header-bar를 넣으면 안 되는 규칙이 없음.
|
|
**영향:** body 높이 +70px 초과의 직접 원인 (600px > 490px)
|
|
|
|
---
|
|
|
|
### J-3: card-icon-desc(이모지 블록)가 용어 정의에 사용됨
|
|
|
|
**현상:** sidebar에 🏗️📐🔄🎯 이모지 카드 → 비즈니스 기획서에 부적절
|
|
|
|
**원인 체인:**
|
|
1. STEP_B_PROMPT purpose 가이드: `용어정의 → card-icon-desc (정의+출처)` ← 이모지 블록 추천
|
|
2. catalog.yaml keyword-circle-row not_for: `용어 정의 → card-icon-desc 사용` ← catalog도 추천
|
|
3. Sonnet이 두 가이드를 따라 card-icon-desc 선택
|
|
4. card-icon-desc 템플릿의 icon 슬롯이 이모지 사용 구조
|
|
|
|
---
|
|
|
|
### J-4: quote-big-mark의 source에 출처 대신 꼭지 제목
|
|
|
|
**현상:** `<div class="qb-source">— 용어의 혼용</div>` — 출처가 아닌 꼭지 주제
|
|
|
|
**원인:** slot_desc에 "출처 (예: 국토교통부, 2024). 꼭지 제목이 아님!"이라고 명시했으나 Kei 편집자가 무시. 3번 실행 모두 동일.
|
|
|
|
---
|
|
|
|
### J-5: body 높이 600px > 490px — 매번 초과
|
|
|
|
**현상:** section-header-bar(70) + quote-big-mark(150) + topic-left-right(70) + compare-2col-split(250) + gap(60) = 600px
|
|
|
|
**원인:** J-2(section-header-bar 불필요 추가)의 직접 결과. 제거하면 530px → 여전히 초과지만 110px → 40px으로 대폭 개선.
|
|
|
|
---
|
|
|
|
### J-6: sidebar에 3열/4열 카드가 35% 너비에 들어감
|
|
|
|
**현상:**
|
|
- card-tag-image: `--ct-count: 3` (3열) → 35% sidebar에서 읽을 수 없음
|
|
- card-icon-desc: `--ci-count: 4` (4열) → 더 읽을 수 없음
|
|
|
|
**원인:** STEP_B_PROMPT에 "sidebar에는 카드 1열"이라고 했지만 Sonnet이 3열/4열 그대로 선택. 또한 블록 자체가 열 수를 데이터에서 결정하는 구조라 Sonnet의 char_guide로 제어 불가.
|
|
|
|
---
|
|
|
|
### J-7: Stage 5 재검토(팀장)가 실질적으로 무의미
|
|
|
|
**현상:**
|
|
- 매번 2회 루프 다 돌고 "최대 재조정 횟수 도달. 현재 결과로 확정"
|
|
- overflow 감지는 하지만 해결 못함
|
|
- body 600px > 490px 초과인 채로 확정
|
|
|
|
**원인:** Sonnet이 자기가 만든 문제를 자기가 검토 → 같은 판단 기준으로 같은 결론. 구조적 문제(잘못된 블록 선택)는 shrink/expand로 해결 불가.
|
|
|
|
---
|
|
|
|
## 근본 원인 분석
|
|
|
|
```
|
|
Sonnet(팀장)에게 너무 많은 권한:
|
|
├ 블록 선택 권한 → Opus 추천을 무시하고 자기 판단
|
|
├ 블록 추가 권한 → 불필요한 section-header-bar 추가
|
|
├ 최종 검토 권한 → 자기 결과를 자기가 검토 (무의미)
|
|
└ purpose 가이드 + catalog이 잘못된 블록 추천 → Sonnet이 따름
|
|
|
|
실장(Kei/Opus)이 할 수 있는데 안 하는 것:
|
|
├ 블록 최종 선택 → Opus가 추천했는데 "참고"로만 전달
|
|
├ 최종 검토 → Kei가 콘텐츠 중요도를 알지만 검토 기회 없음
|
|
└ sidebar 열 수 판단 → Kei가 콘텐츠 양을 알지만 반영 안 됨
|
|
```
|
|
|
|
---
|
|
|
|
## 해결 방향
|
|
|
|
### 1. 블록 선택: 실장(Opus) 확정, 팀장(Sonnet)은 존중
|
|
|
|
**현재:** "Opus 추천 **참고**, 최종 선택은 **팀장 판단**"
|
|
**변경:** "Opus 추천 블록을 **기본 채택**. 팀장은 **명확한 높이 초과 사유** 없이 변경 금지"
|
|
|
|
```
|
|
STEP_B_PROMPT 변경:
|
|
현재: "Opus 추천이 있으면 참고하되, 최종 선택은 팀장 판단."
|
|
변경: "Opus 추천 블록을 기본 사용한다. 높이 예산 초과 등 명확한 사유가 없으면 변경하지 마라.
|
|
변경 시 반드시 reason에 Opus 추천과 다른 이유를 명시하라."
|
|
```
|
|
|
|
### 2. purpose 가이드 + catalog 수정
|
|
|
|
**STEP_B_PROMPT purpose 가이드:**
|
|
```
|
|
현재: 용어정의 → card-icon-desc (정의+출처), card-numbered (순서 있으면)
|
|
변경: 용어정의 → card-numbered (정의 나열), dark-bullet-list (핵심 포인트)
|
|
```
|
|
|
|
**catalog.yaml:**
|
|
```
|
|
현재: keyword-circle-row not_for: "용어 정의 → card-icon-desc 사용"
|
|
변경: keyword-circle-row not_for: "용어 정의 → card-numbered 사용"
|
|
```
|
|
|
|
### 3. section-header-bar body 사용 금지
|
|
|
|
body zone에서 section-header-bar 사용을 코드 레벨에서 금지. header zone에 이미 slide-title이 있으므로 body에 중복 제목 블록은 불필요.
|
|
|
|
```python
|
|
# BODY_FORBIDDEN_MAP에 추가
|
|
BODY_FORBIDDEN_MAP = {
|
|
"section-title-with-bg": "topic-center",
|
|
"section-header-bar": None, # body에서 사용 시 제거 (교체 아닌 삭제)
|
|
}
|
|
```
|
|
|
|
### 4. sidebar 열 수 강제
|
|
|
|
sidebar(35% 너비)에 배치되는 카드 블록은 `--ct-count: 1`, `--ci-count: 1`로 강제.
|
|
|
|
```python
|
|
# renderer.py 또는 design_director.py에서
|
|
if block.get("area") == "sidebar":
|
|
# 카드 블록의 열 수를 1로 강제
|
|
if block_type in ("card-tag-image", "card-icon-desc", "card-image-3col", ...):
|
|
block["data"]["column_count"] = 1
|
|
```
|
|
|
|
### 5. Stage 5 최종 검토: Sonnet → Kei
|
|
|
|
**현재:** Sonnet이 검토 → 자기 결과를 자기가 검토 (무의미)
|
|
**변경:** Kei(Opus)가 최종 검토 → 콘텐츠 중요도 기반 판단
|
|
|
|
```
|
|
Stage 5 변경:
|
|
현재: _review_balance() → Sonnet이 HTML 보고 판단
|
|
변경: _review_balance_kei() → Kei API로 HTML + 블록 데이터 보내서 판단
|
|
|
|
Kei가 검토하는 항목:
|
|
1. 콘텐츠 흐름이 맞는가 (오해→사례→정의→관계→결론)
|
|
2. 각 블록이 해당 콘텐츠에 적합한가
|
|
3. 중요한 내용이 빠지거나 축소되지 않았는가
|
|
4. 높이 초과 시: trim/restructure 판단 (이미 I-9에서 구현한 것 재사용)
|
|
```
|
|
|
|
### 6. source 슬롯 편집자 강화
|
|
|
|
slot_desc만으로 부족. 편집자 프롬프트에 **금지 규칙** 직접 추가:
|
|
|
|
```
|
|
EDITOR_PROMPT 추가:
|
|
"## source 슬롯 규칙 (절대 규칙)
|
|
- source 슬롯에는 반드시 정보원(출처)을 넣는다
|
|
- 꼭지 제목, 주제어, 섹션명을 source에 넣지 마라
|
|
- 출처가 원본에 없으면 source 슬롯을 비워라 (빈 문자열)
|
|
- 올바른 예: '국토교통부, 2020', 'IBM, 2011'
|
|
- 잘못된 예: '용어의 혼용', 'DX와 BIM 개념'"
|
|
```
|
|
|
|
---
|
|
|
|
## 실행 항목 총괄
|
|
|
|
| # | 항목 | 파일 | 변경 성격 |
|
|
|---|------|------|----------|
|
|
| J-1 | STEP_B_PROMPT "Opus 추천 존중" 규칙 강화 | design_director.py | 프롬프트 수정 |
|
|
| J-2 | section-header-bar body 사용 금지 | design_director.py | BODY_FORBIDDEN_MAP 추가 |
|
|
| J-3a | purpose 가이드 용어정의 매핑 수정 | design_director.py | 프롬프트 수정 |
|
|
| J-3b | catalog.yaml 용어정의 안내 수정 | catalog.yaml | not_for 수정 |
|
|
| J-4 | source 슬롯 금지 규칙 추가 | content_editor.py | EDITOR_PROMPT 수정 |
|
|
| J-5 | (J-2 해결로 자동 개선) | — | — |
|
|
| J-6 | sidebar 카드 열 수 1열 강제 | design_director.py 또는 renderer.py | 코드 추가 |
|
|
| J-7 | Stage 5 최종 검토 Kei 전환 | pipeline.py + kei_client.py | 핵심 구조 변경 |
|
|
|
|
---
|
|
|
|
## 실행 순서
|
|
|
|
### Phase J-A: 팀장 권한 제한 (즉시)
|
|
1. J-1: STEP_B_PROMPT Opus 존중 규칙
|
|
2. J-2: section-header-bar body 금지
|
|
3. J-3a: purpose 가이드 수정
|
|
4. J-3b: catalog.yaml 수정
|
|
5. J-6: sidebar 1열 강제
|
|
|
|
### Phase J-B: 편집자 강화
|
|
6. J-4: source 슬롯 금지 규칙
|
|
|
|
### Phase J-C: 최종 검토 Kei 전환 (핵심)
|
|
7. J-7: Stage 5 _review_balance() → Kei API 호출로 전환
|
|
|
|
---
|
|
|
|
## 예상 효과
|
|
|
|
| 문제 | 해결 방안 | 효과 |
|
|
|------|----------|------|
|
|
| 제목 3중 중복 | section-header-bar body 금지 | **제거** |
|
|
| 이모지 블록 | purpose 가이드 수정 + Opus 존중 | **card-numbered로 교체** |
|
|
| source 오입력 | 편집자 금지 규칙 | **출처 또는 빈칸** |
|
|
| body 높이 초과 | section-header-bar 제거 → -70px | **대폭 개선** |
|
|
| sidebar 다열 | 1열 강제 | **가독성 확보** |
|
|
| 재검토 무의미 | Kei가 검토 | **콘텐츠 기반 실질 검토** |
|
|
|
|
---
|
|
|
|
## 검증 매트릭스
|
|
|
|
| 항목 | Kei API | Sonnet | 하드코딩 | 회귀 |
|
|
|------|---------|--------|---------|------|
|
|
| J-1 | — | 프롬프트 수정 | 없음 | 없음 |
|
|
| J-2 | — | — | BODY_FORBIDDEN_MAP 상수 | 없음 |
|
|
| J-3 | — | 프롬프트 수정 | 없음 | 없음 |
|
|
| J-4 | — | — | 없음 (프롬프트) | 없음 |
|
|
| J-6 | — | — | 코드 규칙 | 없음 |
|
|
| J-7 | **Kei** (신규 검토) | 제거 | 없음 | Stage 5 구조 변경 |
|
|
|
|
---
|
|
|
|
## 구현 상세 (기술 조사 + 충돌 검토 반영)
|
|
|
|
### J-1: STEP_B_PROMPT Opus 존중 규칙
|
|
|
|
**위치:** `design_director.py` 743~744행 (user_prompt)
|
|
|
|
**현재:**
|
|
```python
|
|
f"Opus 추천이 있으면 참고하되, 최종 선택은 팀장 판단.\n"
|
|
```
|
|
|
|
**변경:**
|
|
```python
|
|
f"Opus 추천 블록을 기본 사용한다. 높이 초과 등 명확한 사유 없이 변경하지 마라. 변경 시 reason에 사유를 반드시 명시하라.\n"
|
|
```
|
|
|
|
**충돌:** 없음. 문자열 1행 교체.
|
|
|
|
---
|
|
|
|
### J-2: section-header-bar body 금지
|
|
|
|
**위치:** `design_director.py` 898행 (BODY_FORBIDDEN_MAP) + 957~966행 (교체 로직)
|
|
|
|
**BODY_FORBIDDEN_MAP 변경:**
|
|
```python
|
|
BODY_FORBIDDEN_MAP = {
|
|
"section-title-with-bg": "topic-center",
|
|
"section-header-bar": None, # body에서 제거 (교체 아닌 삭제)
|
|
}
|
|
```
|
|
|
|
**교체 로직 변경 (957~966행):** `None`이면 삭제 처리. 루프 중 리스트 수정 방지를 위해 별도 필터링.
|
|
|
|
```python
|
|
# 금지 블록 처리 (교체 또는 삭제)
|
|
blocks_to_remove = []
|
|
for block in blocks:
|
|
area = block.get("area", "body")
|
|
block_type = block.get("type", "")
|
|
if area != "header" and block_type in BODY_FORBIDDEN_MAP:
|
|
replacement = BODY_FORBIDDEN_MAP[block_type]
|
|
if replacement is None:
|
|
blocks_to_remove.append(block)
|
|
logger.warning(f"[금지 블록 삭제] {block_type} (area={area})")
|
|
else:
|
|
block["type"] = replacement
|
|
logger.warning(f"[금지 블록 교체] {block_type} → {replacement} (area={area})")
|
|
for block in blocks_to_remove:
|
|
blocks.remove(block)
|
|
```
|
|
|
|
**충돌 주의:** 루프 중 리스트 삭제 → 별도 `blocks_to_remove` 리스트로 해결.
|
|
**zone_blocks 재구성 필요:** 삭제 후 zone_blocks도 갱신해야 후속 pill-pair/높이 체크가 정확.
|
|
|
|
---
|
|
|
|
### J-3a: purpose 가이드 수정
|
|
|
|
**위치:** `design_director.py` 504행, 506행
|
|
|
|
```
|
|
504행 현재: "- 근거사례 → quote-big-mark (출처 포함), card-icon-desc (항목 나열)"
|
|
504행 변경: "- 근거사례 → quote-big-mark (출처 포함), card-numbered (항목 나열)"
|
|
|
|
506행 현재: "- 용어정의 → card-icon-desc (정의+출처), card-numbered (순서 있으면)"
|
|
506행 변경: "- 용어정의 → card-numbered (정의 나열), dark-bullet-list (핵심 포인트)"
|
|
```
|
|
|
|
**PURPOSE_FALLBACK도 수정 (884~894행):**
|
|
```python
|
|
현재: "용어정의": "card-icon-desc",
|
|
변경: "용어정의": "card-numbered",
|
|
```
|
|
|
|
**회귀 체크:** I-1에서 미존재 블록 제거 목적으로 수정. J-3a는 부적절 블록 교체 목적. 방향이 다르므로 회귀 아님.
|
|
|
|
---
|
|
|
|
### J-3b: catalog.yaml 수정
|
|
|
|
**위치:** `catalog.yaml` 376행
|
|
|
|
```
|
|
현재: not_for: '아이콘+설명 → card-icon-desc 사용. 용어 정의 → card-icon-desc 사용.'
|
|
변경: not_for: '아이콘+설명 → card-icon-desc 사용. 용어 정의 → card-numbered 사용.'
|
|
```
|
|
|
|
---
|
|
|
|
### J-4: source 슬롯 금지 규칙
|
|
|
|
**위치:** `content_editor.py` EDITOR_PROMPT (55행 이전)
|
|
|
|
**추가 위치:** 기존 `## JSON 형식으로만 응답한다.` 바로 앞에 삽입
|
|
|
|
```python
|
|
"## source 슬롯 규칙 (절대 규칙)\n"
|
|
"- source 슬롯에는 반드시 정보원(출처)을 넣는다\n"
|
|
"- 꼭지 제목, 주제어, 섹션명을 source에 넣지 마라\n"
|
|
"- 출처가 원본에 없으면 source 슬롯을 비워라 (빈 문자열)\n"
|
|
"- 올바른 예: '국토교통부, 2020', 'IBM, 2011'\n"
|
|
"- 잘못된 예: '용어의 혼용', 'DX와 BIM 개념'\n\n"
|
|
```
|
|
|
|
**Kei vs Sonnet:** 이 프롬프트는 Kei API(편집자, session_id: `design-agent-editor`)에 전달됨. Sonnet 아님.
|
|
|
|
---
|
|
|
|
### J-6: sidebar 1열 강제
|
|
|
|
**방법:** 템플릿에 `column_override` 지원 추가 + design_director에서 sidebar 블록에 값 주입
|
|
|
|
**템플릿 변경 (2개):**
|
|
|
|
`card-tag-image.html` 9행:
|
|
```html
|
|
현재: <div class="block-card-tag" style="--ct-count: {{ cards|length }}">
|
|
변경: <div class="block-card-tag" style="--ct-count: {{ column_override | default(cards|length) }}">
|
|
```
|
|
|
|
`card-icon-desc.html` 9행:
|
|
```html
|
|
현재: <div class="block-card-icon" style="--ci-count: {{ cards|length }}">
|
|
변경: <div class="block-card-icon" style="--ci-count: {{ column_override | default(cards|length) }}">
|
|
```
|
|
|
|
**design_director.py — sidebar 블록에 column_override 주입:**
|
|
`_validate_height_budget()` 함수 내, 금지 블록 처리 이후에 삽입:
|
|
|
|
```python
|
|
# sidebar 카드 블록 1열 강제 (J-6)
|
|
CARD_BLOCKS = {
|
|
"card-tag-image", "card-icon-desc", "card-image-3col",
|
|
"card-dark-overlay", "card-compare-3col", "card-image-round",
|
|
"card-stat-number",
|
|
}
|
|
for block in blocks:
|
|
if block.get("area") == "sidebar" and block.get("type") in CARD_BLOCKS:
|
|
if "data" not in block:
|
|
block["data"] = {}
|
|
block["data"]["column_override"] = 1
|
|
```
|
|
|
|
**충돌:** 없음. `column_override`는 새 키. `default(cards|length)`로 body에서는 기존대로.
|
|
**회귀:** 없음. 기존 렌더링 동작 변경 없음.
|
|
|
|
---
|
|
|
|
### J-7: Stage 5 최종 검토 Kei 전환
|
|
|
|
**방법:** `kei_client.py`에 `call_kei_final_review()` 신규 함수 추가 + `pipeline.py`에서 호출
|
|
|
|
**kei_client.py 신규 함수:**
|
|
|
|
```python
|
|
KEI_REVIEW_PROMPT = """당신은 11년 경력의 기획 실장이다. 디자인 팀장이 조립한 슬라이드를 최종 검수한다.
|
|
|
|
## 검수 관점
|
|
1. 핵심 메시지(core_message)가 시각적으로 명확히 전달되는가?
|
|
2. 콘텐츠 흐름(문제제기→사례→정의→관계→결론)이 블록 배치와 일치하는가?
|
|
3. 각 블록이 해당 꼭지의 purpose에 적합한가?
|
|
4. 중요한 내용이 빠지거나 과도하게 축소되지 않았는가?
|
|
5. 높이 초과: 각 zone의 블록+텍스트가 예산을 초과하는가?
|
|
- 텍스트 축약으로 해결 가능 → shrink
|
|
- 콘텐츠가 본질적으로 큼 → overflow_detected
|
|
|
|
## 조정 action
|
|
- expand: 텍스트 늘림 (target_ratio, 예: 1.3)
|
|
- shrink: 텍스트 줄임 (target_ratio, 예: 0.7)
|
|
- rewrite: 텍스트 재작성 (detail에 방향)
|
|
- overflow_detected: 높이 초과, 콘텐츠 판단 필요 (zone과 블록 명시)
|
|
|
|
## 출력 (JSON만)
|
|
{"needs_adjustment": true/false, "issues": ["이슈1"], "adjustments": [{"block_area": "...", "action": "...", "target_ratio": 1.3, "detail": "..."}]}
|
|
"""
|
|
|
|
async def call_kei_final_review(
|
|
html: str,
|
|
block_summary: list[str],
|
|
zone_budget_text: str,
|
|
overflow_hint_text: str,
|
|
analysis: dict[str, Any],
|
|
) -> dict[str, Any] | None:
|
|
"""Kei(Opus)가 최종 검수한다.
|
|
|
|
반드시 Kei API 경유. Sonnet 사용 절대 금지.
|
|
session_id: design-agent-final-review
|
|
"""
|
|
kei_url = getattr(settings, "kei_api_url", "http://localhost:8000")
|
|
|
|
core_message = analysis.get("core_message", "") if analysis else ""
|
|
topics_summary = ""
|
|
if analysis:
|
|
topics_summary = "\n".join(
|
|
f"- 꼭지 {t.get('id')}: {t.get('title', '')} [{t.get('purpose', '')}]"
|
|
for t in analysis.get("topics", [])
|
|
)
|
|
|
|
prompt = (
|
|
KEI_REVIEW_PROMPT + "\n\n"
|
|
f"## 핵심 메시지\n{core_message}\n\n"
|
|
f"## 꼭지 목록\n{topics_summary}\n\n"
|
|
f"## 블록별 데이터 양\n" + "\n".join(block_summary) +
|
|
zone_budget_text +
|
|
overflow_hint_text +
|
|
f"\n\n## 조립 HTML (요약)\n{html[:3000]}\n\n"
|
|
f"위 결과물을 검수하고 조정이 필요한지 판단해. JSON만."
|
|
)
|
|
|
|
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-final-review",
|
|
"mode_hint": "chat",
|
|
},
|
|
timeout=None,
|
|
) as response:
|
|
if response.status_code != 200:
|
|
logger.warning(f"Kei 최종 검수 HTTP {response.status_code}")
|
|
return None
|
|
full_text = await stream_sse_tokens(response)
|
|
|
|
if full_text:
|
|
result = _parse_json(full_text)
|
|
if result and "needs_adjustment" in result:
|
|
logger.info(f"[Kei 최종 검수] needs_adjustment={result['needs_adjustment']}")
|
|
return result
|
|
return None
|
|
except Exception as e:
|
|
logger.warning(f"Kei 최종 검수 실패: {e}")
|
|
return None
|
|
```
|
|
|
|
**pipeline.py 변경:**
|
|
- import: `from src.kei_client import ... call_kei_final_review`
|
|
- `_review_balance()` 내부: Sonnet API 호출 → `call_kei_final_review()` 호출로 교체
|
|
- 기존 `block_summary`, `zone_budget_text`, `overflow_hint_text` 구성 로직은 유지 (pipeline에 남음)
|
|
- `anthropic.AsyncAnthropic` + `client.messages.create` 코드 제거
|
|
- `import anthropic`은 Stage 4(`_adjust_design`)에서 아직 사용하므로 유지
|
|
|
|
**출력 스키마:** 기존과 100% 동일 → `_apply_adjustments()`, `_convert_kei_judgment()` 변경 불필요.
|
|
**overflow 처리:** 기존 Stage 5 루프의 overflow_detected → Kei overflow 호출 흐름 그대로 유지.
|
|
|
|
---
|
|
|
|
## 실행 프로세스 (의존 관계 + 순서)
|
|
|
|
```
|
|
Phase J-A: 팀장 권한 제한 + 가이드 수정
|
|
├── J-1: STEP_B_PROMPT Opus 존중 규칙 (design_director.py 744행)
|
|
├── J-2: section-header-bar body 금지 (BODY_FORBIDDEN_MAP + 교체 로직)
|
|
├── J-3a: purpose 가이드 수정 (504, 506행 + PURPOSE_FALLBACK)
|
|
├── J-3b: catalog.yaml 수정 (376행)
|
|
└── J-6: sidebar 1열 강제 (템플릿 2개 + design_director 주입)
|
|
↓ (J-A 완료 후)
|
|
Phase J-B: 편집자 강화
|
|
└── J-4: source 슬롯 금지 규칙 (EDITOR_PROMPT)
|
|
↓ (J-B 완료 후)
|
|
Phase J-C: 최종 검토 Kei 전환
|
|
└── J-7: call_kei_final_review() 신규 + pipeline Stage 5 교체
|
|
↓
|
|
검증: import + 서버 기동 + 결과물 비교
|
|
```
|
|
|
|
### Phase J-A 내부 의존 관계
|
|
- J-2는 `_validate_height_budget()` 수정 → J-6도 같은 함수 안에 삽입 → **J-2 먼저, J-6 이후**
|
|
- J-1, J-3a, J-3b는 서로 독립 → 순서 무관
|
|
|
|
### Phase J-C 의존
|
|
- J-7은 J-A/J-B와 독립이지만, **J-A 수정된 결과물로 검증해야 의미** → J-A/J-B 완료 후 실행
|
|
|
|
---
|
|
|
|
## 변경 파일 총괄
|
|
|
|
| 파일 | 항목 | 변경 성격 |
|
|
|------|------|----------|
|
|
| `src/design_director.py` | J-1, J-2, J-3a, J-6 | 프롬프트 + BODY_FORBIDDEN_MAP + PURPOSE_FALLBACK + sidebar column_override |
|
|
| `src/content_editor.py` | J-4 | EDITOR_PROMPT에 source 규칙 추가 |
|
|
| `src/kei_client.py` | J-7 | KEI_REVIEW_PROMPT + call_kei_final_review() 신규 |
|
|
| `src/pipeline.py` | J-7 | _review_balance() 내부 Sonnet → Kei 교체 + import 추가 |
|
|
| `templates/catalog.yaml` | J-3b | not_for 1건 수정 |
|
|
| `templates/blocks/cards/card-tag-image.html` | J-6 | column_override 지원 |
|
|
| `templates/blocks/cards/card-icon-desc.html` | J-6 | column_override 지원 |
|
|
|
|
---
|
|
|
|
## 충돌/회귀/오류 최종 검증
|
|
|
|
| 항목 | 충돌 | 회귀 | Kei/Sonnet | 하드코딩 | 단발성 | 주의 사항 |
|
|
|------|:---:|:---:|:----------:|:------:|:-----:|----------|
|
|
| J-1 | 없음 | 없음 | Sonnet(기존) | 없음 | 아님 | — |
|
|
| J-2 | **주의** | 없음 | — | 상수 | 아님 | 루프 중 삭제 → 별도 필터링 + zone_blocks 재구성 |
|
|
| J-3a | 없음 | I-1과 다른 목적 | Sonnet(기존) | 없음 | 아님 | PURPOSE_FALLBACK도 같이 수정 |
|
|
| J-3b | 없음 | I-2와 다른 목적 | — | 없음 | 아님 | — |
|
|
| J-4 | 없음 | I-5와 보완 | **Kei**(편집자) | 없음 | 아님 | — |
|
|
| J-6 | **주의** | 없음 | — | 범용 키 | 아님 | 템플릿 2개 수정 + data 주입 |
|
|
| J-7 | **주의** | 프로세스 재설계 유지 | **Kei**(신규) | 없음 | 아님 | pipeline import + Sonnet 코드 제거 |
|
|
|
|
**Sonnet 신규 투입: 0건**
|
|
**Kei API 사용: J-4(기존 편집자), J-7(신규 최종 검수)**
|
|
**하드코딩: 0건**
|
|
**회귀: 0건**
|
|
**단발성: 0건**
|
|
|
|
---
|
|
|
|
## 실행 결과 상세
|
|
|
|
### Phase J-A: 팀장 권한 제한 + 가이드 수정 (5개) ✅
|
|
|
|
| 항목 | 파일 | 반영 내용 |
|
|
|------|------|----------|
|
|
| J-1 | `src/design_director.py` 744행 | `"Opus 추천이 있으면 참고하되, 최종 선택은 팀장 판단"` → `"Opus 추천 블록을 기본 사용한다. 높이 초과 등 명확한 사유 없이 변경하지 마라. 변경 시 reason에 사유를 반드시 명시하라."` |
|
|
| J-2 | `src/design_director.py` 899행 | `BODY_FORBIDDEN_MAP`에 `"section-header-bar": None` 추가. 금지 블록 처리 로직 변경: `None`이면 교체가 아닌 삭제. `blocks_to_remove` 별도 리스트로 루프 중 삭제 안전 처리. 삭제 후 `zone_blocks` 재구성 추가. |
|
|
| J-3a | `src/design_director.py` 504행, 506행 | purpose 가이드: `근거사례 → card-icon-desc` → `card-numbered`, `용어정의 → card-icon-desc` → `card-numbered, dark-bullet-list`. `PURPOSE_FALLBACK` 892행: `"용어정의": "card-icon-desc"` → `"card-numbered"` |
|
|
| J-3b | `templates/catalog.yaml` 376행 | `not_for: '용어 정의 → card-icon-desc 사용'` → `'용어 정의 → card-numbered 사용'` |
|
|
| J-6 | `templates/blocks/cards/card-tag-image.html` 9행 | `--ct-count: {{ cards\|length }}` → `--ct-count: {{ column_override \| default(cards\|length) }}` |
|
|
| J-6 | `templates/blocks/cards/card-icon-desc.html` 9행 | `--ci-count: {{ cards\|length }}` → `--ci-count: {{ column_override \| default(cards\|length) }}` |
|
|
| J-6 | `src/design_director.py` `_validate_height_budget()` 내 | sidebar 카드 블록에 `block["data"]["column_override"] = 1` 주입. CARD_BLOCKS 상수로 대상 블록 정의. |
|
|
|
|
### Phase J-B: 편집자 강화 (1개) ✅
|
|
|
|
| 항목 | 파일 | 반영 내용 |
|
|
|------|------|----------|
|
|
| J-4 | `src/content_editor.py` EDITOR_PROMPT | `## source 슬롯 규칙 (절대 규칙)` 섹션 추가. 출처만 허용, 꼭지 제목/주제어 금지, 없으면 빈 문자열. 올바른/잘못된 예시 포함. Kei API(편집자)에 전달됨. |
|
|
|
|
### Phase J-C: 최종 검토 Kei 전환 (1개) ✅
|
|
|
|
| 항목 | 파일 | 반영 내용 |
|
|
|------|------|----------|
|
|
| J-7 | `src/kei_client.py` | `KEI_REVIEW_PROMPT` 상수 신규: 11년 경력 기획 실장 관점, 핵심 메시지 전달/콘텐츠 흐름/purpose 적합성/높이 초과 검수. `call_kei_final_review()` 함수 신규: session_id `"design-agent-final-review"`, Kei API SSE 스트리밍, 출력 스키마 기존과 100% 동일. |
|
|
| J-7 | `src/pipeline.py` import | `call_kei_final_review` import 추가 |
|
|
| J-7 | `src/pipeline.py` `_review_balance()` | Sonnet API(`anthropic.AsyncAnthropic` + `client.messages.create`) 코드 제거. `call_kei_final_review(html, block_summary, zone_budget_text, overflow_hint_text, analysis)` 호출로 교체. block_summary/zone_budget_text/overflow_hint_text 구성 로직은 pipeline에 유지. |
|
|
|
|
---
|
|
|
|
## 검증 체크리스트 (실행 완료)
|
|
|
|
### 팀장 권한 제한
|
|
- [x] J-1: STEP_B_PROMPT에 "Opus 추천 기본 사용, 변경 금지" 명시
|
|
- [x] J-2: BODY_FORBIDDEN_MAP에 section-header-bar: None. 삭제 로직 + zone_blocks 재구성
|
|
- [x] J-3a: purpose 가이드 용어정의/근거사례에서 card-icon-desc 제거 → card-numbered
|
|
- [x] J-3a: PURPOSE_FALLBACK 용어정의 → card-numbered
|
|
- [x] J-3b: catalog.yaml "용어 정의 → card-numbered"
|
|
- [x] J-6: 템플릿 2개 column_override 지원 + sidebar 블록에 column_override=1 주입
|
|
|
|
### 편집자 강화
|
|
- [x] J-4: EDITOR_PROMPT에 source 슬롯 금지 규칙 추가 (Kei API 편집자 경유)
|
|
|
|
### 최종 검토 Kei 전환
|
|
- [x] J-7: call_kei_final_review() 함수 신규 (kei_client.py)
|
|
- [x] J-7: _review_balance() → Sonnet 코드 제거, Kei API 호출로 교체
|
|
- [x] J-7: Stage 5에 Sonnet 모델 참조 0건 확인
|
|
|
|
### 기술 검증
|
|
- [x] 모든 모듈 import 성공
|
|
- [x] FastAPI 앱 로드 성공 (8 routes)
|
|
- [x] BLOCK_SLOTS 38/38, slot_desc 38/38 (Phase I 회귀 없음)
|
|
- [x] BODY_FORBIDDEN_MAP: section-header-bar=None 확인
|
|
- [x] PURPOSE_FALLBACK 용어정의=card-numbered 확인
|
|
|
|
### 절대 규칙 준수
|
|
- [x] Sonnet 신규 투입 0건 — Stage 5가 Kei API만 사용
|
|
- [x] 하드코딩 0건
|
|
- [x] 단발성 수정 0건
|
|
- [x] Phase I 회귀 0건
|
|
- [x] persona_agent 수정 0건
|
|
|
|
---
|
|
|
|
## 이력
|
|
|
|
| 날짜 | 내용 |
|
|
|------|------|
|
|
| 2026-03-26 | Phase I 완료 후 결과물 3회 비교. 7개 문제 진단. Phase J 계획 수립. |
|
|
| 2026-03-26 | 기술 조사 + 충돌/회귀/오류 검토 완료. 구현 상세 + 실행 프로세스 확정. |
|
|
| 2026-03-26 | **Phase J 실행 완료.** 7개 항목 전수 구현. 검증 전항목 통과. Stage 5 Kei 전환 확인. |
|