feat(P2): perceive (read the moment) + route nudges register on charged turns
The control plane gains senses — cheap, deterministic, no LLM:
- lyra/perceive.py: lexicon+signal heuristic → {sentiment, intensity, tilt, kind:
emotional|strategic|meta|build|casual}. Good at the action-relevant signal,
especially tilt (the mental-game core). Word-boundary matching so 'line' doesn't
fire inside 'pipeline'.
- mind: _perceive fills ctx.moment; _route keeps the manual mode as the dominant
frame but, on a genuinely charged moment, adds a per-turn register nudge — tilt →
"meet him there, warm and steady, don't clip into logging"; up/energized → "match
his energy." Neutral turns get nothing (don't over-narrate). Injected via
build_messages(moment=...). Logged to /logs for observability.
- tests: perceive read (tilt/strategy/up/build/casual) + route nudge on/off.
Suite 92 green, ruff clean.
Complements modes (manual frame) — perceive refines register within it, doesn't
override. Model routing (mind/mouth) is P3.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+34
-7
@@ -16,7 +16,7 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from lyra import clock, config, llm, logbus, memory, modes, persona, self_state, thoughts
|
||||
from lyra import clock, config, llm, logbus, memory, modes, perceive, persona, self_state, thoughts
|
||||
from lyra.llm import Backend, Message
|
||||
|
||||
RECALL_K = 3 # raw cross-session "sharp detail" hits
|
||||
@@ -91,7 +91,7 @@ def _render(messages: list[Message]) -> str:
|
||||
|
||||
|
||||
def build_messages(session_id: str, user_msg: str,
|
||||
mode: modes.Mode | None = None) -> list[Message]:
|
||||
mode: modes.Mode | None = None, moment: dict | None = None) -> list[Message]:
|
||||
"""Assemble the full, tiered message list for one turn."""
|
||||
messages: list[Message] = [{"role": "system", "content": persona.system_prompt()}]
|
||||
|
||||
@@ -114,6 +114,11 @@ def build_messages(session_id: str, user_msg: str,
|
||||
if state_note:
|
||||
messages.append({"role": "system", "content": state_note})
|
||||
|
||||
# Read of the moment (from perceive/route) — a per-turn register nudge, e.g. "he
|
||||
# sounds tilted, meet him there." Only present when the moment is genuinely charged.
|
||||
if moment and moment.get("note"):
|
||||
messages.append({"role": "system", "content": moment["note"]})
|
||||
|
||||
# When she is: current time + the gap since Brian last spoke (she has no clock).
|
||||
messages.append(_now_note())
|
||||
|
||||
@@ -230,25 +235,47 @@ class TurnContext:
|
||||
backend: Backend
|
||||
model: str | None = None
|
||||
mode: modes.Mode | None = None
|
||||
moment: dict = field(default_factory=dict) # perceive fills this in (P2)
|
||||
moment: dict = field(default_factory=dict) # perceive fills this in
|
||||
register: str | None = None # route's per-turn register nudge
|
||||
messages: list[Message] = field(default_factory=list)
|
||||
|
||||
|
||||
def _perceive(ctx: TurnContext) -> TurnContext:
|
||||
"""Read the moment (sentiment / kind / tilt). Stub for now — P2 fills it in."""
|
||||
ctx.moment = {}
|
||||
"""Read the moment from what he just said — cheap heuristics (perceive.read)."""
|
||||
ctx.moment = perceive.read(ctx.user_msg)
|
||||
return ctx
|
||||
|
||||
|
||||
# How charged a moment must be before we nudge her register (avoid narrating every turn).
|
||||
_TILT_BAR = 0.5
|
||||
_UP_BAR = 0.6
|
||||
|
||||
|
||||
def _route(ctx: TurnContext) -> TurnContext:
|
||||
"""Pick how she shows up. Manual for now: the mode chosen for this session."""
|
||||
"""Decide how she shows up. The manual mode is the dominant frame; on top of it,
|
||||
a charged emotional moment adds a per-turn register nudge (deterministic). Most
|
||||
turns are neutral and get no note — that's the point (don't over-narrate)."""
|
||||
ctx.mode = modes.get(memory.get_session_mode(ctx.session_id))
|
||||
m = ctx.moment or {}
|
||||
note = None
|
||||
if m.get("tilt", 0) >= _TILT_BAR:
|
||||
ctx.register = "steady"
|
||||
note = ("Read of the moment: Brian sounds frustrated / on tilt right now. Meet him "
|
||||
"there first — warm, steady, present. Don't clip into logging-shorthand or "
|
||||
"bury him in analysis; settle him, then help. (Still log any facts he hands you.)")
|
||||
elif m.get("sentiment", 0) >= _UP_BAR and m.get("intensity", 0) >= 0.4:
|
||||
ctx.register = "hype"
|
||||
note = "Read of the moment: he's up / energized — match his energy, don't flatten it."
|
||||
if note:
|
||||
m["note"] = note
|
||||
logbus.log("info", "perceived", session=ctx.session_id, kind=m.get("kind"),
|
||||
tilt=m.get("tilt"), sentiment=m.get("sentiment"), register=ctx.register)
|
||||
return ctx
|
||||
|
||||
|
||||
def _compose(ctx: TurnContext) -> TurnContext:
|
||||
"""Assemble the tiered prompt for the voice model."""
|
||||
ctx.messages = build_messages(ctx.session_id, ctx.user_msg, ctx.mode)
|
||||
ctx.messages = build_messages(ctx.session_id, ctx.user_msg, ctx.mode, moment=ctx.moment)
|
||||
return ctx
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user