interactive-chat / docs /ARCHITECTURE.md
Junhoee's picture
Upload 162 files
1c9c13f verified
|
Raw
History Blame Contribute Delete
16.1 kB
# sorac ์นด๋ฅด๋ฐ€๋ผ โ€” ์ „์ฒด ๊ตฌ์กฐ ๋ช…์„ธ์„œ
> ์ด ๋ฌธ์„œ ํ•˜๋‚˜๋กœ "๋ฌด์Šจ ํŒŒ์ผ์ด ์–ด๋”” ์žˆ๊ณ , ์–ด๋–ค ์—ญํ• ์ด๋ฉฐ, ์–ด๋–ป๊ฒŒ ๋งž๋ฌผ๋ ค ๋„๋Š”์ง€"๋ฅผ ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ๊ฒŒ ์“ด ๋ช…์„ธ์„œ.
> ๋งˆ์ง€๋ง‰ ๊ฐฑ์‹ : 2026-07-03 (MVP M0~M6 ์™„๋ฃŒ ์‹œ์ )
---
## 0. ํ•œ ๋ฌธ์žฅ ์š”์•ฝ
**๋น„ํŠธ์นด๋“œ(๋Œ€ํ‘œ๋‹˜์ด ์“ด ์„œ์‚ฌ ์›๊ณ )๋ฅผ YAML๋กœ ๋ณ€ํ™˜ํ•ด ์ ์žฌํ•˜๊ณ , ํŒŒ์ด์ฌ ์ƒํƒœ๋จธ์‹ ์ด "์ฑ… ์ฝ๊ธฐ"๋ฅผ,
google-adk LLM ํŒŒ์ดํ”„๋ผ์ธ์ด "์นด๋ฅด๋ฐ€๋ผ์™€์˜ ๋Œ€ํ™”"๋ฅผ ๋‹ด๋‹นํ•˜๋ฉฐ, ์›น ๋ฆฌ๋”๊ฐ€ ๊ทธ๊ฑธ ํ™”๋ฉด์— ๊ทธ๋ฆฐ๋‹ค.**
```
[๋Œ€ํ‘œ๋‹˜ ๋น„ํŠธ์นด๋“œ MD] โ”€โ”€๋ณ€ํ™˜โ”€โ”€โ–ถ [content/ YAML] โ”€โ”€์ ์žฌโ”€โ”€โ–ถ โ”Œโ”€ StoryEngine (์ฝ๊ธฐ: ๊ฒฐ์ •์  ์ฝ”๋“œ)
โ””โ”€ PocketService (๋Œ€ํ™”: ADK LLM)
โ”‚
[web/index.html] โ—€โ”€โ”€HTTPโ”€โ”€ [router.py]
```
ํ•ต์‹ฌ ์„ค๊ณ„ ์›์น™ (CLAUDE.md):
- **์„œ์‚ฌ ์ง„ํ–‰์€ ์ฝ”๋“œ๊ฐ€, ๋Œ€์‚ฌ ์ƒ์„ฑ๋งŒ LLM์ด** ํ•œ๋‹ค โ€” ์Šคํ† ๋ฆฌ๊ฐ€ ์‚ฐ์œผ๋กœ ๊ฐ€์ง€ ์•Š๊ฒŒ
- **์ˆจ์€ ์ƒํƒœ(์‹ ๋ขฐ/์˜์‹ฌ/์‡ ์•ฝ)๋Š” ์œ ์ €์—๊ฒŒ ์ ˆ๋Œ€ ์ˆซ์ž๋กœ ๋…ธ์ถœํ•˜์ง€ ์•Š๋Š”๋‹ค**
- **์ฝ˜ํ…์ธ (YAML)์™€ ์ฝ”๋“œ์˜ ๋ถ„๋ฆฌ** โ€” ์ด์•ผ๊ธฐ ์ˆ˜์ •์€ ์ฝ˜ํ…์ธ  ์ชฝ์—์„œ๋งŒ
---
## 1. ๋ฃจํŠธ ํด๋” ์ง€๋„
| ๊ฒฝ๋กœ | ๋ˆ„๊ฐ€ ๋งŒ๋“  ๊ฒƒ | ์—ญํ•  |
|---|---|---|
| `CLAUDE.md` | ๊ณต๋™ | ๊ฐœ๋ฐœ ํ—Œ๋ฒ•. ์•„ํ‚คํ…์ฒ˜ ๊ทœ์น™ยท๊ธˆ์ง€์‚ฌํ•ญ. Claude Code๊ฐ€ ๋งค ์„ธ์…˜ ์ฝ์Œ |
| `engine/` | Claude | **์—”์ง„ ๋ณธ์ฒด** (ํŒŒ์ด์ฌ ํŒจํ‚ค์ง€) โ€” ์•„๋ž˜ ยง3 |
| `content/` | ๋ณ€ํ™˜๊ธฐ ์‚ฐ์ถœ๋ฌผ | **์—”์ง„์ด ์ฝ๋Š” ์„œ์‚ฌ ๋ฐ์ดํ„ฐ** (YAML) โ€” ์•„๋ž˜ ยง2 |
| `web/` | Claude | ์›น ๋ฆฌ๋” ํ”„๋ก ํŠธ (๋‹จ์ผ HTML) |
| `scripts/` | Claude | ๊ฐœ๋ฐœ์šฉ ์Šคํฌ๋ฆฝํŠธ (๋ณ€ํ™˜๊ธฐ, CLI ํ”Œ๋ ˆ์ด์–ด) |
| `tests/` | Claude | ์ž๋™ ํ…Œ์ŠคํŠธ 18๊ฐœ |
| `docs/` | Claude | ๊ณ„ํš์„œยทAPI ๊ณ„์•ฝยท๋ณธ ๋ช…์„ธ์„œ |
| `logs/` | ๋Ÿฐํƒ€์ž„ ์ƒ์„ฑ | ํ”Œ๋ ˆ์ด ํ„ด ๋กœ๊ทธ (์—ฐ๊ตฌ ๋ฐ์ดํ„ฐ, git ์ œ์™ธ) |
| `data/` | ๋Ÿฐํƒ€์ž„ ์ƒ์„ฑ | ํ”Œ๋ ˆ์ด ์„ธ์…˜ ์˜์†ํ™” SQLite (`sessions.db`, git ์ œ์™ธ) |
| `.env` | ์œ ์ € | OPENAI_API_KEY (git ์ œ์™ธ โ€” ์ ˆ๋Œ€ ์ปค๋ฐ‹ ๊ธˆ์ง€) |
| `.venv/` | ์„ค์น˜ ์‚ฐ์ถœ๋ฌผ | ํŒŒ์ด์ฌ ๊ฐ€์ƒํ™˜๊ฒฝ (google-adk 2.3.0 ๋“ฑ) |
| `์นด๋ฅด๋ฐ€๋ผ/` | ๋Œ€ํ‘œ๋‹˜ | **์ฝ˜ํ…์ธ  ์›์ฒœ** โ€” ๋น„ํŠธ์นด๋“œ MDยท์›๊ณ ยท๊ธฐํšยท์ด๋ฏธ์ง€ |
| `reference/` | ๋Œ€ํ‘œ๋‹˜(ํ”„๋กœํ† ํƒ€์ž…) | ๋น„-ADK ๊ฐœ๋…๊ฒ€์ฆ ์—”์ง„. **์ฐธ๊ณ ์šฉ, ์‹คํ–‰ ์•ˆ ํ•จ** |
| `requirements.txt` / `pyproject.toml` | Claude | ์˜์กด์„ฑ ๋ชฉ๋ก / pytest ์„ค์ • |
---
## 2. content/ โ€” ์—”์ง„์ด ์ฝ๋Š” ์„œ์‚ฌ ๋ฐ์ดํ„ฐ (3๋ ˆ์ด์–ด)
**์—”์ง„์˜ ์œ ์ผํ•œ ์„œ์‚ฌ ์ž…๋ ฅ.** ์›์ฒœ์€ `์นด๋ฅด๋ฐ€๋ผ/0701_์นด๋ฅด๋ฐ€๋ผ/๋น„ํŠธ์นด๋“œ/`์˜ MD ํŒŒ์ผ(๋Œ€ํ‘œ๋‹˜ ์†Œ์œ )์ด๊ณ ,
์—ฌ๊ธฐ ์žˆ๋Š” YAML์€ `scripts/beatcard_convert.py`๊ฐ€ ๋งŒ๋“  **ํŒŒ์ƒ๋ฌผ**์ด๋‹ค. ์ด์•ผ๊ธฐ๋ฅผ ๊ณ ์น˜๋ ค๋ฉด
MD๋ฅผ ๊ณ ์น˜๊ณ  ๋ณ€ํ™˜๊ธฐ๋ฅผ ๋‹ค์‹œ ๋Œ๋ฆฐ๋‹ค (YAML ์ง์ ‘ ์ˆ˜์ • ๊ธˆ์ง€).
| ํŒŒ์ผ | ๋ ˆ์ด์–ด | ๋‚ด์šฉ |
|---|---|---|
| `content/beatcards/E01.yaml` ~ `E06.yaml` | ๋‚ด๋Ÿฌํ‹ฐ๋ธŒ | **๋น„ํŠธ์นด๋“œ 61์žฅ.** ์นด๋“œ = ํ™”๋ฉด ํ•œ ์žฅ. ์ง€๋ฌธ ๋ธ”๋ก(prose/์ค‘์•™๋Œ€์‚ฌ/์ธ๋ฌผ๋Œ€์‚ฌ/๋ฌด๋Œ€์ง€์‹œ), ์„ ํƒ์ง€(๋ผ๋ฒจยท๊ฒฐ๊ณผ์‚ฐ๋ฌธยท์‹ ๋ขฐ์ถ•ยท๋ถ„๊ธฐ), next ๋งํฌ, ํƒœ๊ทธ |
| `content/beatcards/R-ํšŒํ”ผ์‚ฌ๋‹ค๋ฆฌ.yaml` | ๋‚ด๋Ÿฌํ‹ฐ๋ธŒ | ์œ ์ €๊ฐ€ ์ •์ฒด๋ฅผ ์ง๊ฒฉํ–ˆ์„ ๋•Œ ์นด๋ฅด๋ฐ€๋ผ์˜ 3๋‹จ๊ณ„ ํšŒํ”ผ ๋Œ€์‘ (์žฌ์‚ฌ์šฉ ์นด๋“œ) |
| `content/beatcards/R-์•ฝ์ ๋ฐ˜์‘.yaml` | ๋‚ด๋Ÿฌํ‹ฐ๋ธŒ | ์„ฑ๋ฌผยท๋งˆ๋Š˜ ๋“ฑ ์•ฝ์  ์–ธ๊ธ‰ ์‹œ ๋ฐ˜์‘ ์›์น™ |
| `content/chapters/carmilla_chapter1.yaml` | ์ฑ•ํ„ฐ ๋ฉ”ํƒ€ | ์‹œ์ž‘ ์นด๋“œ(E01-01), ๋ฒ”์œ„(E01~E06), **์—”๋”ฉ 3์ข… ๊ทœ์น™**, ์บ๋…ผ ๊ทœ์น™, R์นด๋“œ ๋“ฑ๋ก, ์ฑ„ํŒ… ํ„ด ์ƒํ•œ(6) |
| `content/characters.yaml` | ์ •์ฒด์„ฑ | ์ธ๋ฌผ ์‹œํŠธ 3๋ช…(๋กœ๋ผ=ํ”Œ๋ ˆ์ด์–ด, ์นด๋ฅด๋ฐ€๋ผ, ์•„๋ฒ„์ง€). ์„ฑ๊ฒฉยทํ™”๋ฒ•ยท์ง€์‹๊ฒฝ๊ณ„ |
**์นด๋“œ ํ•œ ์žฅ์˜ ๊ตฌ์กฐ (์˜ˆ)**:
```yaml
- id: E06-09 # ์นด๋“œ ID
title: ๋‘˜๋งŒ์˜ ๋น„๋ฐ€
canon: true # ์›์ž‘ ๋ผˆ๋Œ€ (๋ถˆ๋ณ€)
next: E06-10 # ๋‹ค์Œ ์นด๋“œ
axis: null # ์นด๋“œ ์ง„์ž… ์‹œ ์‹ ๋ขฐ์ถ• ์ด๋™ (trust/doubt/null)
frailty: false # ์ด ์นด๋“œ์—์„œ ์‡ ์•ฝ๋„ +1 ์—ฌ๋ถ€
narration: # ์ง€๋ฌธ ๋ธ”๋ก๋“ค (ํ™”๋ฉด์— ๊ทธ๋Œ€๋กœ ์ถœ๋ ฅ)
- {type: prose, text: "..."}
- {type: dialogue_center, text: "์ฃฝ์Œ์€ ์˜คํžˆ๋ ค ์ถ•๋ณต์ผ ์ˆ˜๋„ ์žˆ์–ด."}
- {type: dialogue_center, text: "...์‹œ์ž‘์ด์•ผ.", gate: trust_high} # ์‹ ๋ขฐ้ซ˜ ์ „์šฉ(์‹ฌ์ธต ํ•ด๊ธˆ)
- {type: character_dialogue, speaker: ์นด๋ฅด๋ฐ€๋ผ, text: "...", anchor: true} # ์ฑ„ํŒ… ๋ณ€์ฃผ ๊ธฐ์ค€
choices:
- {label: ๋‘˜๋งŒ์˜ ๋น„๋ฐ€์— ๋™์กฐํ•œ๋‹ค, axis: trust, result: "...", next: null}
- {label: ๊ทธ๋ž˜๋„ ๋ˆ„๊ตฐ๊ฐ€์—๊ฒŒ ์•Œ๋ ค์•ผ..., axis: doubt, result: "...", next: null}
```
---
## 3. engine/ โ€” ์—”์ง„ ๋ณธ์ฒด
### 3-1. ์ตœ์ƒ์œ„ ํŒŒ์ผ
| ํŒŒ์ผ | ์—ญํ•  |
|---|---|
| `model.py` | LLM ์„ค์ •. ๋ฃจํŠธ `.env`๋ฅผ ๋กœ๋”ฉํ•˜๊ณ  LiteLLM์œผ๋กœ ๋ชจ๋ธ ์ดˆ๊ธฐํ™” (`LLM_MODEL`/`CHARACTER_MODEL` env๋กœ ๊ต์ฒด ๊ฐ€๋Šฅ, ๊ธฐ๋ณธ openai/gpt-5.4-nano) |
| `agent.py` | **ADK ํŒŒ์ดํ”„๋ผ์ธ ์กฐ๋ฆฝ**: `pocket_session = Sequential[๋กœ๋” โ†’ ๊ฐ€๋“œ โ†’ ์นด๋ฅด๋ฐ€๋ผ โ†’ ๋””๋ ‰ํ„ฐ]`. `adk web`์šฉ `root_agent`๋„ ์—ฌ๊ธฐ์„œ ๋…ธ์ถœ |
| `app.py` | ADK `App`(์ด๋ฆ„+๋ฃจํŠธ์—์ด์ „ํŠธ+ํ”Œ๋Ÿฌ๊ทธ์ธ) + `Runner`(์„ธ์…˜ ์„œ๋น„์Šค) ์ƒ์„ฑ |
| `router.py` | **FastAPI ์„œ๋ฒ„.** HTTP ์—”๋“œํฌ์ธํŠธ(์„ธ์…˜/advance/choose/pocket) + ์›น ๋ฆฌ๋”ยท์ด๋ฏธ์ง€ ์„œ๋น™. ์—ฐ์ถœ ์ด๋ฏธ์ง€ ๋งคํ•‘(`IMAGE_MAP`)๋„ ์—ฌ๊ธฐ |
### 3-2. services/ โ€” ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง (๊ฒŒ์ž„ ๊ทœ์น™์˜ ์‹ฌ์žฅ)
| ํŒŒ์ผ | ์—ญํ•  |
|---|---|
| `story.py` | **์ฝ๊ธฐ ์ƒํƒœ๋จธ์‹ .** ์นด๋“œ ์ „์ด(next), ์„ ํƒ์ง€ ๋ฐ˜์˜, ์‹ฌ์ธต ๊ฒŒ์ดํŠธ ๋…ธ์ถœ ํŒ์ •, **์—”๋”ฉ ์กฐ๊ฑด์‹ ํ‰๊ฐ€**(์ „์šฉ ํŒŒ์„œ โ€” eval ๊ธˆ์ง€). ๋„๋‹ฌ์  ๋ณด์žฅ(preset_branch) ๋กœ์ง ํฌํ•จ |
| `state.py` | **์ˆจ์€ ์ƒํƒœ 3๊ฐ’.** ์‹ ๋ขฐ์ถ• ์‹œ์†Œ(trust/doubt, ์ด๋ฒคํŠธ๋‹น ยฑ12), ์‡ ์•ฝ๋„(๋‹จ์กฐ ์ฆ๊ฐ€๋งŒ ํ—ˆ์šฉ), ์•Ž(๋„์ž… 1ํšŒ ํ™•๋ฆฝ). ์‹ ๋ขฐ ๋ ˆ๋ฒจ ํ™˜์‚ฐ(ยฑ12/ยฑ36 ๊ฒฝ๊ณ„ โ†’ 5๋‹จ๊ณ„) โ€” ์ด ๋ ˆ๋ฒจ์ด ์นด๋ฅด๋ฐ€๋ผ์˜ "๊ฒฐ"๊ณผ ์‹ฌ์ธต ํ•ด๊ธˆ์„ ๊ฒฐ์ • |
| `pocket.py` | **์ฑ„ํŒ… ์„ธ์…˜ ๊ด€๋ฆฌ.** ํฌ์ผ“ ์—ด๊ธฐ(์นด๋ฅด๋ฐ€๋ผ ์นด๋“œ๋งŒ)/ํ„ด ์‹คํ–‰/๋‹ซ๊ธฐ(์ƒํƒœ ์ปค๋ฐ‹). **์ˆ˜๋ ด ์žฌ๊ฒ€์ฆ**: LLM์ด "๋๋‚ด์ž"๊ณ  ํ•ด๋„ ์ฝ”๋“œ๊ฐ€ ๊ฒ€์‚ฌ โ€” ์ตœ์†Œ 3ํ„ด ์ฒด๋ฅ˜, ๋น„ํŠธ ์—†๋Š” ์ข…๋ฃŒ ๊ธฐ๊ฐ, 6ํ„ด ๊ฐ•์ œ ์ˆ˜๋ ด |
| `paging.py` | **ํŽ˜์ด์ง€ ๋ณ‘ํ•ฉ.** ์—ฌ๋Ÿฌ ์นด๋“œ๋ฅผ ํ•œ ํ™”๋ฉด์œผ๋กœ โ€” ๊ฒฐ์ •์  ํŒŒํ‹ฐ์…˜(์„ ํ˜• ๋งํฌ ๊ตฌ๊ฐ„๋งŒ, ์„ ํƒ์ง€ยท์—ฐ์ถœ ์ปทยท์ฑ„ํŒ… ์•ต์ปคยท์—ํ”ผ์†Œ๋“œ ๊ฒฝ๊ณ„์—์„œ ๋ถ„๋ฆฌ, `page_target_chars` ์†Œํ”„ํŠธ ์บก). ๋ถ€์ˆ˜ํšจ๊ณผ๋Š” ์—ฌ์ „ํžˆ story.py๊ฐ€ ์นด๋“œ๋‹น 1ํšŒ |
| `retrieval.py` | (2๋‹จ๊ณ„ ์Šคํ…) GraphRAG ๊ฒ€์ƒ‰ ์ž๋ฆฌ. ์Šคํฌ์ผ๋Ÿฌ ํ•„ํ„ฐ ๊ฐ•์ œ ์ง€์  |
### 3-3. sub_agents/ + agents/ โ€” ADK ๋Œ€ํ™” ํŒŒ์ดํ”„๋ผ์ธ (์ฑ„ํŒ… ํ•œ ํ„ด์— 4๋ช…์ด ์ˆœ์„œ๋Œ€๋กœ ์ผํ•จ)
```
์œ ์ € ์ž…๋ ฅ โ†’ โ‘  pocket_context_loader โ†’ โ‘ก input_guard โ†’ โ‘ข carmilla โ†’ โ‘ฃ director โ†’ ์‘๋‹ต
```
| ์ˆœ์„œ | ํŒŒ์ผ | ํƒ€์ž… | ํ•˜๋Š” ์ผ |
|---|---|---|---|
| โ‘  | `agents/pocket_context_loader.py` | ์ฝ”๋“œ(BaseAgent) | ํ˜„์žฌ ์นด๋“œ์˜ ์ง€๋ฌธยท์•ต์ปค๋Œ€์‚ฌยท์‹ ๋ขฐ ๋ ˆ๋ฒจ๋ณ„ "๊ฒฐ"์„ ์„ธ์…˜์— ์ ์žฌ. **์บ๋ฆญํ„ฐ์šฉ ๋ธŒ๋ฆฌํ”„์™€ ๋””๋ ‰ํ„ฐ์šฉ ๋…ธํŠธ๋ฅผ ๋ถ„๋ฆฌ** (์Šคํฌ์ผ๋Ÿฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€) |
| โ‘ก | `agents/input_guard.py` | ์ฝ”๋“œ(BaseAgent) | ์œ ์ € ์ž…๋ ฅ ๊ฒ€์‚ฌ: ์ •์ฒด ์ง๊ฒฉ("ํกํ˜ˆ๊ท€์ง€?") โ†’ R-ํšŒํ”ผ์‚ฌ๋‹ค๋ฆฌ ๋‹จ๊ณ„ ์ง€์‹œ, ์•ฝ์ (์‹ญ์ž๊ฐ€ ๋“ฑ) โ†’ R-์•ฝ์ ๋ฐ˜์‘ ์ง€์‹œ, ๋ฉ”ํƒ€ ๋ฐœํ™”(AI/๊ฒŒ์ž„) โ†’ ์„ธ๊ณ„๊ด€ ๋ณต๊ท€ ์ง€์‹œ |
| โ‘ข | `sub_agents/character/agent.py` | **LLM** | ์นด๋ฅด๋ฐ€๋ผ ์‘๋‹ต ์ƒ์„ฑ. instruction = `instructions/character_carmilla.md` + ์ธ๋ฌผ์‹œํŠธ + ์žฅ๋ฉด๋ธŒ๋ฆฌํ”„ + ๊ฐ€๋“œ์ง€์‹œ |
| โ‘ฃ | `sub_agents/director/agent.py` | **LLM** | ์‹ฌํŒ. ์œ ์ €์—๊ฒŒ ์•ˆ ๋ณด์ž„. ๋„๊ตฌ ํ˜ธ์ถœ๋กœ๋งŒ ๊ธฐ๋ก: ์‹ ๋ขฐ์ถ• ์ด๋™/๋ชฉํ‘œ ๋น„ํŠธ ๋‹ฌ์„ฑ/์ˆ˜๋ ด ์„ ์–ธ |
### 3-4. tools/state/ โ€” ๋””๋ ‰ํ„ฐ๊ฐ€ ์“ฐ๋Š” ๋„๊ตฌ 3์ข… (1ํŒŒ์ผ 1ํ•จ์ˆ˜)
| ํŒŒ์ผ | ๊ธฐ๋Šฅ |
|---|---|
| `update_trust.py` | ์ด๋ฒˆ ํ„ด ๋Œ€ํ™”๋กœ ์‹ ๋ขฐ/์˜์‹ฌ ์ถ•์„ ยฑ12 ์ด๋™ |
| `mark_beat.py` | ์žฅ๋ฉด ๋ชฉํ‘œ ๋น„ํŠธ ๋‹ฌ์„ฑ ๊ธฐ๋ก |
| `finish_pocket.py` | ์ˆ˜๋ ด ์„ ์–ธ(escalate) โ€” ๋‹จ **์ตœ์ข… ๊ฒฐ์ •์€ pocket.py ์ฝ”๋“œ๊ฐ€ ์žฌ๊ฒ€์ฆ** |
### 3-5. ๋‚˜๋จธ์ง€
| ๊ฒฝ๋กœ | ์—ญํ•  |
|---|---|
| `schemas/` | ์ฝ˜ํ…์ธ  YAML์˜ Pydantic ์Šคํ‚ค๋งˆ(beatcard/chapter/character) + **validate CLI** (`python -m engine.schemas.validate content/` โ€” ์Šคํ‚ค๋งˆยท๋งํฌ ๋ฌด๊ฒฐ์„ฑยท๋„๋‹ฌ์„ฑ ๊ฒ€์‚ฌ) |
| `repositories/content.py` | YAML โ†’ ๋ฉ”๋ชจ๋ฆฌ ์ ์žฌ. **์Šคํฌ์ผ๋Ÿฌ ํ•„ํ„ฐ**(ํ˜„์žฌ ์นด๋“œ ์ดํ›„ ์กฐํšŒ ์ฐจ๋‹จ). ์นด๋“œ ์ˆœ์„œ/์ „์ฒด ์ˆ˜(์ง„ํ–‰๋„์šฉ). ์ฝ˜ํ…์ธ  ๋ฒ„์ „ ์Šคํƒฌํ”„(`content_version`) |
| `repositories/playlog.py` | ํ„ด ๋กœ๊ทธ jsonl ์ €์žฅ (`logs/playlog.jsonl`, ์—ฐ๊ตฌ ๋ฐ์ดํ„ฐ) |
| `repositories/play_session.py` | **์„ธ์…˜ ์˜์†ํ™”** โ€” ์„œ์‚ฌ ์œ„์น˜+์ˆจ์€ ์ƒํƒœ ์Šค๋ƒ…์ƒท์„ SQLite(`data/sessions.db`)์— ๋งค ๋ณ€๊ฒฝ ์ฆ‰์‹œ ์ €์žฅ. ์„œ๋ฒ„ ์žฌ์‹œ์ž‘ ํ›„ ๋ณต์›(ํฌ์ผ“ ์ฑ„ํŒ…์€ ๋น„์˜์†ยทํ๊ธฐ). ์ฝ˜ํ…์ธ  ๋ฒ„์ „ ๋ถˆ์ผ์น˜ ์„ธ์…˜์€ ๋ฌดํšจํ™” |
| `plugins/logging.py` | ADK ํ”Œ๋Ÿฌ๊ทธ์ธ โ€” ์ฑ„ํŒ…์˜ ์œ ์ €์ž…๋ ฅ/์‘๋‹ต/๋„๊ตฌํ˜ธ์ถœ/์ƒํƒœ๋ณ€ํ™”๋ฅผ ์ž๋™ ๊ธฐ๋ก |
| `instructions/` | LLM ํ”„๋กฌํ”„ํŠธ ์›๋ฌธ (์นด๋ฅด๋ฐ€๋ผ ์ธ๊ฒฉ ๊ทœ์น™, ๋””๋ ‰ํ„ฐ ํŒ์ • ์ ˆ์ฐจ). **๋งํˆฌยทํ–‰๋™ ๊ทœ์น™์„ ๊ณ ์น˜๋ ค๋ฉด ์—ฌ๊ธฐ** |
| `callbacks/` | (๋นˆ ์Šค์บํด๋“œ) before_model ์Šคํฌ์ผ๋Ÿฌ ์ตœ์ข… ๋ฐฉ์–ด์„  ์ž๋ฆฌ โ€” 2๋‹จ๊ณ„ |
---
## 4. web/ โ€” ์›น ๋ฆฌ๋” (ํ”„๋ก ํŠธ์—”๋“œ)
`web/index.html` ๋‹จ์ผ ํŒŒ์ผ (๋ฐ”๋‹๋ผ JS, ๋นŒ๋“œ ์—†์Œ). router.py๊ฐ€ ์„œ๋น™.
- **์ฝ๊ธฐ**: ํ™”๋ฉด ๋‹จ์œ„ ํŽ˜์ด์ง• โ€” ์Šคํฌ๋กค ์—†์ด ๋„˜๊น€์œผ๋กœ๋งŒ ์ฝ๋Š”๋‹ค. ์„œ๋ฒ„ ํŽ˜์ด์ง€(์—ฌ๋Ÿฌ ์นด๋“œ ๋ณ‘ํ•ฉ, `paging.py`๊ฐ€ run๋ณ„ ๊ท ํ˜• ๋ถ„ํ•  โ€” ๋ชฉํ‘œ 500์ž, ๋ณ‘ํ•ฉ ํŽ˜์ด์ง€ ๋ฐด๋“œ ์•ฝ 360~760์ž)๋ฅผ ํ”„๋ก ํŠธ๊ฐ€ ๋ทฐํฌํŠธ ์‹ค์ธก(`#page-measure`)์œผ๋กœ ํ™”๋ฉด๋“ค๋กœ ๋ถ„ํ• (`buildScreens`), ๊ฐ™์€ ์ง„ํ–‰๋„ ๋ฒˆํ˜ธ๋ฅผ ๊ณต์œ . ์šฐ์ธก ํด๋ฆญ/โ†’ = ๋‹ค์Œ, ์ขŒ์ธก ํด๋ฆญ/โ† = ์ด์ „(์ฝ๊ธฐ ์ „์šฉ ๋˜๋Œ์•„๋ณด๊ธฐ โ€” ์„ ํƒ ๋ฒˆ๋ณต ๋ถˆ๊ฐ€). ์—ฐ์ถœ ์ปท(์ด๋ฏธ์ง€)์€ ์ž์ฒด ๋ฒˆํ˜ธ๋ฅผ ๊ฐ€์ง„ ๋…๋ฆฝ ํŽ˜์ด์ง€
- **ํƒ€์ดํ‹€**: ์—ํ”ผ์†Œ๋“œ ์ฒซ ํŽ˜์ด์ง€์˜ "์ œNํ™”" ํ—ค๋”๋งŒ ํ‘œ์‹œ โ€” ์นด๋“œ ์†Œ์ œ๋ชฉ์€ ๋ Œ๋”ํ•˜์ง€ ์•Š๋Š”๋‹ค(์ฑ…์˜ ํ๋ฆ„). ์ปท ์บก์…˜์€ ์œ ์ง€
- **์ด์–ด ์ฝ๊ธฐ**: ์„ธ์…˜ ID๋ฅผ `localStorage`์— ๋ณด๊ด€ โ€” ์ƒˆ๋กœ๊ณ ์นจยท์„œ๋ฒ„ ์žฌ์‹œ์ž‘ ํ›„์—๋„ ํ˜„์žฌ ํŽ˜์ด์ง€๋ถ€ํ„ฐ ์žฌ๊ฐœ (ํ‘œ์ง€ ๋ฒ„ํŠผ์ด "๋ฐค์„ ์ž‡๋Š”๋‹ค"๋กœ ๋ฐ”๋€œ). ์—”๋”ฉ์—์„œ ์žฌ์‹œ์ž‘ํ•˜๋ฉด ํ๊ธฐ
- **์บ๋ฆญํ„ฐ ๋ฐœํ™”(ํ•˜์ด๋ธŒ๋ฆฌ๋“œ)**: ์„œ์‚ฌ ์† `character_dialogue`๋Š” ์ปดํŒฉํŠธ char-line(์ขŒ ํ™”์ž๋ช… ยท ์šฐ ๋Œ€์‚ฌ)์œผ๋กœ ๋ณธ๋ฌธ์— ํก์ˆ˜ โ€” ์ฑ…์˜ ํ๋ฆ„ ์šฐ์„ . ์œ ์ €ยท๋กœ๋ผ๋Š” "๋‚˜"๋กœ ํ‘œ๊ธฐ. `dialogue_center`๋Š” ์ค‘์•™ ์ธ์šฉ ์Šคํƒ€์ผ ์œ ์ง€
- **VN ์Šคํ…Œ์ด์ง€**: ์ธํ„ฐ๋ž™์…˜ ํŽ˜์ด์ง€(์„ ํƒ์ง€ยท์ฑ„ํŒ…)์˜ ๋งˆ์ง€๋ง‰ ์ดˆ์ƒ ๋ฐœํ™”๋งŒ ์Šคํ…Œ์ด์ง€(๋‹จ๋… ํ™”๋ฉด) โ€” ์ดˆ์ƒ์ด ํ”„๋ ˆ์ž„(76vh)์„ ์ฑ„์šฐ๊ณ  ํ•˜๋‹จ ํŒจ๋„์— ์žฅ์‹ ์ด๋ฆ„ํ‘œยท๋Œ€์‚ฌ, ๋งˆ์ง€๋ง‰ ๊ทธ๋ฃน์ด๋ฉด ์„ ํƒ์ง€ยท๋ง๊ฑธ๊ธฐ ๋ฒ„ํŠผ ๋‚ด์žฅ. **๋ฐœํ™” ์ง์ „์˜ ์งง์€ ๋„์ž… prose(role=lead_in, 140์ž ์ดํ•˜ ์ž๋™ ์œ ๋„)๋Š” ์Šคํ…Œ์ด์ง€ ์ƒ๋‹จ์— ์–นํžŒ๋‹ค** โ€” ํ•œ ๋ฌธ์žฅ ํŒŒํŽธ ํ™”๋ฉด ๋ฐฉ์ง€. ์ฑ„ํŒ… ๋“œ๋กœ์–ด๊ฐ€ ์—ด๋ฆฌ๋ฉด ์ดˆ์ƒ ์œ„์—์„œ ๋Œ€ํ™”ํ•˜๋Š” ๊ตฌ๋„ (์ฐธ๊ณ : ํ”Œ๋ž˜์ด ์ด๋ฏธ์ง€/7.png)
- **์„ ํƒ์ง€**: `โ€บ` ๋งˆ์ปค + ๊ฐ€๋Š” ๊ตฌ๋ถ„์„  ๋ฆฌ์ŠคํŠธ (๋Œ€ํ™”ํ•˜๋“ฏ). ์„ ํƒ ๊ฒฐ๊ณผ ์‚ฐ๋ฌธ ํ‘œ์‹œ ํ›„ ์šฐ์ธก ํด๋ฆญ์œผ๋กœ ์ง„ํ–‰
- **์ฑ„ํŒ…**: ์นด๋ฅด๋ฐ€๋ผ ์นด๋“œ์—์„œ "โ€ฆ๋ง์„ ๊ฑด๋‹ค" โ†’ ํ•˜๋‹จ ๋“œ๋กœ์–ด. ์ˆ˜๋ ด ์‹œ ์—ฌ์šด(3.4์ดˆ) ํ›„ ์ž๋™ ๋ณต๊ท€
- **์—ฐ์ถœ**: ์ƒ๋‹จ ์ง„ํ–‰ ๋ฐ”(n/์ดํŽ˜์ด์ง€ = ์นด๋“œ 61 + ์ปท 5 = 66), ์ค‘์š” ์žฅ๋ฉด ์ด๋ฏธ์ง€ ํŽ˜์ด์ง€, ํ‘œ์ง€
- ์ด๋ฏธ์ง€ ์›๋ณธ: `์นด๋ฅด๋ฐ€๋ผ/0630โ€ฆ/์ด๋ฏธ์ง€/` (router์˜ `/assets`๋กœ ์„œ๋น™, ๋งคํ•‘์€ `IMAGE_MAP`)
---
## 5. scripts/ ยท tests/ ยท docs/
| ๊ฒฝ๋กœ | ์—ญํ•  |
|---|---|
| `scripts/beatcard_convert.py` | **๋น„ํŠธ์นด๋“œ MD โ†’ content/ YAML ๋ณ€ํ™˜๊ธฐ.** ์„ ํƒ์ง€-๊ฒฐ๊ณผ ๋งค์นญ, ๋ถ„๊ธฐ ํ•ด์„, ๋„๋‹ฌ์  ์šฐํšŒ ๋ฐฉ์ง€. ์ฝ˜ํ…์ธ  ๊ฐฑ์‹  ์‹œ ์žฌ์‹คํ–‰ |
| `scripts/play.py` | ํ„ฐ๋ฏธ๋„ ํ”Œ๋ ˆ์ด์–ด. `--policy trust/doubt/neutral --quiet`๋กœ ์—”๋”ฉ 3์ข… ์ž๋™ ๊ฒ€์ฆ ๊ฐ€๋Šฅ |
| `tests/` (18๊ฐœ) | `test_story.py` ์ฝ๊ธฐ/์—”๋”ฉ/์Šคํฌ์ผ๋Ÿฌ ยท `test_pocket.py` ์ฑ„ํŒ… ํŒŒ์ดํ”„๋ผ์ธ(๊ฐ€์งœ LLM์œผ๋กœ ๊ฒฐ์ •์  ๊ฒ€์ฆ) ยท `test_integration.py` ์ฝ๊ธฐโ†’์ฑ„ํŒ…โ†’์—”๋”ฉ ์ „์ฒด ๊ด€ํ†ต ยท `fakes.py` ์Šคํฌ๋ฆฝํŠธ LLM ํ…Œ์ŠคํŠธ ๋”๋ธ” |
| `docs/plans/mvp.md` | ๋งˆ์ผ์Šคํ†ค M0~M6 ๊ณ„ํšยท์ƒํƒœยท๊ฒฐ์ • ๋กœ๊ทธ |
| `docs/contract/demo_api.md` | ์›น ๋ฆฌ๋” โ†” ์„œ๋ฒ„ HTTP API ๊ณ„์•ฝ |
| `docs/contract/beatcard_guide.md` | ๋น„ํŠธ์นด๋“œ ์ž‘์„ฑ ๊ฐ€์ด๋“œ (๊ถŒ์žฅ ๋ถ„๋Ÿ‰ยท3๋ง‰ ํŒจํ„ดยท๋ฆฌ๋“œ์ธ ๊ทœ์น™ โ€” E07~ ์ง‘ํ•„ ์ฐธ๊ณ , validate ๊ฒฝ๊ณ ์™€ ์—ฐ๋™) |
| `docs/ARCHITECTURE.md` | ๋ณธ ๋ฌธ์„œ |
---
## 6. ๋Ÿฐํƒ€์ž„ ํ๋ฆ„ (์‹ค์ œ๋กœ ์–ด๋–ป๊ฒŒ ๋„๋‚˜)
### ์ฝ๊ธฐ ํ„ด (LLM ์—†์Œ, 0์›)
```
[ํฐ/๋ธŒ๋ผ์šฐ์ €] ์šฐ์ธก ํด๋ฆญ
โ†’ POST /api/session/{id}/advance (router.py)
โ†’ StoryEngine.advance() (story.py)
์นด๋“œ next ๋”ฐ๋ผ ์ด๋™ ยท ์‡ ์•ฝ๋„/์‹ ๋ขฐ์ถ• ๋ฐ˜์˜ ยท ์‹ฌ์ธต ๊ฒŒ์ดํŠธ ํŒ์ •
โ†’ ์นด๋“œ JSON ์‘๋‹ต โ†’ ํ™”๋ฉด ๋ Œ๋”
```
### ์„ ํƒ ํ„ด (LLM ์—†์Œ)
```
์„ ํƒ์ง€ ํด๋ฆญ โ†’ POST /choose {index}
โ†’ StoryEngine.choose(): ์‹ ๋ขฐ์ถ• ยฑ12 ยท ๋ถ„๊ธฐ(next/preset) ๊ฒฐ์ • ยท ๊ฒฐ๊ณผ ์‚ฐ๋ฌธ ๋ฐ˜ํ™˜
```
### ์ฑ„ํŒ… ํ„ด (LLM 2ํšŒ ํ˜ธ์ถœ โ€” ์นด๋ฅด๋ฐ€๋ผ 1 + ๋””๋ ‰ํ„ฐ 1)
```
"โ€ฆ๋ง์„ ๊ฑด๋‹ค" โ†’ POST /pocket/open โ†’ PocketService.open() (์นด๋ฅด๋ฐ€๋ผ ์นด๋“œ ๊ฒ€์ฆ + ์ƒํƒœ ์‹œ๋”ฉ)
๋ฉ”์‹œ์ง€ ์ „์†ก โ†’ POST /pocket/turn
โ†’ ADK Runner: ๋กœ๋” โ†’ ๊ฐ€๋“œ โ†’ ์นด๋ฅด๋ฐ€๋ผ(LLM) โ†’ ๋””๋ ‰ํ„ฐ(LLM+๋„๊ตฌ)
โ†’ PocketService๊ฐ€ ์ˆ˜๋ ด ์žฌ๊ฒ€์ฆ (3ํ„ด ๋ฏธ๋งŒ ๊ธฐ๊ฐ / 6ํ„ด ๊ฐ•์ œ ์ข…๋ฃŒ)
โ†’ ์ˆ˜๋ ด ์‹œ: ์ˆจ์€ ์ƒํƒœ๋ฅผ ๋ณธํŽธ์— ์ปค๋ฐ‹ํ•˜๊ณ  ์ฝ๊ธฐ๋กœ ๋ณต๊ท€
```
### ์—”๋”ฉ (LLM ์—†์Œ)
```
E06 ๋(โ†’E07 ์ฐธ์กฐ) ๋„๋‹ฌ โ†’ story.py๊ฐ€ ending_rules ํ‰๊ฐ€:
doubt โ‰ฅ 30 โ†’ โ‘  ์ฒ˜๋‹จ (์›์ž‘ ์บ๋…ผ)
trust โ‰ฅ 60 & doubt<20 โ†’ โ‘ก ์˜์› (๋‹คํฌ ๋กœ๋งจ์Šค)
๊ทธ ์™ธ โ†’ โ‘ข ๊ตฌ์› (์ด๋ณ„)
```
---
## 7. ์‹คํ–‰ ๋ช…๋ น ๋ชจ์Œ
```bash
# ์›น ๋ฆฌ๋” (ํฐ ์ ‘์† ํ—ˆ์šฉ)
.venv/bin/uvicorn engine.router:app --host 0.0.0.0 --port 8777
# ํ„ฐ๋ฏธ๋„ ํ”Œ๋ ˆ์ด / ์ž๋™ ์—”๋”ฉ ๊ฒ€์ฆ
.venv/bin/python scripts/play.py
.venv/bin/python scripts/play.py --policy trust --quiet
# ์ฝ˜ํ…์ธ  ๊ฐฑ์‹  (๋น„ํŠธ์นด๋“œ MD ์ˆ˜์ • ํ›„)
.venv/bin/python scripts/beatcard_convert.py "์นด๋ฅด๋ฐ€๋ผ/0701_์นด๋ฅด๋ฐ€๋ผ/๋น„ํŠธ์นด๋“œ" -o content/beatcards
.venv/bin/python -m engine.schemas.validate content/
# ํ…Œ์ŠคํŠธ
.venv/bin/python -m pytest tests/ -q
```
---
## 8. "์ด๊ฑธ ๋ฐ”๊พธ๊ณ  ์‹ถ์œผ๋ฉด ์–ด๋””๋ฅผ ๋ณด๋‚˜" ์น˜ํŠธ์‹œํŠธ
| ํ•˜๊ณ  ์‹ถ์€ ๊ฒƒ | ๋ณด๋Š” ๊ณณ |
|---|---|
| ์ด์•ผ๊ธฐ ๋ณธ๋ฌธยท์„ ํƒ์ง€ ์ˆ˜์ • | ๋น„ํŠธ์นด๋“œ MD (`์นด๋ฅด๋ฐ€๋ผ/0701โ€ฆ/๋น„ํŠธ์นด๋“œ/`) โ†’ ๋ณ€ํ™˜๊ธฐ ์žฌ์‹คํ–‰ |
| ์นด๋ฅด๋ฐ€๋ผ ๋งํˆฌยทํ–‰๋™ ๊ทœ์น™ | `engine/instructions/character_carmilla.md` |
| ๋Œ€ํ™” ์ข…๋ฃŒ(์ˆ˜๋ ด) ํŒ์ • ๊ธฐ์ค€ | `engine/instructions/director.md` + `engine/services/pocket.py` (MIN_TURNS ๋“ฑ) |
| ์—”๋”ฉ ์กฐ๊ฑด ์ˆซ์ž | `content/chapters/carmilla_chapter1.yaml` (ending_rules) |
| ์‹ ๋ขฐ์ถ• ์ด๋™๋Ÿ‰ยท๋ ˆ๋ฒจ ๊ฒฝ๊ณ„ | `engine/services/state.py` (AXIS_DELTA, trust_level) |
| ์ฑ„ํŒ… ํ„ด ์ƒํ•œ | `content/chapters/carmilla_chapter1.yaml` (pocket_max_turns) |
| ํŽ˜์ด์ง€๋‹น ๊ธ€ ๋ฐ€๋„ | `content/chapters/carmilla_chapter1.yaml`์— `page_target_chars` (๋ฏธ์„ค์ • ์‹œ 500) |
| ์—ฐ์ถœ ์ด๋ฏธ์ง€ ์ถ”๊ฐ€ยท๊ต์ฒด | `engine/router.py` (IMAGE_MAP) + `scripts/sync_assets.py` ์žฌ์‹คํ–‰ (์›์ฒœ: ์นด๋ฅด๋ฐ€๋ผ/0701_์นด๋ฅด๋ฐ€๋ผ/์ด๋ฏธ์ง€/) |
| ์žฅ์†Œ๋ณ„ ๋ฐฐ๊ฒฝ ์ถ”๊ฐ€ยท๊ต์ฒด | `engine/router.py` (BACKGROUND_MAP โ€” ์นด๋“œ `#์žฅ์†Œ/` ํƒœ๊ทธ ๊ธฐ์ค€) + sync_assets |
| ํ™”์ž ์ดˆ์ƒ ์ถ”๊ฐ€ยท๊ต์ฒด (VN ๋ฐœํ™”) | `engine/router.py` (SPEAKER_PORTRAIT) + sync_assets |
| ์ธํŠธ๋กœ ์‹œํ€€์Šค(์ž‘ํ’ˆยท์ธ๋ฌผ ์†Œ๊ฐœ) ๊ต์ฒด | `engine/router.py` (INTRO_IMAGES) |
| ํ™”๋ฉด ๋””์ž์ธยท์ธํ„ฐ๋ž™์…˜ | `web/index.html` |
| ์„ธ์…˜ ์ €์žฅยท๋ณต์› ์ •์ฑ… (TTL, ๋ฒ„์ „ ๋ฌดํšจํ™”) | `engine/repositories/play_session.py` + `engine/router.py` (`_save`/`_restore`) |
| ์ •์ฒด์ง๊ฒฉ/์•ฝ์  ํŠธ๋ฆฌ๊ฑฐ ๋‹จ์–ด | `engine/agents/input_guard.py` (TRIGGERS) |
| LLM ๋ชจ๋ธ ๊ต์ฒด | `.env`์— `LLM_MODEL=...` / `CHARACTER_MODEL=...` |