WillHbx's picture
refactor(utils): extract shared helpers into visualnovel/utils.py
7163908
Raw
History Blame Contribute Delete
2.95 kB
"""The Voices β€” the actor-context layer.
This isn't a separate model call; it's the slice of context that tells the shared LLM *who*
is on stage and how they speak. We only ever inject the sheets of the **present** characters
(never the whole cast) to respect the token budget and to keep NPCs non-omniscient.
"""
from __future__ import annotations
from .schemas import Character, GameState
from .utils import clip_words
def present_characters(state: GameState) -> list[Character]:
return [state.characters[cid] for cid in state.scene.present if cid in state.characters]
def actor_roster(state: GameState) -> str:
"""One-liner list of who can speak this turn."""
rows = [f"{c.id} = {c.name} ({c.one_line})" for c in present_characters(state)]
return "; ".join(rows) if rows else "narrator"
def present_sheets_block(state: GameState) -> str:
"""Compact, prompt-friendly sheets for the spirits currently on stage."""
blocks: list[str] = []
for c in present_characters(state):
facts = "; ".join(c.known_facts) if c.known_facts else "β€”"
rel = c.relationship
if rel <= -100:
rel_note = " ⚠️ DESPISES the wanderer β€” refuses all contact, storms off if addressed"
elif rel <= -60:
rel_note = " ⚠️ Very hostile β€” any unsolicited touch or flirt will make them leave"
elif rel <= -20:
rel_note = " β€” cold and resentful; physical contact is unwanted (negative delta)"
elif rel < 20:
rel_note = " β€” barely acquainted; unsolicited touch or flirt feels uncomfortable (negative delta)"
else:
rel_note = ""
# NPC↔NPC bonds β€” only mention others who are in the cast
npc_bond_lines: list[str] = []
for other_id, bond_val in c.npc_relations.items():
if other_id in state.characters:
other = state.characters[other_id]
note = c.npc_relation_notes.get(other_id, "")
label = f" ({note})" if note else ""
npc_bond_lines.append(f"{other.name}: {bond_val:+d}{label}")
npc_bonds_str = "; ".join(npc_bond_lines) if npc_bond_lines else "β€”"
blocks.append(
# clip_words guards the token budget against runaway generated fields
# (characters created before the creation-time caps can carry huge goals)
f"### {c.name} (id: {c.id})\n"
f"- one_line: {clip_words(c.one_line, 30)}\n"
f"- traits: {', '.join(c.traits) or 'β€”'}\n"
f"- voice: {clip_words(c.voice, 25) or 'β€”'}\n"
f"- goal: {clip_words(c.goals, 50) or 'β€”'}\n"
f"- mood: {c.mood} | feeling toward wanderer: {rel}/100{rel_note}\n"
f"- bonds with others: {npc_bonds_str}\n"
f"- knows: {facts}"
)
return "\n\n".join(blocks) if blocks else "(no spirits on stage β€” the narrator may speak)"