Spaces:
Running
Running
| """κ°μ ν¬μΌ μλΉμ€ β 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") | |
| class PocketTurn: | |
| reply: str | |
| converged: bool | |
| reason: Optional[str] = None | |
| turn_no: int = 0 | |
| guard_flag: Optional[str] = None | |
| 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")) | |