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. |
||
|---|---|---|
| .. | ||
| app | ||
| tests | ||
| .dockerignore | ||
| Dockerfile | ||
| README.md | ||
| pytest.ini | ||
| requirements-dev.txt | ||
| requirements.txt | ||
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 쪽 작업)