feat: session modes (Talk/Cash) + live session HUD
Lyra now switches register based on what she's doing at the table instead of
being a wishy-washy companion mid-session.
Modes (lyra/modes.py):
- Talk (default companion) + Cash (live cash copilot); a mode = prompt card +
tool allow-list. Tool gating via tools.specs(allow=).
- Two-register Cash voice: act-first one-line logging when fed facts; full warm
companion voice for strategy / tilt / mental game.
- mode persisted per chat session (new sessions.mode column); auto-switch into
Cash when start_session fires; UI forces cloud backend in Cash (tools only
fire there).
Stack tracking + HUD:
- log_stack tool + poker_stack_log table; live net while sitting (stack - buy-in).
- poker.hud() bundle; /session HUD page (stack sparkline, hands, villains, notes,
stats) polling /session/data every 5s; Talk/Cash switcher + Session nav.
Endpoints: /session, /session/data, GET/POST /sessions/{id}/mode, /modes.
tests/test_modes.py (gating, mode roundtrip, stack/HUD); 36 tests green. v0.3.0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+28
-8
@@ -10,7 +10,7 @@ After replying, the session is compacted if enough new turns have accumulated.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from lyra import clock, config, llm, logbus, memory, persona, self_state, summary
|
||||
from lyra import clock, config, llm, logbus, memory, modes, persona, self_state, summary
|
||||
from lyra import tools as toolkit
|
||||
from lyra.llm import Backend, Message
|
||||
|
||||
@@ -24,6 +24,15 @@ MAX_TOOL_ROUNDS = 5 # cap tool-call iterations per turn
|
||||
TOOL_BACKENDS = {"cloud"}
|
||||
|
||||
|
||||
def _maybe_switch_mode(session_id: str, tool_name: str) -> None:
|
||||
"""Keep the chat framing aligned with the live data: opening a poker session
|
||||
auto-flips this chat into Cash mode (so the next turn gets the cash card + the
|
||||
full live toolset). Manual UI switching still overrides anytime."""
|
||||
if tool_name == "start_session":
|
||||
memory.set_session_mode(session_id, modes.CASH.key)
|
||||
logbus.log("info", "mode auto-switch", session=session_id, mode=modes.CASH.key)
|
||||
|
||||
|
||||
def _summary_note(summaries: list[memory.Summary]) -> Message:
|
||||
lines = [f"- ({(s.session_started_at or s.created_at)[:10]}) {s.content}" for s in summaries]
|
||||
body = "Gist of earlier sessions (compacted — ask if you need specifics):\n" + "\n".join(lines)
|
||||
@@ -56,7 +65,8 @@ def _render(messages: list[Message]) -> str:
|
||||
return "\n\n".join(f"[{m['role']}]\n{m['content']}" for m in messages)
|
||||
|
||||
|
||||
def build_messages(session_id: str, user_msg: str) -> list[Message]:
|
||||
def build_messages(session_id: str, user_msg: str,
|
||||
mode: modes.Mode | None = None) -> list[Message]:
|
||||
"""Assemble the full, tiered message list for one turn."""
|
||||
messages: list[Message] = [{"role": "system", "content": persona.system_prompt()}]
|
||||
|
||||
@@ -64,6 +74,12 @@ def build_messages(session_id: str, user_msg: str) -> list[Message]:
|
||||
# right after the persona — her sense of self before her model of the world.
|
||||
messages.append({"role": "system", "content": self_state.render_for_context(self_state.load())})
|
||||
|
||||
# Mode card: how to behave *right now* (e.g. live-cash copilot). High priority —
|
||||
# it sits just after her sense of self, before her model of the world. Talk mode
|
||||
# has no card (the persona's default voice is the Talk register).
|
||||
if mode and mode.card:
|
||||
messages.append({"role": "system", "content": mode.card})
|
||||
|
||||
# When she is: current time + the gap since Brian last spoke (she has no clock).
|
||||
messages.append(_now_note())
|
||||
|
||||
@@ -133,11 +149,12 @@ def respond(session_id: str, user_msg: str, backend: Backend = "cloud",
|
||||
model=model, embed=cfg.embed_backend,
|
||||
)
|
||||
|
||||
messages = build_messages(session_id, user_msg)
|
||||
mode = modes.get(memory.get_session_mode(session_id))
|
||||
messages = build_messages(session_id, user_msg, mode=mode)
|
||||
|
||||
# Tool loop: offer Lyra her tools; if she calls one, run it and feed the
|
||||
# result back so she can continue, until she returns a normal text reply.
|
||||
tool_specs = toolkit.specs() if backend in TOOL_BACKENDS else None
|
||||
# Tool loop: offer Lyra her tools (scoped to the mode); if she calls one, run it
|
||||
# and feed the result back so she can continue, until she returns a text reply.
|
||||
tool_specs = toolkit.specs(mode.tools) if backend in TOOL_BACKENDS else None
|
||||
ctx = {"session_id": session_id, "backend": backend}
|
||||
reply = ""
|
||||
for _ in range(MAX_TOOL_ROUNDS):
|
||||
@@ -152,6 +169,7 @@ def respond(session_id: str, user_msg: str, backend: Backend = "cloud",
|
||||
result = toolkit.dispatch(tc["name"], tc["arguments"], ctx)
|
||||
logbus.log("info", "tool call", session=session_id, tool=tc["name"], result=result[:80])
|
||||
messages.append({"role": "tool", "tool_call_id": tc["id"], "content": result})
|
||||
_maybe_switch_mode(session_id, tc["name"])
|
||||
if not reply:
|
||||
reply = "(I got tangled using my tools there — say that again?)"
|
||||
logbus.log("info", "reply", session=session_id, chars=len(reply))
|
||||
@@ -183,8 +201,9 @@ def respond_stream(session_id: str, user_msg: str, backend: Backend = "cloud",
|
||||
model=model, embed=cfg.embed_backend,
|
||||
)
|
||||
|
||||
messages = build_messages(session_id, user_msg)
|
||||
tool_specs = toolkit.specs() if backend in TOOL_BACKENDS else None
|
||||
mode = modes.get(memory.get_session_mode(session_id))
|
||||
messages = build_messages(session_id, user_msg, mode=mode)
|
||||
tool_specs = toolkit.specs(mode.tools) if backend in TOOL_BACKENDS else None
|
||||
ctx = {"session_id": session_id, "backend": backend}
|
||||
parts: list[str] = []
|
||||
for _ in range(MAX_TOOL_ROUNDS):
|
||||
@@ -207,6 +226,7 @@ def respond_stream(session_id: str, user_msg: str, backend: Backend = "cloud",
|
||||
result = toolkit.dispatch(tc["name"], tc["arguments"], ctx)
|
||||
logbus.log("info", "tool call", session=session_id, tool=tc["name"], result=result[:80])
|
||||
messages.append({"role": "tool", "tool_call_id": tc["id"], "content": result})
|
||||
_maybe_switch_mode(session_id, tc["name"])
|
||||
yield ("tool", tc["name"])
|
||||
|
||||
reply = "".join(parts)
|
||||
|
||||
Reference in New Issue
Block a user