"""The NPC role service (charter §5/§6). Loads the NPC's server-only persona + knowledge, renders the digest, routes the model, calls Ollama with one retry, logs the call (§10), and returns raw prose — move/fact tags left intact for the client to extract, validate, and apply (§2/§6). Mirrors narrate.py. """ import random from time import perf_counter from . import call_log, ollama_client, prompts, routing from .content import load_npc class UnknownNpc(Exception): """No authored content resolves for the requested npc_id.""" def run( canon_log: dict, npc_id: str, disposition: int, available_moves: list[str], utterance: str, ) -> str: npc = load_npc(npc_id) if npc is None: raise UnknownNpc(npc_id) cfg = routing.for_role("npc") options = {**cfg.options, "seed": random.randint(0, 2**31 - 1)} messages = [ {"role": "system", "content": prompts.system_prompt("npc")}, {"role": "user", "content": prompts.render_npc_digest( canon_log, persona=npc.get("persona", ""), knowledge=npc.get("knowledge", []), disposition=disposition, available_moves=available_moves, utterance=utterance, )}, ] start = perf_counter() try: text = ollama_client.chat(cfg.model, messages, options, think=cfg.think) except ollama_client.ModelError as exc: call_log.record( role="npc", model=cfg.model, options=options, messages=messages, canon_log=canon_log, ok=False, latency_ms=int((perf_counter() - start) * 1000), error=str(exc), ) raise call_log.record( role="npc", model=cfg.model, options=options, messages=messages, canon_log=canon_log, ok=True, latency_ms=int((perf_counter() - start) * 1000), response=text, ) return text