60 lines
1.8 KiB
Python
60 lines
1.8 KiB
Python
"""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
|