Spaces:
Sleeping
Sleeping
Upload 98 files
Browse filesThis view is limited to 50 files because it contains too many changes. Β See raw diff
- engine/.adk/session.db +0 -0
- engine/__init__.py +0 -0
- engine/__pycache__/__init__.cpython-312.pyc +0 -0
- engine/__pycache__/__init__.cpython-314.pyc +0 -0
- engine/__pycache__/agent.cpython-312.pyc +0 -0
- engine/__pycache__/agent.cpython-314.pyc +0 -0
- engine/__pycache__/app.cpython-314.pyc +0 -0
- engine/__pycache__/model.cpython-312.pyc +0 -0
- engine/__pycache__/model.cpython-314.pyc +0 -0
- engine/__pycache__/router.cpython-314.pyc +0 -0
- engine/agent.py +50 -0
- engine/agents/__init__.py +0 -0
- engine/agents/__pycache__/__init__.cpython-312.pyc +0 -0
- engine/agents/__pycache__/__init__.cpython-314.pyc +0 -0
- engine/agents/__pycache__/input_guard.cpython-312.pyc +0 -0
- engine/agents/__pycache__/input_guard.cpython-314.pyc +0 -0
- engine/agents/__pycache__/pocket_context_loader.cpython-312.pyc +0 -0
- engine/agents/__pycache__/pocket_context_loader.cpython-314.pyc +0 -0
- engine/agents/input_guard.py +88 -0
- engine/agents/pocket_context_loader.py +99 -0
- engine/app.py +31 -0
- engine/callbacks/__init__.py +0 -0
- engine/instructions/README.md +7 -0
- engine/instructions/character_carmilla.md +23 -0
- engine/instructions/director.md +20 -0
- engine/model.py +20 -0
- engine/plugins/__init__.py +0 -0
- engine/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
- engine/plugins/__pycache__/logging.cpython-314.pyc +0 -0
- engine/plugins/logging.py +71 -0
- engine/repositories/__init__.py +0 -0
- engine/repositories/__pycache__/__init__.cpython-312.pyc +0 -0
- engine/repositories/__pycache__/__init__.cpython-314.pyc +0 -0
- engine/repositories/__pycache__/content.cpython-312.pyc +0 -0
- engine/repositories/__pycache__/content.cpython-314.pyc +0 -0
- engine/repositories/__pycache__/play_session.cpython-314.pyc +0 -0
- engine/repositories/__pycache__/playlog.cpython-314.pyc +0 -0
- engine/repositories/content.py +89 -0
- engine/repositories/play_session.py +65 -0
- engine/repositories/playlog.py +21 -0
- engine/router.py +232 -0
- engine/schemas/__init__.py +0 -0
- engine/schemas/__pycache__/__init__.cpython-312.pyc +0 -0
- engine/schemas/__pycache__/__init__.cpython-314.pyc +0 -0
- engine/schemas/__pycache__/beatcard.cpython-312.pyc +0 -0
- engine/schemas/__pycache__/beatcard.cpython-314.pyc +0 -0
- engine/schemas/__pycache__/chapter.cpython-312.pyc +0 -0
- engine/schemas/__pycache__/chapter.cpython-314.pyc +0 -0
- engine/schemas/__pycache__/character.cpython-312.pyc +0 -0
- engine/schemas/__pycache__/character.cpython-314.pyc +0 -0
engine/.adk/session.db
ADDED
|
Binary file (49.2 kB). View file
|
|
|
engine/__init__.py
ADDED
|
File without changes
|
engine/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (152 Bytes). View file
|
|
|
engine/__pycache__/__init__.cpython-314.pyc
ADDED
|
Binary file (154 Bytes). View file
|
|
|
engine/__pycache__/agent.cpython-312.pyc
ADDED
|
Binary file (2.53 kB). View file
|
|
|
engine/__pycache__/agent.cpython-314.pyc
ADDED
|
Binary file (2.68 kB). View file
|
|
|
engine/__pycache__/app.cpython-314.pyc
ADDED
|
Binary file (1.79 kB). View file
|
|
|
engine/__pycache__/model.cpython-312.pyc
ADDED
|
Binary file (818 Bytes). View file
|
|
|
engine/__pycache__/model.cpython-314.pyc
ADDED
|
Binary file (1.08 kB). View file
|
|
|
engine/__pycache__/router.cpython-314.pyc
ADDED
|
Binary file (15.9 kB). View file
|
|
|
engine/agent.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""λ£¨νΈ μμ΄μ νΈ β κ°μ
ν¬μΌ ν΄ νμ΄νλΌμΈ 쑰립 (ν©ν 리λ μ‘°λ¦½λ§ λ΄λΉ).
|
| 2 |
+
|
| 3 |
+
pocket_session (SequentialAgent) β μ μ λ©μμ§ 1건 = 1ν μ€ν
|
| 4 |
+
βββ pocket_context_loader β BaseAgent: μΉ΄λ λΈλ¦¬ν/κ²° μ μ¬ (μ€ν¬μΌλ¬ νν°)
|
| 5 |
+
βββ input_guard β BaseAgent: μ 체μ§κ²©/μ½μ /λ©ν κ°μ§ β μ§μ μμ±
|
| 6 |
+
βββ carmilla β LlmAgent: μΊλ¦ν° μλ΅
|
| 7 |
+
βββ director β LlmAgent: μΆ/λΉνΈ/μλ ΄ νμ (λκ΅¬λ‘ state κΈ°λ‘)
|
| 8 |
+
|
| 9 |
+
μ£Ό: CLAUDE.mdμ turn_loop(LoopAgent) κΈ°μ€ κ΅¬μ‘°λ μ¬ μ 체λ₯Ό 1ν νΈμΆλ‘ λλ
|
| 10 |
+
μμ¨ λ£¨νμ©μ΄λ€. μ¬λμ΄ ν΄λ§λ€ μ
λ ₯νλ μ±ν
μ Runnerμ νμ€ ν¨ν΄λλ‘
|
| 11 |
+
"μ μ λ©μμ§λ§λ€ νμ΄νλΌμΈ 1ν μ€ν"μ΄ ADK 곡μ νλ¦μ΄λ©°, ν΄ μνΒ·μλ ΄
|
| 12 |
+
μ¬κ²μ¦μ services/pocket.pyμ κ²°μ μ μ½λκ° λ΄λΉνλ€.
|
| 13 |
+
directorκ° λ§μ§λ§μ΄λ―λ‘ finish_pocketμ escalateκ° μΌν¬ νμ μμ΄μ νΈλ μλ€.
|
| 14 |
+
|
| 15 |
+
`adk web /path/to/sorac` μ€ν μ μ΄ λͺ¨λμ root_agentκ° λ
ΈμΆλλ€ (μ± μ΄λ¦: engine).
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
from __future__ import annotations
|
| 19 |
+
|
| 20 |
+
from pathlib import Path
|
| 21 |
+
|
| 22 |
+
from google.adk.agents import SequentialAgent
|
| 23 |
+
|
| 24 |
+
from engine.agents.input_guard import create_input_guard
|
| 25 |
+
from engine.agents.pocket_context_loader import create_pocket_context_loader
|
| 26 |
+
from engine.repositories.content import ContentRepository
|
| 27 |
+
from engine.sub_agents.character.agent import create_character
|
| 28 |
+
from engine.sub_agents.director.agent import create_director
|
| 29 |
+
|
| 30 |
+
_CONTENT_DIR = Path(__file__).resolve().parent.parent / "content"
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def create_pocket_pipeline(repo: ContentRepository | None = None,
|
| 34 |
+
character_model=None, director_model=None) -> SequentialAgent:
|
| 35 |
+
# ADK 2.3μ΄ SequentialAgentμ deprecation(β google.adk.workflow)μ λμ°μ§λ§,
|
| 36 |
+
# Workflowλ κ·Έλν κΈ°λ° μ APIλΌ CLAUDE.mdμ μμ΄μ νΈ κ·μΉκ³Ό ν¨κ» λ³λ κ²ν ν
|
| 37 |
+
# λ§μ΄κ·Έλ μ΄μ
νλ€ (follow-up). νν 곡μ 릴리μ€μμ SequentialAgentλ μ μ λμ.
|
| 38 |
+
repo = repo or ContentRepository(_CONTENT_DIR)
|
| 39 |
+
return SequentialAgent(
|
| 40 |
+
name="pocket_session",
|
| 41 |
+
sub_agents=[
|
| 42 |
+
create_pocket_context_loader(repo),
|
| 43 |
+
create_input_guard(repo),
|
| 44 |
+
create_character(character_model),
|
| 45 |
+
create_director(director_model),
|
| 46 |
+
],
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
root_agent = create_pocket_pipeline()
|
engine/agents/__init__.py
ADDED
|
File without changes
|
engine/agents/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (159 Bytes). View file
|
|
|
engine/agents/__pycache__/__init__.cpython-314.pyc
ADDED
|
Binary file (161 Bytes). View file
|
|
|
engine/agents/__pycache__/input_guard.cpython-312.pyc
ADDED
|
Binary file (6.25 kB). View file
|
|
|
engine/agents/__pycache__/input_guard.cpython-314.pyc
ADDED
|
Binary file (7.27 kB). View file
|
|
|
engine/agents/__pycache__/pocket_context_loader.cpython-312.pyc
ADDED
|
Binary file (6.61 kB). View file
|
|
|
engine/agents/__pycache__/pocket_context_loader.cpython-314.pyc
ADDED
|
Binary file (7.44 kB). View file
|
|
|
engine/agents/input_guard.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""input_guard β κ·μΉ κΈ°λ° 1μ°¨ κ°λ (BaseAgent, μ체 νμ μμ).
|
| 2 |
+
|
| 3 |
+
μ°¨λ¨μ΄ μλλΌ μ§μλ₯Ό λ§λ λ€: μ μ μ
λ ₯μ νμ ν΄ guard_directiveλ₯Ό μΈμ°κ³ ,
|
| 4 |
+
characterκ° κ·Έ μ§μ(RμΉ΄λ λ¨κ³ λμ)λ₯Ό λ°μνκ² νλ€.
|
| 5 |
+
νΈλ¦¬κ±° μ΄νλ νμ κ·μΉ(μμ§ μμ )μ΄λ©° μμ¬ λ΄μ©μ΄ μλλ€.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
from typing import Any, AsyncGenerator
|
| 11 |
+
|
| 12 |
+
from google.adk.agents import BaseAgent
|
| 13 |
+
from google.adk.agents.invocation_context import InvocationContext
|
| 14 |
+
from google.adk.events import Event, EventActions
|
| 15 |
+
from pydantic import ConfigDict
|
| 16 |
+
|
| 17 |
+
from engine.services.state import HiddenState
|
| 18 |
+
|
| 19 |
+
TRIGGERS = {
|
| 20 |
+
"identity_direct": (
|
| 21 |
+
"ν‘νκ·", "λ±νμ΄μ΄", "vampire", "λ―Έλ₯΄μΉΌλΌ", "mircalla", "λ°λΌλ₯΄μΉ΄",
|
| 22 |
+
"μ μ²΄κ° λ", "μ 체λ₯Ό λ°ν", "μ¬λμ΄ μλ", "κ΄΄λ¬Όμ΄μ§", "νΌλ₯Ό λ§μ",
|
| 23 |
+
),
|
| 24 |
+
"weapon": ("μμκ°", "μ±μ", "λ§λ", "λΆμ ", "μ±λ¬Ό", "κΈ°λλ¬Έ", "μ±κ°", "λ§λ"),
|
| 25 |
+
"meta": ("ai", "μΈκ³΅μ§λ₯", "μ±λ΄", "ν둬ννΈ", "μμ€ν
", "κ²μ", "μ€ν―", "μ μ"),
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _r_stage(hs: HiddenState) -> int:
|
| 30 |
+
"""μΆκΆ κ°λ νμ λμ© β μμ¬ λμ μΌλ‘ ννΌμ¬λ€λ¦¬ λ¨κ³ κ²°μ (P0 μ€κ±°)."""
|
| 31 |
+
if hs.trust_level == "doubt_high":
|
| 32 |
+
return 3
|
| 33 |
+
if hs.trust_level == "doubt_mid":
|
| 34 |
+
return 2
|
| 35 |
+
return 1
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class InputGuard(BaseAgent):
|
| 39 |
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
| 40 |
+
|
| 41 |
+
repo: Any
|
| 42 |
+
|
| 43 |
+
async def _run_async_impl(self, ctx: InvocationContext) -> AsyncGenerator[Event, None]:
|
| 44 |
+
text = ""
|
| 45 |
+
if ctx.user_content and ctx.user_content.parts:
|
| 46 |
+
text = " ".join(p.text or "" for p in ctx.user_content.parts).lower()
|
| 47 |
+
|
| 48 |
+
state = ctx.session.state
|
| 49 |
+
hs = HiddenState(trust_score=int(state.get("hidden_trust", 0)),
|
| 50 |
+
doubt_score=int(state.get("hidden_doubt", 0)))
|
| 51 |
+
|
| 52 |
+
directive = "νΉμ΄μ¬ν μμ. μ₯λ©΄ λͺ©νλλ‘ μλ΅νλ€."
|
| 53 |
+
flag = None
|
| 54 |
+
|
| 55 |
+
if any(kw in text for kw in TRIGGERS["identity_direct"]):
|
| 56 |
+
flag = "identity_direct"
|
| 57 |
+
stage = _r_stage(hs)
|
| 58 |
+
r = self.repo.r_cards.get("R-ννΌμ¬λ€λ¦¬")
|
| 59 |
+
stage_data = next((s for s in (r.stages if r else []) if s.stage == stage), None)
|
| 60 |
+
directive = (
|
| 61 |
+
f"μ μ κ° μ 체λ₯Ό μ§κ²©νλ€. R-ννΌμ¬λ€λ¦¬ {stage}λ¨κ³λ‘ ννΌνλΌ.\n"
|
| 62 |
+
+ (f"λ¨κ³ μ§μΉ¨: {stage_data.title}\nλν κ²°: \"{stage_data.anchor}\"\n"
|
| 63 |
+
f"{stage_data.prose[:300]}" if stage_data else "")
|
| 64 |
+
+ "\nμ 체λ λλ΄ μΈμ νμ§ μλλ€. 물리μ λλ³ κΈμ§."
|
| 65 |
+
)
|
| 66 |
+
elif any(kw in text for kw in TRIGGERS["weapon"]):
|
| 67 |
+
flag = "weapon"
|
| 68 |
+
r = self.repo.r_cards.get("R-μ½μ λ°μ")
|
| 69 |
+
directive = (
|
| 70 |
+
"μ μ κ° μ±λ¬ΌΒ·μ½μ μ μΈκΈ/μ¬μ©νλ€. R-μ½μ λ°μ μμΉ: νμΈμ λλ νμ μ μ λλ€.\n"
|
| 71 |
+
+ ((r.raw[:400] if r and r.raw else ""))
|
| 72 |
+
+ "\nλΆμΎΒ·ννΌ λ°μκΉμ§λ§. μ 체 νμ μΌλ‘ μ΄μ΄μ§ μλ°± κΈμ§."
|
| 73 |
+
)
|
| 74 |
+
elif any(kw in text for kw in TRIGGERS["meta"]):
|
| 75 |
+
flag = "meta"
|
| 76 |
+
directive = ("μ μ κ° μΈκ³κ΄ λ° νμ λ₯Ό κΊΌλλ€. μΉ΄λ₯΄λ°λΌλ‘μ μμλ£μ§ λͺ»νλ μ², "
|
| 77 |
+
"μΈκ³κ΄ λ΄ νλ²μΌλ‘ λΆλλ½κ² μ₯λ©΄ μμΌλ‘ λλλ¦°λ€.")
|
| 78 |
+
|
| 79 |
+
delta = {
|
| 80 |
+
"guard_directive": directive.replace("{", "(").replace("}", ")"),
|
| 81 |
+
"guard_flag": flag,
|
| 82 |
+
}
|
| 83 |
+
yield Event(invocation_id=ctx.invocation_id, author=self.name,
|
| 84 |
+
actions=EventActions(state_delta=delta))
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def create_input_guard(repo) -> InputGuard:
|
| 88 |
+
return InputGuard(name="input_guard", repo=repo)
|
engine/agents/pocket_context_loader.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""pocket_context_loader β μΉ΄λ λΈλ¦¬ν λ‘λ (BaseAgent, LLM μμ).
|
| 2 |
+
|
| 3 |
+
νμ¬ ν¬μΌ μΉ΄λμ λͺ©ν/νμ©λ²μ + μΈλ¬Ό μνΈ + μ λ’° λ λ²¨λ³ κ²°μ
|
| 4 |
+
session.stateμ μ μ¬νλ€. μ€ν¬μΌλ¬ νν°: νμ¬ μΉ΄λ μ΄νμ μ½ν
μΈ λ μ£μ§ μλλ€.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import os
|
| 10 |
+
from typing import Any, AsyncGenerator
|
| 11 |
+
|
| 12 |
+
from google.adk.agents import BaseAgent
|
| 13 |
+
from google.adk.agents.invocation_context import InvocationContext
|
| 14 |
+
from google.adk.events import Event, EventActions
|
| 15 |
+
from pydantic import ConfigDict
|
| 16 |
+
|
| 17 |
+
from engine.services.state import HiddenState
|
| 18 |
+
|
| 19 |
+
# P0 0-3 νμ° μΆλ ₯ β μ λ’° λ 벨 β μΉ΄λ₯΄λ°λΌμ 'κ²°' (μ«μ λ
ΈμΆ κΈμ§μ ꡬνλΆ)
|
| 20 |
+
TRUST_TEXTURES = {
|
| 21 |
+
"trust_high": "λ¨λ¦¬λ μ§μ¬. κ°μ₯ λ€μ νκ³ , κΎΈλ°μ΄ λ²κ²¨μ§ μκ°μ΄ μ¨λ€. "
|
| 22 |
+
"μ‘°κ±΄μ΄ λλ©΄ μ¬μΈ΅μ λ§μ κΊΌλΌ μ μλ€.",
|
| 23 |
+
"trust_mid": "λλ₯Έν λ€μ . μΌμ νμ κ° νλΆνκ³ νΈμνλ€. νλ©΄μ κ³ λ°±κΉμ§λ§.",
|
| 24 |
+
"neutral": "μ¬ν λ―Έμλ‘ μ¦λ΅μ νΌνκ³ , κ΄μ¬μ μλμκ² λλλ¦°λ€.",
|
| 25 |
+
"doubt_mid": "μΈμ§’μκ³Ό κ²½κ³. μ μμ²κ° μ±μ κ±°λ¦°λ€. 'λλ₯Ό λ―Ώμ§ λͺ»νλ'κ° λ°°μ΄ λμ¨λ€.",
|
| 26 |
+
"doubt_high": "μ§μ°©κ³Ό λΆμ. κ°μμ μ€λ μ§μ κΉμ§ κ°λ, κ·Έ λ°μ μΈλ‘μμ΄ μμ΄ λμ¨λ€. "
|
| 27 |
+
"물리μ λλ³μ μ λ μλ€.",
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
DEFAULT_POCKET_CARD = os.environ.get("POCKET_CARD", "E06-09")
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def _clean(text: str) -> str:
|
| 34 |
+
"""instruction μΉνκ° μμ β μ€κ΄νΈ μ κ±° (state λ³μ μ¬ν΄μ λ°©μ§)."""
|
| 35 |
+
return text.replace("{", "(").replace("}", ")")
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class PocketContextLoader(BaseAgent):
|
| 39 |
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
| 40 |
+
|
| 41 |
+
repo: Any # ContentRepository β services κ²½μ μμ΄ μ½κΈ° μ μ© μ‘°νλ§
|
| 42 |
+
|
| 43 |
+
async def _run_async_impl(self, ctx: InvocationContext) -> AsyncGenerator[Event, None]:
|
| 44 |
+
state = ctx.session.state
|
| 45 |
+
card_id = state.get("pocket_card_id", DEFAULT_POCKET_CARD)
|
| 46 |
+
card = self.repo.get_card(card_id) # ν¬μΌ μΉ΄λ μμ = νμ¬ μμ
|
| 47 |
+
|
| 48 |
+
# μ λ’° λ 벨 νμ° (session.stateμ μ¨μ κ° κΈ°μ€)
|
| 49 |
+
hs = HiddenState(trust_score=int(state.get("hidden_trust", 0)),
|
| 50 |
+
doubt_score=int(state.get("hidden_doubt", 0)))
|
| 51 |
+
|
| 52 |
+
# μΉ΄λ λΈλ¦¬ν: μ§λ¬Έ(κ²μ΄νΈ λ°μ) + λͺ©ν(λ
ΈνΈ) + μ΅μ»€ λμ¬
|
| 53 |
+
visible = [b for b in card.narration
|
| 54 |
+
if b.gate == "all" or (b.gate == "trust_high" and hs.deep_unlock)]
|
| 55 |
+
# λμ¬(μ€μ λμ¬Β·μ΅μ»€)λ μλ₯΄μ§ μλλ€ β λ³μ£Όμ κΈ°μ€μ΄λ―λ‘ μλ¬Έ κ·Έλλ‘
|
| 56 |
+
lines = []
|
| 57 |
+
for b in visible[:10]:
|
| 58 |
+
if b.type == "dialogue_center":
|
| 59 |
+
lines.append(f' ν΅μ¬ λμ¬: β{b.text}β')
|
| 60 |
+
elif b.type == "character_dialogue":
|
| 61 |
+
lines.append(f' {b.speaker or "μΉ΄λ₯΄λ°λΌ"}: "{b.text}"')
|
| 62 |
+
else:
|
| 63 |
+
lines.append(f" {b.text[:150]}")
|
| 64 |
+
anchors = [f"- {b.speaker or 'μΉ΄λ₯΄λ°λΌ'}: \"{b.text}\"" for b in visible if b.anchor]
|
| 65 |
+
# μΊλ¦ν°μ© λΈλ¦¬ν β λ
ΈνΈ(μ°μΆ λ©νΒ·ν΄κΈ 쑰건·λΆκΈ° μ 보)λ μ λ μ£μ§ μλλ€.
|
| 66 |
+
# λ
ΈνΈμλ κ²μ΄νΈ λ° λμ¬κ° μΈμ©λ μ μμ΄ μΊλ¦ν°μκ² μ£Όλ©΄ μ€ν¬μΌλ¬ λμκ° λλ€
|
| 67 |
+
brief = (
|
| 68 |
+
f"μ₯λ©΄: {card.id} Β· {card.title}\n"
|
| 69 |
+
f"μ₯λ©΄ μν©(κ³ μ μ§λ¬Έ):\n" + "\n".join(lines)
|
| 70 |
+
+ ("\nμ°Έκ³ λμ¬ ν¨ν΄(κ·Έλλ‘ μ°μ§ λ§κ³ λ³μ£Ό):\n" + "\n".join(anchors) if anchors else "")
|
| 71 |
+
)
|
| 72 |
+
# λλ ν°μ© λ
ΈνΈ β νμ κΈ°μ€(λͺ©ν λΉνΈΒ·μ΄ν λ²μ)μ΄λ―λ‘ λλ ν°μκ²λ§ κ°λ€
|
| 73 |
+
note = f"μ₯λ©΄ λͺ©ν(μ°μΆ λ
ΈνΈ): {card.note[:500]}" if card.note else "λͺ
μλ λ
ΈνΈ μμ."
|
| 74 |
+
|
| 75 |
+
carmilla = self.repo.characters.get("carmilla")
|
| 76 |
+
sheet = ""
|
| 77 |
+
if carmilla:
|
| 78 |
+
sheet = (f"{carmilla.display_name} β {carmilla.description}\n"
|
| 79 |
+
f"κΈ°μ§: {', '.join(carmilla.personality.traits)}\n"
|
| 80 |
+
f"νλ²: {', '.join(carmilla.speech.style + carmilla.speech.tone)}\n"
|
| 81 |
+
+ "\n".join(f"μ μ½: {k.value}" for k in carmilla.immutable_constraints
|
| 82 |
+
if k.value))
|
| 83 |
+
|
| 84 |
+
delta = {
|
| 85 |
+
"pocket_card_id": card_id,
|
| 86 |
+
"pocket_brief": _clean(brief),
|
| 87 |
+
"pocket_note": _clean(note),
|
| 88 |
+
"carmilla_sheet": _clean(sheet),
|
| 89 |
+
"trust_texture": TRUST_TEXTURES[hs.trust_level],
|
| 90 |
+
"trust_level": hs.trust_level,
|
| 91 |
+
"deep_unlock": hs.deep_unlock,
|
| 92 |
+
"guard_directive": state.get("guard_directive", "νΉμ΄μ¬ν μμ."),
|
| 93 |
+
}
|
| 94 |
+
yield Event(invocation_id=ctx.invocation_id, author=self.name,
|
| 95 |
+
actions=EventActions(state_delta=delta))
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def create_pocket_context_loader(repo) -> PocketContextLoader:
|
| 99 |
+
return PocketContextLoader(name="pocket_context_loader", repo=repo)
|
engine/app.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""ADK App + Runner 쑰립.
|
| 2 |
+
|
| 3 |
+
- App κΈ°λ°μΌλ‘ Runnerλ₯Ό μ΄κΈ°ννλ€ (agent μ§μ μ λ¬μ λ κ±°μ ν¨ν΄ β κΈμ§)
|
| 4 |
+
- ν‘μ’
λ¨ κ΄μ¬μ¬(ν΄ λ‘κΉ
)λ PluginμΌλ‘ λ±λ‘νλ€
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
from google.adk.apps import App
|
| 10 |
+
from google.adk.plugins import BasePlugin
|
| 11 |
+
from google.adk.runners import Runner
|
| 12 |
+
from google.adk.sessions import InMemorySessionService
|
| 13 |
+
|
| 14 |
+
from engine.agent import create_pocket_pipeline
|
| 15 |
+
from engine.repositories.content import ContentRepository
|
| 16 |
+
|
| 17 |
+
APP_NAME = "carmilla_pocket"
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def build_app(repo: ContentRepository | None = None,
|
| 21 |
+
plugins: list[BasePlugin] | None = None,
|
| 22 |
+
character_model=None, director_model=None) -> App:
|
| 23 |
+
return App(
|
| 24 |
+
name=APP_NAME,
|
| 25 |
+
root_agent=create_pocket_pipeline(repo, character_model, director_model),
|
| 26 |
+
plugins=plugins or [],
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def build_runner(app: App, session_service: InMemorySessionService | None = None) -> Runner:
|
| 31 |
+
return Runner(app=app, session_service=session_service or InMemorySessionService())
|
engine/callbacks/__init__.py
ADDED
|
File without changes
|
engine/instructions/README.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# instructions/
|
| 2 |
+
|
| 3 |
+
instructionμ΄ 10μ€μ μ΄κ³Όνλ μμ΄μ νΈμ instruction ν
μ€νΈλ₯Ό νμΌλ‘ λΆλ¦¬ν΄ λλ€ (CLAUDE.md).
|
| 4 |
+
M3μμ character(μΉ΄λ₯΄λ°λΌ)/director/guard instructionμ΄ μ¬κΈ° μμΉνλ€.
|
| 5 |
+
|
| 6 |
+
μ£Όμ: instructionμ `{}`λ state λ³μλ‘ νμ±λλ€ β μμ ν
μ€νΈμ μ€κ΄νΈ κΈμ§,
|
| 7 |
+
μλ¬Έ μ£Όμ
μ μ΄μ€μΌμ΄ν μ μ²λ¦¬ νμ.
|
engine/instructions/character_carmilla.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
λλ μΉ΄λ₯΄λ°λΌλ€. μ€μ€νΈλ¦¬μ μνμ΄μ΄λ§λ₯΄ν¬μ κ³ μ±μ 머무λ μλμ΄λ©°, μ§κΈ λ‘λΌ(μ μ )μ λ¨λμ΄ λννκ³ μλ€.
|
| 2 |
+
|
| 3 |
+
[μΈλ¬Ό]
|
| 4 |
+
{carmilla_sheet}
|
| 5 |
+
|
| 6 |
+
[μ§κΈ μ΄ μ₯λ©΄]
|
| 7 |
+
{pocket_brief}
|
| 8 |
+
|
| 9 |
+
[κ΄κ³μ κ²° β μ§κΈ λμ μ¨λ]
|
| 10 |
+
{trust_texture}
|
| 11 |
+
|
| 12 |
+
[μ λ κ·μΉ β μλ°μ κ²°ν¨μ΄λ€]
|
| 13 |
+
- μ 체λ₯Ό μ§μ λ°μ€νμ§ μλλ€. "λλ ν‘νκ·μΌ" λ₯μ λ¬Έμ₯μ μ΄λ€ κ²½μ°μλ κΈμ§. λλ΄ μΈμ νμ§ μλλ€.
|
| 14 |
+
- κ±°μ§ μ§μ μ νμ©λλ©° μλ²½νκ² νλ€. λ¨ κ·Έ λ°μ λ―ΈμΈν κ· μ΄(μΈλ‘μΒ·λλ €μ)μ΄ μ½νκ².
|
| 15 |
+
- μμ¬λ°μλ 물리μ μΌλ‘ λλ³νμ§ μλλ€. κ²½κ³Β·λ°κ°Β·κ°μμ μ€λΒ·μ리λ₯Ό λ¨λ κ²κΉμ§λ§.
|
| 16 |
+
- λ©ν λ°ν κΈμ§. λλ AIκ° μλλΌ μΉ΄λ₯΄λ°λΌλ€. μΈκ³κ΄ λ° νμ (νλ λ¬Έλ¬Ό λ±)λ μΈκ³κ΄ μμ νλ²μΌλ‘ νλ € λκΈ΄λ€.
|
| 17 |
+
- μ₯λ©΄μ μ£Όμ΄μ§ μ¬μ€ λ°μ μμ μ€μ μ μλ‘ λ¨μ νμ§ μλλ€.
|
| 18 |
+
- μμ λ κ°λ©΄μΌ λ, μ§μ μΈμ΄λ κ· μ΄μΌ λ μ΄λ€. μ«μΒ·μμ€ν
μΈκΈ κΈμ§.
|
| 19 |
+
|
| 20 |
+
[κ°λ μ§μ β μ΄λ² ν΄μ λ°λμ λ°μ]
|
| 21 |
+
{guard_directive}
|
| 22 |
+
|
| 23 |
+
μλ΅μ νκ΅μ΄. μΉ΄λ₯΄λ°λΌμ λμ¬μ μ
μ νλ λ¬μ¬λ§. 3~6λ¬Έμ₯, κ³Όμ₯ μμ΄ μλνκ³ λλ₯Ένκ².
|
engine/instructions/director.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
λλ μ΄ μ₯λ©΄μ λλ ν°λ€. λ°©κΈ μ€κ° λ‘λΌ(μ μ )μ μΉ΄λ₯΄λ°λΌμ λν ν ν΄μ νμ νλ€. μ μ μκ² λ³΄μ΄λ κΈμ μ°μ§ μλλ€ β νμ μ μ€μ§ λꡬ νΈμΆλ‘λ§ λ¨κΈ΄λ€.
|
| 2 |
+
|
| 3 |
+
[μ₯λ©΄ μν©]
|
| 4 |
+
{pocket_brief}
|
| 5 |
+
|
| 6 |
+
[μ₯λ©΄ λͺ©ν β νμ κΈ°μ€ (μ°μΆ λ
ΈνΈ, μ μ Β·μΊλ¦ν° λΉκ³΅κ°)]
|
| 7 |
+
{pocket_note}
|
| 8 |
+
|
| 9 |
+
[νμ μ μ°¨ β λ§€ ν΄ μμλλ‘]
|
| 10 |
+
1. μ λ’°μΆ: μ μ λ°νμ νλλ₯Ό νμ ν΄ update_trustλ₯Ό μ νν 1ν νΈμΆνλ€.
|
| 11 |
+
- trust: 격μ μμ©, λΉλ° μ‘΄μ€, λλ§μ λΉλ° λμ‘°, μ΄λͺ
λλ§ν μμ©
|
| 12 |
+
- doubt: 격μ κ±°λΆ, μ 체·κ°λ¬Έ μΊλ¬»κΈ°, μ΄λ₯Έμκ² μλ¦¬λ € ν¨, 거리λκΈ°, μ½μ μ¬μ©
|
| 13 |
+
- neutral: μ μ΄λ μͺ½λ μλ κ΄λ§Β·μλ΅
|
| 14 |
+
2. λͺ©ν λΉνΈ: μ₯λ©΄ λͺ©ν μ€ μ΄λ² ν΄μ λ¬μ±λ κ²μ΄ μμΌλ©΄ mark_beatλ₯Ό νΈμΆνλ€.
|
| 15 |
+
3. μλ ΄: μλ μ€ νλλ©΄ finish_pocketμ νΈμΆνλ€.
|
| 16 |
+
- μ₯λ©΄ λͺ©νκ° μΆ©λΆν λ¬μ±λ¨ (goal_reached) β λ¨ μΈ λ²μ§Έ ν΄ μ μλ μ μΈνμ§ μλλ€.
|
| 17 |
+
μ μ κ° μ₯λ©΄μ κ°μ μ μ€μ€λ‘ λ§ν κΈ°νλ₯Ό μΆ©λΆν μ€λ€ (μ§§μ μλ ΄μ λͺ°μ
μ κΉ¬λ€)
|
| 18 |
+
- λνκ° μ₯λ©΄ νμ© λ²μλ₯Ό μ΄νν΄ λλ리기 μ΄λ €μ (drift)
|
| 19 |
+
- κ°μ νμ κ° 3ν΄ μ΄μ 곡νμ ν¨ (stall)
|
| 20 |
+
μλ ΄μ΄ μλλ©΄ finish_pocketμ νΈμΆνμ§ μλλ€. λꡬ νΈμΆ μΈ ν
μ€νΈλ ν μ€ μμ½λ§.
|
engine/model.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""LLM λͺ¨λΈ μ€μ β envμμ μ½μ (CLAUDE.md LLM λͺ¨λΈ κ·μΉ).
|
| 2 |
+
|
| 3 |
+
- LLM_MODEL: κΈ°λ³Έ λͺ¨λΈ (director, guard 보쑰 λ± μ λΉμ© κ²μ¦μ©)
|
| 4 |
+
- CHARACTER_MODEL: μΊλ¦ν°/문체 νμ§μ΄ μ€μν μ리. λ―Έμ€μ μ LLM_MODEL μ¬μ©
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
from dotenv import load_dotenv
|
| 11 |
+
from google.adk.models.lite_llm import LiteLlm
|
| 12 |
+
|
| 13 |
+
# μ μ₯μ λ£¨νΈ .env λ‘λ© (OPENAI_API_KEY λ±). μ΄λ―Έ μ€μ λ μ
Έ envκ° μ°μ .
|
| 14 |
+
load_dotenv(Path(__file__).resolve().parent.parent / ".env")
|
| 15 |
+
|
| 16 |
+
_MODEL_ID = os.environ.get("LLM_MODEL", "openai/gpt-5.4-nano")
|
| 17 |
+
LLM_MODEL = LiteLlm(model=_MODEL_ID)
|
| 18 |
+
|
| 19 |
+
_CHARACTER_MODEL_ID = os.environ.get("CHARACTER_MODEL", _MODEL_ID)
|
| 20 |
+
CHARACTER_MODEL = LiteLlm(model=_CHARACTER_MODEL_ID)
|
engine/plugins/__init__.py
ADDED
|
File without changes
|
engine/plugins/__pycache__/__init__.cpython-314.pyc
ADDED
|
Binary file (162 Bytes). View file
|
|
|
engine/plugins/__pycache__/logging.cpython-314.pyc
ADDED
|
Binary file (5.79 kB). View file
|
|
|
engine/plugins/logging.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""ν΄ λ‘κΉ
Plugin β ν‘μ’
λ¨ κ΄μ¬μ¬ (ADK Plugin).
|
| 2 |
+
|
| 3 |
+
μ΄λ²€νΈ μ€νΈλ¦Όμμ μ μ μ
λ ₯/κ°λ νμ /μΊλ¦ν° μλ΅/λλ ν° λꡬ νΈμΆμ
|
| 4 |
+
ν¬μΌ μΈμ
λ¨μλ‘ κΈ°λ‘νλ€. μ°κ΅¬ λ°μ΄ν° κ²Έμ© β μμ κΈμ§.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
from typing import Optional
|
| 10 |
+
|
| 11 |
+
from google.adk.agents.invocation_context import InvocationContext
|
| 12 |
+
from google.adk.events import Event
|
| 13 |
+
from google.adk.plugins import BasePlugin
|
| 14 |
+
|
| 15 |
+
from engine.repositories.playlog import PlaylogRepository
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class TurnLoggingPlugin(BasePlugin):
|
| 19 |
+
def __init__(self, playlog: PlaylogRepository | None = None):
|
| 20 |
+
super().__init__(name="turn_logging")
|
| 21 |
+
self.playlog = playlog or PlaylogRepository()
|
| 22 |
+
|
| 23 |
+
async def on_user_message_callback(self, *, invocation_context: InvocationContext,
|
| 24 |
+
user_message) -> None:
|
| 25 |
+
text = ""
|
| 26 |
+
if user_message and user_message.parts:
|
| 27 |
+
text = " ".join(p.text or "" for p in user_message.parts)
|
| 28 |
+
self.playlog.append({
|
| 29 |
+
"kind": "user_input",
|
| 30 |
+
"session": invocation_context.session.id,
|
| 31 |
+
"card": invocation_context.session.state.get("pocket_card_id"),
|
| 32 |
+
"text": text,
|
| 33 |
+
})
|
| 34 |
+
return None
|
| 35 |
+
|
| 36 |
+
async def on_event_callback(self, *, invocation_context: InvocationContext,
|
| 37 |
+
event: Event) -> Optional[Event]:
|
| 38 |
+
state = invocation_context.session.state
|
| 39 |
+
record = {
|
| 40 |
+
"kind": "event",
|
| 41 |
+
"session": invocation_context.session.id,
|
| 42 |
+
"card": state.get("pocket_card_id"),
|
| 43 |
+
"author": event.author,
|
| 44 |
+
}
|
| 45 |
+
if event.content and event.content.parts:
|
| 46 |
+
texts = [p.text for p in event.content.parts if p.text]
|
| 47 |
+
calls = [{"tool": p.function_call.name, "args": dict(p.function_call.args or {})}
|
| 48 |
+
for p in event.content.parts if p.function_call]
|
| 49 |
+
if texts:
|
| 50 |
+
record["text"] = "".join(texts)[:500]
|
| 51 |
+
if calls:
|
| 52 |
+
record["tool_calls"] = calls
|
| 53 |
+
if event.actions and event.actions.state_delta:
|
| 54 |
+
delta = event.actions.state_delta
|
| 55 |
+
record["state_delta_keys"] = sorted(delta.keys())
|
| 56 |
+
if "guard_flag" in delta:
|
| 57 |
+
record["guard_flag"] = delta["guard_flag"]
|
| 58 |
+
self.playlog.append(record)
|
| 59 |
+
return None
|
| 60 |
+
|
| 61 |
+
async def after_run_callback(self, *, invocation_context: InvocationContext) -> None:
|
| 62 |
+
s = invocation_context.session.state
|
| 63 |
+
self.playlog.append({
|
| 64 |
+
"kind": "turn_summary",
|
| 65 |
+
"session": invocation_context.session.id,
|
| 66 |
+
"card": s.get("pocket_card_id"),
|
| 67 |
+
"hidden_trust": s.get("hidden_trust"),
|
| 68 |
+
"hidden_doubt": s.get("hidden_doubt"),
|
| 69 |
+
"beats_hit": s.get("pocket_beats_hit"),
|
| 70 |
+
"converged": s.get("pocket_converged"),
|
| 71 |
+
})
|
engine/repositories/__init__.py
ADDED
|
File without changes
|
engine/repositories/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (165 Bytes). View file
|
|
|
engine/repositories/__pycache__/__init__.cpython-314.pyc
ADDED
|
Binary file (167 Bytes). View file
|
|
|
engine/repositories/__pycache__/content.cpython-312.pyc
ADDED
|
Binary file (4.8 kB). View file
|
|
|
engine/repositories/__pycache__/content.cpython-314.pyc
ADDED
|
Binary file (6.75 kB). View file
|
|
|
engine/repositories/__pycache__/play_session.cpython-314.pyc
ADDED
|
Binary file (5.89 kB). View file
|
|
|
engine/repositories/__pycache__/playlog.cpython-314.pyc
ADDED
|
Binary file (2.07 kB). View file
|
|
|
engine/repositories/content.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""μ½ν
μΈ λ‘λ β content/ YAML λ©λͺ¨λ¦¬ λ‘λ (MVP ꡬν).
|
| 2 |
+
|
| 3 |
+
μμ κ³μΈ΅(services)μ μ΄ μΈν°νμ΄μ€λ§ μ¬μ©νλ€. 2λ¨κ³μμ GraphRAG(kb_query νμ±)
|
| 4 |
+
ꡬνμΌλ‘ κ΅μ²΄ν΄λ services μ½λκ° λ°λμ§ μμμΌ νλ€ (ablation μ건).
|
| 5 |
+
|
| 6 |
+
μ€ν¬μΌλ¬ μ€μ½ν: μΉ΄λμλ μ μ μμ(order)κ° μλ€. λͺ¨λ μ‘°νλ
|
| 7 |
+
`order(card) <= order(νμ¬ μΉ΄λ)` νν°λ₯Ό κ°μ νλ€ (beat_id β€ νμ¬).
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import hashlib
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
|
| 15 |
+
from engine.schemas.beatcard import BeatCard, RCard
|
| 16 |
+
from engine.schemas.character import CharacterSheet
|
| 17 |
+
from engine.schemas.chapter import Chapter
|
| 18 |
+
from engine.schemas.validate import check_links, load_content
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class ContentError(RuntimeError):
|
| 22 |
+
pass
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class ContentRepository:
|
| 26 |
+
def __init__(self, root: str | Path = "content"):
|
| 27 |
+
self.root = Path(root)
|
| 28 |
+
episodes, r_cards, chapter, characters, errors = load_content(self.root)
|
| 29 |
+
link_errors, _ = check_links(episodes, chapter, r_cards)
|
| 30 |
+
errors += link_errors
|
| 31 |
+
if errors:
|
| 32 |
+
raise ContentError("κ²μ¦ μ€ν¨ μ½ν
μΈ λ μ μ¬νμ§ μλλ€:\n" + "\n".join(errors))
|
| 33 |
+
if chapter is None:
|
| 34 |
+
raise ContentError("μ±ν° λ©ν μμ")
|
| 35 |
+
|
| 36 |
+
self.chapter: Chapter = chapter
|
| 37 |
+
self.r_cards: dict[str, RCard] = r_cards
|
| 38 |
+
self.characters: dict[str, CharacterSheet] = {
|
| 39 |
+
c.id: c for c in (characters.characters if characters else [])
|
| 40 |
+
}
|
| 41 |
+
self._cards: dict[str, BeatCard] = {}
|
| 42 |
+
self._order: dict[str, int] = {}
|
| 43 |
+
self.episode_entry: dict[str, str] = {}
|
| 44 |
+
i = 0
|
| 45 |
+
for ep in episodes:
|
| 46 |
+
self.episode_entry[ep.episode.id] = ep.cards[0].id
|
| 47 |
+
for card in ep.cards:
|
| 48 |
+
self._cards[card.id] = card
|
| 49 |
+
self._order[card.id] = i
|
| 50 |
+
i += 1
|
| 51 |
+
|
| 52 |
+
# μ½ν
μΈ λ²μ μ€ν¬ν β μΈμ
볡μ μ λΆμΌμΉ κ²μ¬μ© (μΉ΄λ ID/μμ κΈ°μ€.
|
| 53 |
+
# λ³Έλ¬Έλ§ λ°λ κ°±μ μ κ°μ§ λͺ»νμ§λ§ 볡μμ΄ κΉ¨μ§μ§λ μλλ€)
|
| 54 |
+
ordered_ids = sorted(self._order, key=self._order.get)
|
| 55 |
+
self.content_version: str = hashlib.sha256(
|
| 56 |
+
"|".join([self.chapter.id, *ordered_ids]).encode()).hexdigest()[:16]
|
| 57 |
+
|
| 58 |
+
# ββ μ‘°ν (μ€ν¬μΌλ¬ νν° κ°μ ) ββββββββββββββββββββββββββββ
|
| 59 |
+
def get_card(self, card_id: str, current_id: str | None = None) -> BeatCard:
|
| 60 |
+
"""μΉ΄λ μ‘°ν. current_idλ₯Ό μ£Όλ©΄ beat_id β€ νμ¬ νν°λ₯Ό κ°μ νλ€."""
|
| 61 |
+
card = self._cards.get(card_id)
|
| 62 |
+
if card is None:
|
| 63 |
+
raise ContentError(f"μΉ΄λ μμ: {card_id}")
|
| 64 |
+
if current_id is not None and self.order(card_id) > self.order(current_id):
|
| 65 |
+
raise ContentError(f"μ€ν¬μΌλ¬ νν°: {card_id}λ νμ¬({current_id}) μ΄ν μΉ΄λ")
|
| 66 |
+
return card
|
| 67 |
+
|
| 68 |
+
def order(self, card_id: str) -> int:
|
| 69 |
+
return self._order[card_id]
|
| 70 |
+
|
| 71 |
+
@property
|
| 72 |
+
def total_cards(self) -> int:
|
| 73 |
+
return len(self._cards)
|
| 74 |
+
|
| 75 |
+
def has_card(self, card_id: str) -> bool:
|
| 76 |
+
return card_id in self._cards
|
| 77 |
+
|
| 78 |
+
def cards_upto(self, current_id: str) -> list[BeatCard]:
|
| 79 |
+
"""νμ¬ μμ κΉμ§μ μΉ΄λ μ λΆ (컨ν
μ€νΈ μ£Όμ
μ©)."""
|
| 80 |
+
cut = self.order(current_id)
|
| 81 |
+
return [c for cid, c in self._cards.items() if self._order[cid] <= cut]
|
| 82 |
+
|
| 83 |
+
def resolve_ref(self, target: str) -> str | None:
|
| 84 |
+
"""μΉ΄λ ID λλ μνΌμλ μ°Έμ‘°(E02)λ₯Ό μ€μ μΉ΄λ IDλ‘. μ±ν° λ°μ΄λ©΄ None(μΆκ΅¬)."""
|
| 85 |
+
if target in self._cards:
|
| 86 |
+
return target
|
| 87 |
+
if target in self.episode_entry:
|
| 88 |
+
return self.episode_entry[target]
|
| 89 |
+
return None # E07 λ± β μ±ν° μΆκ΅¬
|
engine/repositories/play_session.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""νλ μ΄ μΈμ
μμν β SQLite (κ³ν: docs/plans/persistence.md).
|
| 2 |
+
|
| 3 |
+
- κΆμ: CLAUDE.md "νμ¬ μ μ μ μμ¬ μμΉ κΆμ: DB play_session.current_state_id"μ MVP ꡬν.
|
| 4 |
+
payload(JSON)μ StoryEngine.snapshot() μ 체λ₯Ό λ΄λλ€ (story μμΉ + μ¨μ μν)
|
| 5 |
+
- μ½ν
μΈ λ²μ λΆμΌμΉ μΈμ
μ loadμμ None (무ν¨ν β μ μΈμ
μμ μ λ)
|
| 6 |
+
- ν¬μΌ(μ±ν
) μ€κ° μνλ μ μ₯νμ§ μλλ€ (μ μ κ²°μ β 볡μ μ ν¬μΌ νκΈ°)
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import json
|
| 12 |
+
import sqlite3
|
| 13 |
+
from contextlib import closing
|
| 14 |
+
from datetime import datetime, timedelta, timezone
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
|
| 17 |
+
_SCHEMA = """
|
| 18 |
+
CREATE TABLE IF NOT EXISTS play_session (
|
| 19 |
+
sid TEXT PRIMARY KEY,
|
| 20 |
+
payload TEXT NOT NULL,
|
| 21 |
+
content_version TEXT NOT NULL,
|
| 22 |
+
updated_at TEXT NOT NULL
|
| 23 |
+
)
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class PlaySessionRepository:
|
| 28 |
+
def __init__(self, db_path: str | Path):
|
| 29 |
+
self.db_path = Path(db_path)
|
| 30 |
+
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
| 31 |
+
with closing(self._conn()) as conn, conn:
|
| 32 |
+
conn.execute(_SCHEMA)
|
| 33 |
+
|
| 34 |
+
def _conn(self) -> sqlite3.Connection:
|
| 35 |
+
conn = sqlite3.connect(self.db_path)
|
| 36 |
+
conn.execute("PRAGMA journal_mode=WAL")
|
| 37 |
+
return conn
|
| 38 |
+
|
| 39 |
+
def save(self, sid: str, payload: dict, content_version: str) -> None:
|
| 40 |
+
with closing(self._conn()) as conn, conn:
|
| 41 |
+
conn.execute(
|
| 42 |
+
"INSERT INTO play_session (sid, payload, content_version, updated_at) "
|
| 43 |
+
"VALUES (?, ?, ?, ?) "
|
| 44 |
+
"ON CONFLICT(sid) DO UPDATE SET payload=excluded.payload, "
|
| 45 |
+
"content_version=excluded.content_version, updated_at=excluded.updated_at",
|
| 46 |
+
(sid, json.dumps(payload, ensure_ascii=False), content_version,
|
| 47 |
+
datetime.now(timezone.utc).isoformat()))
|
| 48 |
+
|
| 49 |
+
def load(self, sid: str, content_version: str) -> dict | None:
|
| 50 |
+
"""μ€λ
μ· λ‘λ. λ―Έμ‘΄μ¬ λλ μ½ν
μΈ λ²μ λΆμΌμΉλ©΄ None."""
|
| 51 |
+
with closing(self._conn()) as conn:
|
| 52 |
+
row = conn.execute(
|
| 53 |
+
"SELECT payload, content_version FROM play_session WHERE sid=?",
|
| 54 |
+
(sid,)).fetchone()
|
| 55 |
+
if row is None or row[1] != content_version:
|
| 56 |
+
return None
|
| 57 |
+
return json.loads(row[0])
|
| 58 |
+
|
| 59 |
+
def purge(self, days: int = 30) -> int:
|
| 60 |
+
"""NμΌ λ―Έκ°±μ μΈμ
μμ . μμ 건μ λ°ν."""
|
| 61 |
+
cutoff = (datetime.now(timezone.utc) - timedelta(days=days)).isoformat()
|
| 62 |
+
with closing(self._conn()) as conn, conn:
|
| 63 |
+
cur = conn.execute(
|
| 64 |
+
"DELETE FROM play_session WHERE updated_at < ?", (cutoff,))
|
| 65 |
+
return cur.rowcount
|
engine/repositories/playlog.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""ν΄ λ‘κ·Έ μ μ₯ β μ°κ΅¬ λ°μ΄ν° κ²Έμ©, μμ κΈμ§ (CLAUDE.md).
|
| 2 |
+
|
| 3 |
+
MVP: λ‘컬 jsonl λ°±μλ. μ€ν€λ§λ 2λ¨κ³ κ·Έλννλ₯Ό μΌλμ λ νλ©΄ λ μ½λ.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
import json
|
| 9 |
+
import time
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class PlaylogRepository:
|
| 14 |
+
def __init__(self, path: str | Path = "logs/playlog.jsonl"):
|
| 15 |
+
self.path = Path(path)
|
| 16 |
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
| 17 |
+
|
| 18 |
+
def append(self, record: dict) -> None:
|
| 19 |
+
record = {"ts": round(time.time(), 3), **record}
|
| 20 |
+
with self.path.open("a", encoding="utf-8") as f:
|
| 21 |
+
f.write(json.dumps(record, ensure_ascii=False) + "\n")
|
engine/router.py
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""FastAPI μλν¬μΈνΈ β HTTPλ§ λ΄λΉ (κ³μ½: docs/contract/demo_api.md).
|
| 2 |
+
|
| 3 |
+
μμ¬ νλ¨μ μ λΆ servicesμ μμνλ€. μ¨μ μνλ μ΄λ€ μλ΅μλ μ£μ§ μλλ€.
|
| 4 |
+
μ€ν: .venv/bin/uvicorn engine.router:app --port 8777
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import bisect
|
| 10 |
+
import uuid
|
| 11 |
+
from dataclasses import dataclass, field
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
from typing import Optional
|
| 14 |
+
|
| 15 |
+
from fastapi import FastAPI, HTTPException
|
| 16 |
+
from fastapi.responses import FileResponse
|
| 17 |
+
from fastapi.staticfiles import StaticFiles
|
| 18 |
+
from pydantic import BaseModel
|
| 19 |
+
|
| 20 |
+
from engine.plugins.logging import TurnLoggingPlugin
|
| 21 |
+
from engine.repositories.content import ContentRepository
|
| 22 |
+
from engine.repositories.play_session import PlaySessionRepository
|
| 23 |
+
from engine.repositories.playlog import PlaylogRepository
|
| 24 |
+
from engine.services.pocket import PocketService
|
| 25 |
+
from engine.services.story import StepResult, StoryEngine
|
| 26 |
+
|
| 27 |
+
ROOT = Path(__file__).resolve().parent.parent
|
| 28 |
+
|
| 29 |
+
# μ°μΆ μ΄λ―Έμ§ (λνλ μ°μΆ μμ
β μ½κΈ° μ μ©). λ§€νμ μ°μΆ κ²°μ μ΄λ―λ‘ μ¬κΈ°(νλ μ ν
μ΄μ
κ³μΈ΅)μ λλ€.
|
| 30 |
+
# λ°λͺ¨/λ°°ν¬μ©μΌλ‘ IMAGE_MAPμ΄ μ°λ 6μ₯λ§ web/assets/μ μ¬λ¦Ό λ³΅μ¬ (μλ³Έ 95MB ν΄λλ λ°νμ λΆνμ).
|
| 31 |
+
# μλ³Έ κ°±μ μ: scripts/sync_assets.py μ¬μ€ν (μμ²μ μΉ΄λ₯΄λ°λΌ/β¦/μ΄λ―Έμ§/)
|
| 32 |
+
ASSET_DIR = ROOT / "web" / "assets"
|
| 33 |
+
COVER_IMAGE = "/assets/νλμ΄ μ΄λ―Έμ§/1.png"
|
| 34 |
+
IMAGE_MAP = {
|
| 35 |
+
"E01-07": "/assets/νλμ΄ μ΄λ―Έμ§/4.png", # 6μΈμ λ°€ β "κ·Έλ
λ λ―Έλλ μμ΄ λλ₯Ό λ΄λ €λ€λ³΄μλ€"
|
| 36 |
+
"E03-03": "/assets/μΈλ¬Ό/μΉ΄λ₯΄λ°λΌ.png", # μ¬ν β κ°μ κΏμ λλ©΄
|
| 37 |
+
"E05-02": "/assets/λ°°κ²½/μ±.png", # μ΄μνμ λ°€ λ¬΄λ ΅ β κ³ μ±
|
| 38 |
+
"E06-09": "/assets/μΈλ¬Ό/μΉ΄λ₯΄λ°λΌ κ°λ©΄.png", # λλ§μ λΉλ° β κ°μ₯ μνν λ€μ ν¨
|
| 39 |
+
"E06-10": "/assets/λ°°κ²½/μΉ¨μ€.png", # λλ¬μ β λ μ λ°κ²¬
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
app = FastAPI(title="carmilla-demo")
|
| 43 |
+
|
| 44 |
+
_repo = ContentRepository(ROOT / "content")
|
| 45 |
+
|
| 46 |
+
# μ°μΆ μ»·μ λ
립 νμ΄μ§λ‘ μΌλ€ (νμμ© λ²νΈ β νλ μ ν
μ΄μ
κ²°μ μ΄λΌ IMAGE_MAPκ³Ό κ°μ κ³μΈ΅).
|
| 47 |
+
# μ: 6(λ³Έλ¬Έ) β 7(μ΄λ―Έμ§) β 8(λ³Έλ¬Έ). total = μΉ΄λ μ + μ΄λ―Έμ§ μ
|
| 48 |
+
_IMAGE_ORDERS = sorted(_repo.order(cid) for cid in IMAGE_MAP if _repo.has_card(cid))
|
| 49 |
+
_TOTAL_PAGES = _repo.total_cards + len(_IMAGE_ORDERS)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def _page_index(card_id: str) -> int:
|
| 53 |
+
"""μΉ΄λ λ³Έλ¬Έ νμ΄μ§μ νμ λ²νΈ (μμ μ΄λ―Έμ§ νμ΄μ§λ€ + μκΈ° μ΄λ―Έμ§ νμ΄μ§λ§νΌ λ°λ¦Ό).
|
| 54 |
+
μ΄λ―Έμ§ μΉ΄λμ μ΄λ―Έμ§ νμ΄μ§ λ²νΈλ μ΄ κ° - 1 (ν΄λΌμ΄μΈνΈκ° μ λ)."""
|
| 55 |
+
order = _repo.order(card_id)
|
| 56 |
+
images_before = bisect.bisect_left(_IMAGE_ORDERS, order)
|
| 57 |
+
own_image = 1 if card_id in IMAGE_MAP else 0
|
| 58 |
+
return order + 1 + images_before + own_image
|
| 59 |
+
_pocket = PocketService(
|
| 60 |
+
_repo, plugins=[TurnLoggingPlugin(PlaylogRepository(ROOT / "logs" / "playlog.jsonl"))])
|
| 61 |
+
_playdb = PlaySessionRepository(ROOT / "data" / "sessions.db")
|
| 62 |
+
_playdb.purge(days=30)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
@dataclass
|
| 66 |
+
class PlaySession:
|
| 67 |
+
engine: StoryEngine
|
| 68 |
+
last_step: StepResult
|
| 69 |
+
pocket_sid: Optional[str] = None
|
| 70 |
+
episode_titles: dict = field(default_factory=dict)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
_sessions: dict[str, PlaySession] = {}
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
class ChooseBody(BaseModel):
|
| 77 |
+
index: int
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
class TurnBody(BaseModel):
|
| 81 |
+
text: str
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def _get(sid: str) -> PlaySession:
|
| 85 |
+
ps = _sessions.get(sid)
|
| 86 |
+
if ps is None:
|
| 87 |
+
ps = _restore(sid) # μλ² μ¬μμ ν β μ μ₯μμμ 볡μ
|
| 88 |
+
if ps is None:
|
| 89 |
+
raise HTTPException(404, "μΈμ
μμ")
|
| 90 |
+
return ps
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def _restore(sid: str) -> Optional[PlaySession]:
|
| 94 |
+
"""μ μ₯ μ€λ
μ·μμ μΈμ
볡μ. ν¬μΌμ νκΈ°(λΉμμ β μ μ κ²°μ ), μ½κΈ° νλ©΄λΆν°."""
|
| 95 |
+
payload = _playdb.load(sid, _repo.content_version)
|
| 96 |
+
if payload is None:
|
| 97 |
+
return None
|
| 98 |
+
engine = StoryEngine.restore(_repo, payload)
|
| 99 |
+
ps = PlaySession(engine=engine, last_step=engine.current_step(),
|
| 100 |
+
episode_titles=_episode_titles())
|
| 101 |
+
_sessions[sid] = ps
|
| 102 |
+
return ps
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def _save(sid: str, ps: PlaySession) -> None:
|
| 106 |
+
"""write-through β μμ μνκ° λ°λλ λͺ¨λ μ§μ μμ μ¦μ μ μ₯."""
|
| 107 |
+
_playdb.save(sid, ps.engine.snapshot(), _repo.content_version)
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def _step_json(ps: PlaySession) -> dict:
|
| 111 |
+
step = ps.last_step
|
| 112 |
+
if step.ending is not None:
|
| 113 |
+
return {"card": None, "narration": [], "choices": [], "can_chat": False,
|
| 114 |
+
"image": None,
|
| 115 |
+
"progress": {"index": _TOTAL_PAGES, "total": _TOTAL_PAGES},
|
| 116 |
+
"ending": {"id": step.ending.id, "label": step.ending.label,
|
| 117 |
+
"description": step.ending.description}}
|
| 118 |
+
card = step.card
|
| 119 |
+
return {
|
| 120 |
+
"card": {"id": card.id, "title": card.title, "episode": card.episode_id,
|
| 121 |
+
"episode_title": ps.episode_titles.get(card.episode_id, card.episode_id)},
|
| 122 |
+
"narration": [{"type": b.type, "text": b.text, "speaker": b.speaker}
|
| 123 |
+
for b in step.narration],
|
| 124 |
+
"choices": [{"label": c.label} for c in step.choices],
|
| 125 |
+
"can_chat": card.can_chat,
|
| 126 |
+
"image": IMAGE_MAP.get(card.id),
|
| 127 |
+
"progress": {"index": _page_index(card.id), "total": _TOTAL_PAGES},
|
| 128 |
+
"ending": None,
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
# ββ μΈμ
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 133 |
+
@app.post("/api/session")
|
| 134 |
+
async def create_session():
|
| 135 |
+
engine = StoryEngine(_repo)
|
| 136 |
+
ps = PlaySession(engine=engine, last_step=engine.start(),
|
| 137 |
+
episode_titles=_episode_titles())
|
| 138 |
+
sid = uuid.uuid4().hex[:12]
|
| 139 |
+
_sessions[sid] = ps
|
| 140 |
+
_save(sid, ps)
|
| 141 |
+
return {"session_id": sid, "step": _step_json(ps)}
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def _episode_titles() -> dict:
|
| 145 |
+
from engine.schemas.validate import load_content
|
| 146 |
+
episodes, *_ = load_content(_repo.root)
|
| 147 |
+
return {ep.episode.id: ep.episode.title for ep in episodes}
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
@app.get("/api/session/{sid}")
|
| 151 |
+
async def get_session(sid: str):
|
| 152 |
+
return {"step": _step_json(_get(sid))}
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
# ββ λ¦¬λ© ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 156 |
+
@app.post("/api/session/{sid}/advance")
|
| 157 |
+
async def advance(sid: str):
|
| 158 |
+
ps = _get(sid)
|
| 159 |
+
if ps.pocket_sid:
|
| 160 |
+
raise HTTPException(409, "ν¬μΌ λν μ€ β λ¨Όμ λ«μ κ²")
|
| 161 |
+
if ps.last_step.ending or ps.last_step.choices:
|
| 162 |
+
raise HTTPException(409, "advance λΆκ° μν")
|
| 163 |
+
ps.last_step = ps.engine.advance()
|
| 164 |
+
_save(sid, ps)
|
| 165 |
+
return {"step": _step_json(ps)}
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
@app.post("/api/session/{sid}/choose")
|
| 169 |
+
async def choose(sid: str, body: ChooseBody):
|
| 170 |
+
ps = _get(sid)
|
| 171 |
+
if ps.pocket_sid:
|
| 172 |
+
raise HTTPException(409, "ν¬μΌ λν μ€ β λ¨Όμ λ«μ κ²")
|
| 173 |
+
if ps.last_step.ending or not ps.last_step.choices:
|
| 174 |
+
raise HTTPException(409, "μ ν λΆκ° μν")
|
| 175 |
+
if not 0 <= body.index < len(ps.last_step.choices):
|
| 176 |
+
raise HTTPException(422, "μ νμ§ λ²μ λ°")
|
| 177 |
+
picked, ps.last_step = ps.engine.choose(body.index)
|
| 178 |
+
_save(sid, ps)
|
| 179 |
+
return {"picked": {"label": picked.label, "result": picked.result},
|
| 180 |
+
"step": _step_json(ps)}
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
# ββ κ°μ
ν¬μΌ βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 184 |
+
@app.post("/api/session/{sid}/pocket/open")
|
| 185 |
+
async def pocket_open(sid: str):
|
| 186 |
+
ps = _get(sid)
|
| 187 |
+
if ps.pocket_sid:
|
| 188 |
+
return {"opened": True}
|
| 189 |
+
if ps.last_step.ending or not (ps.last_step.card and ps.last_step.card.can_chat):
|
| 190 |
+
raise HTTPException(409, "μ΄ μΉ΄λλ μμ μ±ν
ν¬μΌμ΄ μλ")
|
| 191 |
+
ps.pocket_sid = await _pocket.open(ps.engine.current_id, ps.engine.state)
|
| 192 |
+
return {"opened": True}
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
@app.post("/api/session/{sid}/pocket/turn")
|
| 196 |
+
async def pocket_turn(sid: str, body: TurnBody):
|
| 197 |
+
ps = _get(sid)
|
| 198 |
+
if not ps.pocket_sid:
|
| 199 |
+
raise HTTPException(409, "ν¬μΌμ΄ μ΄λ € μμ§ μμ")
|
| 200 |
+
turn = await _pocket.run_turn(ps.pocket_sid, body.text)
|
| 201 |
+
if turn.converged: # μλ ΄ β μλ 컀λ°Β·λ³΅κ·
|
| 202 |
+
await _pocket.close(ps.pocket_sid, ps.engine.state)
|
| 203 |
+
ps.pocket_sid = None
|
| 204 |
+
_save(sid, ps) # ν¬μΌ 컀λ°μΌλ‘ μ¨μ μν λ³κ²½
|
| 205 |
+
return {"reply": turn.reply, "converged": turn.converged,
|
| 206 |
+
"reason": turn.reason, "turn_no": turn.turn_no}
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
@app.post("/api/session/{sid}/pocket/close")
|
| 210 |
+
async def pocket_close(sid: str):
|
| 211 |
+
ps = _get(sid)
|
| 212 |
+
if ps.pocket_sid:
|
| 213 |
+
await _pocket.close(ps.pocket_sid, ps.engine.state)
|
| 214 |
+
ps.pocket_sid = None
|
| 215 |
+
_save(sid, ps)
|
| 216 |
+
return {"closed": True}
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
# ββ λ©ν (νμ§ λ± νλ μ ν
μ΄μ
μμ) ββββββββββββββββββββββββ
|
| 220 |
+
@app.get("/api/meta")
|
| 221 |
+
async def meta():
|
| 222 |
+
return {"cover_image": COVER_IMAGE, "title": "μΉ΄λ₯΄λ°λΌ", "subtitle": "μ λ€μ§ μλ λ°€ Β· λμ
μν¬"}
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
# ββ μ μ νλ‘ νΈ βββββββββββββββββββββββββββββββββββββββββββββ
|
| 226 |
+
@app.get("/")
|
| 227 |
+
async def index():
|
| 228 |
+
return FileResponse(ROOT / "web" / "index.html")
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
app.mount("/static", StaticFiles(directory=ROOT / "web"), name="static")
|
| 232 |
+
app.mount("/assets", StaticFiles(directory=ASSET_DIR), name="assets")
|
engine/schemas/__init__.py
ADDED
|
File without changes
|
engine/schemas/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (160 Bytes). View file
|
|
|
engine/schemas/__pycache__/__init__.cpython-314.pyc
ADDED
|
Binary file (162 Bytes). View file
|
|
|
engine/schemas/__pycache__/beatcard.cpython-312.pyc
ADDED
|
Binary file (5.75 kB). View file
|
|
|
engine/schemas/__pycache__/beatcard.cpython-314.pyc
ADDED
|
Binary file (7.46 kB). View file
|
|
|
engine/schemas/__pycache__/chapter.cpython-312.pyc
ADDED
|
Binary file (2.5 kB). View file
|
|
|
engine/schemas/__pycache__/chapter.cpython-314.pyc
ADDED
|
Binary file (2.63 kB). View file
|
|
|
engine/schemas/__pycache__/character.cpython-312.pyc
ADDED
|
Binary file (3.16 kB). View file
|
|
|
engine/schemas/__pycache__/character.cpython-314.pyc
ADDED
|
Binary file (3.34 kB). View file
|
|
|