feat(buffer): ContextualBufferProxy + append_system_message

_SessionBuffer.append_system_message(user_id, session_id, text) injects a
synthetic SystemMessage into the named session slot (creating it if absent).

ContextualBufferProxy closes over user_id + session_id so call sites need
only call proxy.append_system_message(text).

get_session_buffer(user_id, session_id, channel) in device_ws returns a
ContextualBufferProxy, keeping the test-patchable function signature intact.
This commit is contained in:
Roberto
2026-05-14 21:11:13 +02:00
parent 6188ae15b3
commit 2b71469e86
2 changed files with 44 additions and 6 deletions

View File

@@ -374,14 +374,15 @@ async def _handle_floating_request(
# ── v8 Contextual Sidebar Handlers ─────────────────────────────────── # ── v8 Contextual Sidebar Handlers ───────────────────────────────────
def get_session_buffer(session_id: str, channel: str = "contextual"): def get_session_buffer(user_id: str, session_id: str, channel: str = "contextual"):
"""Return the session buffer for the given session. """Return a session-scoped buffer proxy for the given user+session.
The channel kwarg is accepted for forward-compatibility but not used for Returns a _ContextualBufferProxy that exposes append_system_message().
namespacing yet (session ids are UUIDs so collisions are negligible).
Defined at module level so tests can monkeypatch it. Defined at module level so tests can monkeypatch it.
The channel kwarg is accepted for forward-compatibility.
""" """
return session_buffer from app.core.agent_session_buffer import ContextualBufferProxy # noqa: PLC0415
return ContextualBufferProxy(session_buffer, user_id, session_id)
async def _handle_contextual_request( async def _handle_contextual_request(
@@ -472,7 +473,7 @@ async def _handle_contextual_scope_update(
session_id: str = frame.get("session_id") or str(uuid4()) session_id: str = frame.get("session_id") or str(uuid4())
scope = ContextualScope.model_validate(frame.get("scope", {})) scope = ContextualScope.model_validate(frame.get("scope", {}))
block = render_scope_block(scope) block = render_scope_block(scope)
buf = get_session_buffer(session_id, channel="contextual") buf = get_session_buffer(user_id, session_id, channel="contextual")
buf.append_system_message( buf.append_system_message(
f"User navigated to a new view. {block} Treat this as the new active context." f"User navigated to a new view. {block} Treat this as the new active context."
) )

View File

@@ -54,6 +54,43 @@ class _SessionBuffer:
with self._lock: with self._lock:
self._store.pop((user_id, session_id), None) self._store.pop((user_id, session_id), None)
def append_system_message(self, user_id: str, session_id: str, text: str) -> None:
"""Append a synthetic system message to the buffer for the given session.
Creates the session slot if it does not yet exist. Used by the
contextual_scope_update handler to inject navigation events without
making an LLM call.
"""
from langchain_core.messages import SystemMessage # noqa: PLC0415
key = (user_id, session_id)
with self._lock:
entry = self._store.get(key)
if entry is None:
msgs: list[BaseMessage] = [SystemMessage(content=text)]
else:
_, existing = entry
msgs = list(existing) + [SystemMessage(content=text)]
capped = msgs[-MAX_MESSAGES_PER_SESSION:]
self._store[key] = (time.monotonic(), capped)
class ContextualBufferProxy:
"""Thin wrapper around _SessionBuffer that closes over user_id + session_id.
Returned by get_session_buffer() so callers can call
``proxy.append_system_message(text)`` without threading user_id/session_id
through every call site.
"""
def __init__(self, buf: "_SessionBuffer", user_id: str, session_id: str) -> None:
self._buf = buf
self._user_id = user_id
self._session_id = session_id
def append_system_message(self, text: str) -> None:
self._buf.append_system_message(self._user_id, self._session_id, text)
# Module-level singleton — same pattern as _pending_states in api/app/api/routes/auth.py # Module-level singleton — same pattern as _pending_states in api/app/api/routes/auth.py
session_buffer = _SessionBuffer() session_buffer = _SessionBuffer()