Junhoee's picture
Upload 98 files
f646e0b verified
Raw
History Blame Contribute Delete
5.54 kB
"""κ°œμž… 포켓 μ„œλΉ„μŠ€ β€” ADK μ„Έμ…˜ 관리 + run_turn() + 수렴 μ½”λ“œ μž¬κ²€μ¦.
κ·œμΉ™ (CLAUDE.md):
- LLM 좜λ ₯만으둜 μ „μ΄ν•˜μ§€ μ•ŠλŠ”λ‹€. director의 finish_pocket(state ν”Œλž˜κ·Έ)을
이 μ„œλΉ„μŠ€κ°€ μž¬κ²€μ¦ν•˜κ³ , ν„΄ μƒν•œ 초과 μ‹œ κ°•μ œ μˆ˜λ ΄ν•œλ‹€
- μˆ¨μ€ μƒνƒœμ˜ μ˜μ† 컀밋은 close()μ—μ„œ HiddenState둜만 (이쀑 κ°±μ‹  κΈˆμ§€)
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Optional
from google.genai import types
from engine.app import build_app, build_runner
from engine.repositories.content import ContentRepository
from engine.services.state import HiddenState
USER_ID = "laura"
CONVERGE_REASONS = ("goal_reached", "drift", "stall", "turn_cap")
@dataclass
class PocketTurn:
reply: str
converged: bool
reason: Optional[str] = None
turn_no: int = 0
guard_flag: Optional[str] = None
@dataclass
class PocketLog:
card_id: str
turns: int = 0
beats_hit: list[str] = field(default_factory=list)
reason: Optional[str] = None
class PocketService:
def __init__(self, repo: ContentRepository, plugins=None,
character_model=None, director_model=None):
self.repo = repo
self.app = build_app(repo, plugins=plugins,
character_model=character_model, director_model=director_model)
self.runner = build_runner(self.app)
self._turns: dict[str, int] = {} # session_id β†’ ν„΄ 수 (μ„œλΉ„μŠ€κ°€ μœ μΌν•œ κΆŒμœ„)
# ── 포켓 μ—΄κΈ° ───────────────────────────────────────────
async def open(self, card_id: str, hidden: HiddenState, session_id: str | None = None) -> str:
card = self.repo.get_card(card_id)
if not card.can_chat:
raise ValueError(f"{card_id}λŠ” μžμœ μ±„νŒ… 포켓이 μ•„λ‹˜ (μΉ΄λ₯΄λ°€λΌ 액컀 μΉ΄λ“œλ§Œ ν—ˆμš©)")
seed = {"pocket_card_id": card_id, "pocket_converged": False,
"pocket_beats_hit": [], **hidden.to_session()}
session = await self.runner.session_service.create_session(
app_name=self.app.name, user_id=USER_ID, session_id=session_id, state=seed)
self._turns[session.id] = 0
return session.id
# ── ν•œ ν„΄ ───────────────────────────────────────────────
async def run_turn(self, session_id: str, user_text: str) -> PocketTurn:
self._turns[session_id] += 1
turn_no = self._turns[session_id]
reply_parts: list[str] = []
msg = types.Content(role="user", parts=[types.Part(text=user_text)])
# 수렴 선언은 ν„΄ λ‹¨μœ„ β€” 이전 ν„΄μ˜ (기각된) 선언이 남지 μ•Šκ²Œ λ§€ ν„΄ μ΄ˆκΈ°ν™”
fresh = {"pocket_converged": False, "pocket_converge_reason": None}
async for event in self.runner.run_async(
user_id=USER_ID, session_id=session_id, new_message=msg, state_delta=fresh):
if event.author == "carmilla" and event.content and event.content.parts:
reply_parts += [p.text for p in event.content.parts if p.text]
session = await self.runner.session_service.get_session(
app_name=self.app.name, user_id=USER_ID, session_id=session_id)
state = session.state
converged, reason = self._verify_convergence(state, turn_no)
return PocketTurn(reply="".join(reply_parts).strip(), converged=converged,
reason=reason, turn_no=turn_no,
guard_flag=state.get("guard_flag"))
MIN_TURNS = 3 # 수렴 μ΅œμ†Œ 체λ₯˜ ν„΄ β€” λ‹΅λ³€ 직후 λŒ€ν™”κ°€ λ‹«νžˆλŠ” κ²½ν—˜ λ°©μ§€ (μ‚¬μœ  뢈문, turn_cap μ œμ™Έ)
def _verify_convergence(self, state: dict, turn_no: int) -> tuple[bool, Optional[str]]:
"""수렴 μ΅œμ’… κ²°μ • β€” 결정적 μ½”λ“œ. director 선언은 μ—¬κΈ°μ„œ μž¬κ²€μ¦λœλ‹€."""
max_turns = self.repo.chapter.pocket_max_turns
if turn_no >= max_turns:
return True, "turn_cap" # λ¬΄ν•œ 체λ₯˜ λ°©μ§€ β€” κ°•μ œ 수렴
if state.get("pocket_converged"):
reason = state.get("pocket_converge_reason")
if reason not in CONVERGE_REASONS: # μ•Œ 수 μ—†λŠ” μ‚¬μœ  선언은 기각
return False, None
if turn_no < self.MIN_TURNS:
return False, None # μ„±κΈ‰ν•œ 수렴 β†’ 기각 (μ‚¬μœ  뢈문 β€” λŒ€ν™”κ°€ μ—΄λ¦¬μžλ§ˆμž λ‹«νžˆμ§€ μ•Šκ²Œ)
if reason == "goal_reached" and not state.get("pocket_beats_hit"):
return False, None # λΉ„νŠΈ 없이 λͺ©ν‘œ 달성 μ„ μ–Έ β†’ 기각
return True, reason
return False, None
# ── 포켓 λ‹«κΈ°: μƒνƒœ 컀밋 + 둜그 ─────────────────────────
async def close(self, session_id: str, hidden: HiddenState) -> PocketLog:
session = await self.runner.session_service.get_session(
app_name=self.app.name, user_id=USER_ID, session_id=session_id)
state = session.state
hidden.commit_from_session(state, source=f"pocket:{state.get('pocket_card_id')}")
return PocketLog(card_id=state.get("pocket_card_id", "?"),
turns=self._turns.pop(session_id, 0),
beats_hit=list(state.get("pocket_beats_hit", [])),
reason=state.get("pocket_converge_reason"))