iykyka/ai-service
Claude 64323b824e
feat: implement 단톡 따라잡기 (group catch-up summary) — roadmap.md §2.7-A
Closes the biggest content gap found against PRD.md §3.1 P0: group chat
catch-up was one of only two v1 MVP scenarios and had zero implementation.

Backend (core-backend):
- ConversationParticipant.LastReadMessageID read marker.
- POST /conversations/:id/read advances the caller's marker (never backward).
- GET /conversations/:id/summary builds context from messages since that
  marker and calls ai-service's new POST /summarize; returns unread_count
  and needs_reply.
- GET /conversations now reports unread_count per room.
- Safety: group conversations now unconditionally block twin-authored
  sends (POST /conversations/:id/messages), regardless of the sender's
  global autonomy level. PRD.md §2.3-③ requires this scenario stay L0-fixed
  with no auto-send; autonomy level is a per-user global setting today, so
  this closes the only path a global L2 whitelist match could otherwise
  auto-send into a group.

AI service (ai-service): new summarize.py module (same Gemini-call shape as
generation.py's draft_reply, no escalation/identity gating since nothing
generated here is ever sent) + POST /summarize.

Mobile: "새 대화" dialog now supports adding/removing multiple peer fields
(2+ peers -> is_group:true automatically); unread badge on conversation
rows; ChatScreen takes isGroup and renders its L1 panel as L0-locked for
groups; new "안 본 동안 요약" AppBar action opens a dialog with the summary
and, when a reply looks needed, a button that feeds straight into the
existing draft-request flow.

Verified with a real Flutter build against a live core-backend + ai-service
instance (Playwright driving 3 demo accounts through group creation, unread
badges, and the summary dialog) — this caught a real ordering bug: marking
the read marker on chat *open* meant the summary was always empty by the
time you could tap it, since opening the room already advanced the marker
past everything you'd come to catch up on. Fixed by marking read on screen
*exit* (dispose) instead, so the marker reflects what was unread that whole
visit and updates once you leave.

go test / pytest / flutter analyze+test all pass.
2026-08-03 03:16:54 +00:00
..
app feat: implement 단톡 따라잡기 (group catch-up summary) — roadmap.md §2.7-A 2026-08-03 03:16:54 +00:00
tests feat: implement 단톡 따라잡기 (group catch-up summary) — roadmap.md §2.7-A 2026-08-03 03:16:54 +00:00
.dockerignore feat(deploy): Docker Compose stack for msn.iykyka.com (N2-B) 2026-07-31 02:26:40 +00:00
Dockerfile feat(deploy): Docker Compose stack for msn.iykyka.com (N2-B) 2026-07-31 02:26:40 +00:00
README.md 제품명 변경(분신→와카뷰) + 소프트 그라디언트/글래스모피즘 UI + 브랜드 로딩 화면 2026-07-31 01:15:42 +00:00
pytest.ini 2.5 QA: ai-service pytest 스위트 승격 + 자율성 플로우(L0~L2) 통합 테스트 2026-07-30 02:37:36 +00:00
requirements-dev.txt 2.5 QA: ai-service pytest 스위트 승격 + 자율성 플로우(L0~L2) 통합 테스트 2026-07-30 02:37:36 +00:00
requirements.txt Promote PoC scripts to the AI service (item 2.2) 2026-07-30 02:09:19 +00:00

README.md

와카뷰 AI service (Python)

Phase 1 AI 서비스 (docs/roadmap.md Phase 1 §2.2). poc/tone-corpus/의 세 스크립트 (generate_draft.py·escalation_filter.py·retrieve_style.py)를 그대로 승격한 내부 API — core-backend/(Go)가 이 서비스를 내부망 HTTP로 호출한다 (tech-design.md §8).

poc/tone-corpus/는 그대로 둔다 — 거긴 코퍼스 실험/블라인드 평가용 PoC 도구로 계속 쓰고, 여기 코드가 실제로 서비스에 쓰이는 "승격된" 버전이다. 두 곳의 로직은 지금 동일하지만, 앞으로 갈라질 수 있다(예: 여기는 프로덕션 안정성 위주로만 바뀌고, PoC 쪽은 계속 실험적으로 바뀌는 식).

실행

pip install -r requirements.txt
export GEMINI_API_KEY=...   # 또는 이 디렉토리에 .env 파일
uvicorn app.main:app --reload --port 8001

테스트

pip install -r requirements-dev.txt
pytest tests/ -v

escalation_filter.py(SELFTEST_CASES 승격 + 추가 케이스)·retrieve_style.py(overlap이 recency를 항상 이긴다는 것, 동점일 때만 recency가 tie-break한다는 것)·generation.py(escalate/no_key/ok 세 경로, Gemini 호출은 mock)·main.py(/health·/escalate/check·/draft 전부)를 tests/의 정식 pytest 스위트로 승격함 (docs/roadmap.md Phase 1 §2.5). 이전엔 ad-hoc TestClient 스크립트로만 확인했던 것들.

API

POST /escalate/check

{ "text": "계좌번호 알려줄래?" }

응답: { "escalate": true, "reason": "금전" }

/draft와 별개로 존재하는 독립 하드게이트 엔드포인트 — core-backend가 트윈(자동발송) 메시지를 저장하기 직전에 이걸 직접 호출해서, /draft를 거치지 않은 발송 경로도 전부 이 게이트를 통과하게 만든다 (docs/roadmap.md Phase 1 §2.4, AGENTS.md 안전 불변식). TestClient로 금전/약속/감정 케이스와 비대상 텍스트 전부 확인함.

POST /draft

{
  "context_lines": ["상대: 오늘 저녁에 뭐 먹을래?"],
  "style_examples": ["ㅇㅇ 좋지", "나도 궁금하네ㅋㅋ"]
}

style_examples(직접 큐레이션) 또는 history(과거 발화 전체, 자동 검색 — k로 개수 조절) 중 정확히 하나만 넣는다. 둘 다 넣거나 둘 다 안 넣으면 422.

응답:

{ "status": "ok" | "escalate" | "no_key", "text": "..." }
  • ok: text는 생성된 답장 초안
  • escalate: text는 에스컬레이션 사유 (금전/약속 확정/감정적으로 무거운 주제) — 이 경우 LLM은 호출되지 않는다 (escalation_filter.py가 하드 게이트)
  • no_key: GEMINI_API_KEY가 없어서 실제 전송될 프롬프트만 text에 담아 반환

TestClient로 style_examples/history 두 경로, 에스컬레이션 케이스, 검증 오류(422) 전부 확인함.

아직 없는 것

  • 온디바이스 말투 이력 저장 (이건 클라이언트/코어 백엔드 쪽 책임 — tech-design.md §2 참고)
  • 사후 알림 + 되돌리기 UI/전체 흐름 (코어 백엔드는 이제 에스컬레이션 시 escalation_logs에 기록은 하지만, 사용자에게 사후 알림을 띄우고 되돌리는 클라이언트 UX는 아직 — Flutter 쪽 작업)