Files
web-page-backend/docs/superpowers/specs/2026-06-11-insta-autonomous-card-issuance-design.md
gahusb c99017e68c docs(spec): insta 자율 카드 발급 (스마트 에이전트 3번) 설계
선별 지능(4신호)+카드별 승인 게이트+상태머신/발행이력. 접근법 A: insta-lab 선별·상태 소유, agent-office 오케스트레이션·텔레그램 승인.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 02:05:51 +09:00

121 lines
9.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# insta 자율 카드 발급 (스마트 에이전트 3번) — 설계
> 작성 2026-06-11. InstaAgent를 "후보 푸시/단순 auto_select"에서 **선별 지능 + 승인 게이트 + 카덴스/추적**을 갖춘 자율 발급 파이프라인으로 확장.
## 1. 목표
매일 09:30, InstaAgent가 **발행할 가치 있는 주제만 자율 선별**해 카드를 생성·렌더하고, **카드별 승인 게이트**로 사람이 최종 결정(브랜드 안전)한 뒤 업로드용 카드를 발급한다. 발행 상태·이력을 추적해 중복 회피·카덴스 판단에 환류한다.
Instagram Graph API는 사용하지 않는다(수동 업로드). "발행(published)" = 승인되어 업로드 준비가 끝난 카드 상태 + 텔레그램으로 전달.
## 2. 현재 상태 (배경)
- insta-lab: 뉴스수집→키워드추출→슬레이트 생성(`POST /slates`)→Redis push→Windows 워커 렌더→webhook이 `card_assets` 등록. (2026-06-11 렌더 갭 복구 완료, slate 상태 `draft→rendered`.)
- agent-office `InstaAgent`: 09:30 cron에서 collect+extract 후 (기본) 텔레그램 후보 버튼 푸시 / (`auto_select=True`) 카테고리 1위 키워드 자동 렌더+미디어그룹 발송. 버튼 탭 → `render_{kid}` 콜백 → 슬레이트 생성·렌더·발송.
- `account_preferences`(카테고리 가중치) 존재. 발행 성과 추적은 없음.
즉 "생성→렌더→전달"은 동작한다. 본 설계는 그 앞단의 **자율 선별**과 뒷단의 **승인·추적**을 추가한다.
## 3. 요구사항 (확정)
- **선별 신호 4종**: ① 중복 회피(최근 발행/반려 주제 제외) ② 신선도(뉴스 최신성) ③ 계정 컨셉 적합도(카테고리 가중치) ④ Claude 판단(카드가치·흥미·리스크). 가중합 → threshold 게이트.
- **카덴스**: 에이전트 결정 — 매일 09:30, threshold 이상인 픽만 `max_per_day`까지(0~N 가변). 가치 없으면 발행 안 함.
- **승인**: 카드별 게이트. 자동 생성 후 텔레그램 프리뷰 `[✅승인][❌반려][🔄재생성]`. 승인만 published.
- **추적**: slate 상태 ∈ `{draft, rendered, rejected, published}` + 발행 이력. decision=approved→`published`, decision=rejected→`rejected`("approved"는 별도 저장 상태가 아니라 decision 액션). 성과 지표(좋아요·도달)는 범위 외(YAGNI — IG API 없어 수동).
## 4. 아키텍처 (접근법 A: 데이터 있는 곳에서 선별, 에이전트는 오케스트레이션)
```
[09:30 cron] InstaAgent.on_schedule (autonomous_issue=True)
1. collect + extract (기존 재사용)
2. GET /api/insta/keywords/ranked?threshold&limit ← insta-lab: 4신호 점수
3. eligible 픽마다(max_per_day): create_slate → wait render (기존 재사용)
4. 텔레그램 프리뷰(커버1장+요약) + [✅][❌][🔄] + agent_task(requires_approval) → waiting
[telegram webhook] → InstaAgent.on_callback
issue_approve_{id} → POST /slates/{id}/decision{approved} → published + 10장 미디어그룹 + /package zip
issue_reject_{id} → POST /slates/{id}/decision{rejected}
issue_regen_{id} → 같은 키워드로 슬레이트 재생성(새 카피) → 새 프리뷰 (이전 슬레이트 폐기)
```
경계: **insta-lab = 선별 점수 + 상태머신(DB 소유)**, **agent-office = cron 오케스트레이션 + 텔레그램 승인**.
## 5. insta-lab 상세
### 5.1 `app/selection.py` (순수 함수)
입력: 후보 키워드 리스트, 발행/반려 이력, 카테고리 선호 가중치, (선택) Claude 판단 점수.
출력: 후보별 `{keyword_id, final_score, breakdown:{dedup,freshness,account_fit,claude}, eligible}`.
신호별 정의:
- **dedup** (0 또는 1, exclude 게이트): 최근 `dedup_window_days`(기본 14) 내 `published`/`rejected` 슬레이트와 동일 키워드(정규화 후 exact/substring) + 동일 카테고리면 `eligible=False`로 제외.
- **freshness** (0~1): 키워드 `suggested_at`이 최근일수록 높음(예: 24h=1.0, 선형 감쇠, 7일+=0).
- **account_fit** (0~1): `account_preferences[category].weight`(정규화) × 키워드 자체 score.
- **claude** (0~1): Claude Haiku가 후보 일괄 평가(아래 5.3). 실패 시 이 항 제외하고 나머지로 정규화(graceful).
- **final_score** = 가중합 `w_fresh*freshness + w_fit*account_fit + w_claude*claude` (dedup 제외 통과한 것만). 기본 가중치 `{fresh:0.3, fit:0.3, claude:0.4}`. `eligible = (dedup 통과) and (final_score >= threshold)`.
### 5.2 엔드포인트
- `GET /api/insta/keywords/ranked?limit=N&threshold=T`
- 내부에서: 미사용 키워드 조회 + 발행/반려 이력 조회 + 선호 조회 + Claude 일괄 호출 → `selection.py` → 정렬된 후보 + breakdown + `eligible` 반환.
- `POST /api/insta/slates/{id}/decision` body `{"decision": "approved"|"rejected"}`
- approved → `status='published'`, `published_at=now`, `decision_at=now` (멱등: 이미 published면 no-op).
- rejected → `status='rejected'`, `decision_at=now`.
### 5.3 Claude 판단 프롬프트 (insta-lab, 기존 ANTHROPIC 클라이언트 재사용)
- 1회 호출로 후보 N개 일괄 평가. 입력: 각 후보 `{keyword, category}`. 출력: JSON `[{keyword_id, score(0~1), reason}]`.
- 기준: 카드뉴스로 만들 가치(흥미·시의성·정보성) 및 리스크(민감·논란). 모델 `ANTHROPIC_MODEL_HAIKU`.
- 실패/파싱오류 → 빈 결과 반환 → selection이 claude 항 제외.
### 5.4 스키마 (idempotent ALTER)
- `card_slates``published_at TEXT NULL`, `decision_at TEXT NULL` 추가.
- 상태값: `draft → rendered → approved/rejected → published`. (approved는 과도기 상태 없이 decision=approved 시 바로 published로 둔다 — 단순화. rejected는 종결.)
- 발행 이력 = `SELECT keyword, category, published_at FROM card_slates WHERE status IN ('published','rejected') AND COALESCE(published_at,decision_at) >= datetime('now', '-D days')`.
## 6. agent-office 상세
### 6.1 `InstaAgent.on_schedule`
- `custom_config.autonomous_issue` 분기. False면 **기존 동작 유지**(candidate-push / auto_select) — 하위호환.
- True면: collect+extract(기존) → `service_proxy.insta_ranked(threshold, limit=max_per_day)``eligible` 픽 순회(최대 `max_per_day`):
- 슬레이트 생성·렌더 대기(기존 `_render_and_push`의 생성·대기 부분 재사용/분리) → **프리뷰 발송**(6.3) → `create_task(requires_approval=True)``waiting` 상태.
- eligible 0개 → "오늘 발행할 가치 있는 주제 없음" 1통.
### 6.2 콜백 (telegram webhook → `on_callback`)
- `issue_approve_{slate_id}`: `insta_decision(slate_id, "approved")` → 전체 10장 미디어그룹 + `/package` zip 전달 + "✅ 발행 완료" → 해당 task succeeded.
- `issue_reject_{slate_id}`: `insta_decision(slate_id, "rejected")` → "❌ 반려됨" → task 종료.
- `issue_regen_{slate_id}`: 해당 슬레이트의 키워드로 새 슬레이트 생성(새 Claude 카피)·렌더 → 새 프리뷰. 이전 슬레이트는 rejected 처리.
### 6.3 텔레그램 프리뷰 (미디어그룹은 인라인 키보드 불가)
- 커버(01.png) 단장 사진 + 캡션: 키워드·카테고리·`final_score`·breakdown 요약 + inline `[✅승인][❌반려][🔄재생성]` (`callback_data=issue_*_{slate_id}`).
### 6.4 설정 (`agent_config.custom_config`)
- `autonomous_issue` (bool, 기본 false), `select_threshold` (기본 0.6), `max_per_day` (기본 2), `dedup_window_days` (기본 14).
### 6.5 service_proxy 추가
- `insta_ranked(threshold, limit)``GET /keywords/ranked`
- `insta_decision(slate_id, decision)``POST /slates/{id}/decision`
## 7. 에러 처리 / 엣지
- ranked의 Claude 실패 → 룰 점수만으로 진행(graceful), 경고 로그.
- eligible 0개 → 안내 1통(또는 무음 옵션, 기본 안내).
- 렌더 실패 → task failed 통지, 프리뷰 미발송.
- 승인 미응답 → 슬레이트 pending(rendered) 유지, 자동 발행 안 함(안전). 만료 없음.
- 멱등: 중복 승인/반려 no-op. cron 재실행 시 이미 발행/반려 주제는 dedup으로 회피.
- regen 무한루프 방지: regen은 사용자 트리거(버튼)라 자동 반복 없음.
## 8. 테스트
- **insta-lab**: `selection.py` 순수 단위테스트(dedup 최근 제외 / freshness 정렬 / account_fit 가중 / 가중합·threshold 게이트 / claude 실패 시 정규화). ranked 엔드포인트(Claude mock). decision 엔드포인트(approved→published+published_at, rejected, 멱등).
- **agent-office**: 자율 `on_schedule`(proxy mock: ranked eligible→슬레이트 생성→프리뷰 발송 + task requires_approval). 콜백 approve/reject/regen(proxy·messaging mock).
## 9. 범위 외 (YAGNI)
- 발행 성과 지표(좋아요·도달) 수집/학습 — IG API 미사용, 수동 입력 부담으로 제외.
- 신뢰도 하이브리드 자동발행(승인 생략) — 승인 게이트로 통일.
- 임베딩 기반 유사도 dedup — 정규화 exact/substring + 카테고리로 충분(추후 필요 시 확장).
## 10. 영향받는 파일
- insta-lab: `app/selection.py`(신규), `app/main.py`(ranked·decision 라우트), `app/db.py`(컬럼 ALTER + 발행이력/상태 헬퍼), `tests/`.
- agent-office: `app/agents/insta.py`(자율 경로·콜백), `app/service_proxy.py`(2 헬퍼), `app/webhook.py`(issue_* 콜백 디스패치), `tests/`.
- web-backend/CLAUDE.md insta API 목록 + `service_insta.md` 메모리 갱신.