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๋ช…(๋กœ๋ผ=ํ”Œ๋ ˆ์ด์–ด, ์นด๋ฅด๋ฐ€๋ผ, ์•„๋ฒ„์ง€). ์„ฑ๊ฒฉยทํ™”๋ฒ•ยท์ง€์‹๊ฒฝ๊ณ„

์นด๋“œ ํ•œ ์žฅ์˜ ๊ตฌ์กฐ (์˜ˆ):

- 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. ์‹คํ–‰ ๋ช…๋ น ๋ชจ์Œ

# ์›น ๋ฆฌ๋” (ํฐ ์ ‘์† ํ—ˆ์šฉ)
.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=...