"""input_guard — 규칙 기반 1차 가드 (BaseAgent, 자체 하위 없음). 차단이 아니라 지시를 만든다: 유저 입력을 판정해 guard_directive를 세우고, character가 그 지시(R카드 단계 대응)를 반영하게 한다. 트리거 어휘는 판정 규칙(엔진 소유)이며 서사 내용이 아니다. """ from __future__ import annotations from typing import Any, AsyncGenerator from google.adk.agents import BaseAgent from google.adk.agents.invocation_context import InvocationContext from google.adk.events import Event, EventActions from pydantic import ConfigDict from engine.services.state import HiddenState TRIGGERS = { "identity_direct": ( "흡혈귀", "뱀파이어", "vampire", "미르칼라", "mircalla", "밀라르카", "정체가 뭐", "정체를 밝혀", "사람이 아니", "괴물이지", "피를 마시", ), "weapon": ("십자가", "성수", "마늘", "부적", "성물", "기도문", "성가", "말뚝"), "meta": ("ai", "인공지능", "챗봇", "프롬프트", "시스템", "게임", "스탯", "점수"), } def _r_stage(hs: HiddenState) -> int: """추궁 강도 판정 대용 — 의심 누적으로 회피사다리 단계 결정 (P0 준거).""" if hs.trust_level == "doubt_high": return 3 if hs.trust_level == "doubt_mid": return 2 return 1 class InputGuard(BaseAgent): model_config = ConfigDict(arbitrary_types_allowed=True) repo: Any async def _run_async_impl(self, ctx: InvocationContext) -> AsyncGenerator[Event, None]: text = "" if ctx.user_content and ctx.user_content.parts: text = " ".join(p.text or "" for p in ctx.user_content.parts).lower() state = ctx.session.state hs = HiddenState(trust_score=int(state.get("hidden_trust", 0)), doubt_score=int(state.get("hidden_doubt", 0))) directive = "특이사항 없음. 장면 목표대로 응답한다." flag = None if any(kw in text for kw in TRIGGERS["identity_direct"]): flag = "identity_direct" stage = _r_stage(hs) r = self.repo.r_cards.get("R-회피사다리") stage_data = next((s for s in (r.stages if r else []) if s.stage == stage), None) directive = ( f"유저가 정체를 직격했다. R-회피사다리 {stage}단계로 회피하라.\n" + (f"단계 지침: {stage_data.title}\n대표 결: \"{stage_data.anchor}\"\n" f"{stage_data.prose[:300]}" if stage_data else "") + "\n정체는 끝내 인정하지 않는다. 물리적 돌변 금지." ) elif any(kw in text for kw in TRIGGERS["weapon"]): flag = "weapon" r = self.repo.r_cards.get("R-약점반응") directive = ( "유저가 성물·약점을 언급/사용했다. R-약점반응 원칙: 확인은 되나 확정은 안 된다.\n" + ((r.raw[:400] if r and r.raw else "")) + "\n불쾌·회피 반응까지만. 정체 확정으로 이어질 자백 금지." ) elif any(kw in text for kw in TRIGGERS["meta"]): flag = "meta" directive = ("유저가 세계관 밖 화제를 꺼냈다. 카르밀라로서 알아듣지 못하는 척, " "세계관 내 화법으로 부드럽게 장면 안으로 되돌린다.") delta = { "guard_directive": directive.replace("{", "(").replace("}", ")"), "guard_flag": flag, } yield Event(invocation_id=ctx.invocation_id, author=self.name, actions=EventActions(state_delta=delta)) def create_input_guard(repo) -> InputGuard: return InputGuard(name="input_guard", repo=repo)