Files
chat/chat/services/significance.py
T
2026-04-26 13:27:25 -04:00

76 lines
2.5 KiB
Python

"""Turn-level significance scorer (T22).
Per Requirements §11.1, each turn is scored on a 0-3 scale:
- 0 = Routine: small talk, ordinary action.
- 1 = Notable: a specific detail or beat worth remembering.
- 2 = Significant: a scene-level moment, real disagreement, confided secret.
- 3 = Pivotal: a relationship-altering event (first kiss, betrayal, "I love
you").
The scorer is conservative: pivotal (3) requires a clear signal because the
auto-pin rule (§8.5) gives those memories permanent shelf space. The
classifier returns a strict-JSON ``SignificanceVerdict``; a malformed or
refusal-shaped response falls back to ``score=1`` (Notable) — a safe
middle-of-the-road default that won't trigger auto-pin.
"""
from __future__ import annotations
from pydantic import BaseModel, Field
from chat.llm.classify import classify
from chat.llm.client import LLMClient
class SignificanceVerdict(BaseModel):
score: int = Field(ge=0, le=3)
reason: str = ""
_SYSTEM = """You score the significance of a roleplay turn 0-3:
0 = Routine: small talk, ordinary action.
1 = Notable: a specific detail or beat worth remembering.
2 = Significant: a scene-level moment, real disagreement, confided secret.
3 = Pivotal: a relationship-altering event (first kiss, betrayal, "I love you").
Be conservative — pivotal (3) requires a clear signal. Reply with JSON: {"score": int 0-3, "reason": str}."""
async def compute_significance(
client: LLMClient,
*,
model: str,
narrative_text: str,
prior_dialogue: list[dict],
timeout_s: float = 10.0,
) -> int:
"""Score the significance of ``narrative_text`` (the just-written turn).
``prior_dialogue`` is a list of ``{"speaker", "text"}`` dicts ordered
oldest-first; the last 6 entries are stitched into the user prompt as
context so the classifier can recognize escalation. Returns an int in
``[0, 3]`` — clamped defensively in case the classifier slips a value
past the schema validator.
"""
user_prompt = "PRIOR DIALOGUE:\n"
for turn in prior_dialogue[-6:]:
speaker = turn.get("speaker", "?")
text = turn.get("text", "")
user_prompt += f"{speaker}: {text}\n"
user_prompt += (
f"\nNEW TURN:\n{narrative_text}\n\n"
"Score the significance of the NEW TURN."
)
result = await classify(
client,
model=model,
system=_SYSTEM,
user=user_prompt,
schema=SignificanceVerdict,
default=SignificanceVerdict(score=1, reason="fallback"),
timeout_s=timeout_s,
)
return max(0, min(3, result.score))