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:
@@ -374,14 +374,15 @@ async def _handle_floating_request(
|
||||
# ── v8 Contextual Sidebar Handlers ───────────────────────────────────
|
||||
|
||||
|
||||
def get_session_buffer(session_id: str, channel: str = "contextual"):
|
||||
"""Return the session buffer for the given session.
|
||||
def get_session_buffer(user_id: str, session_id: str, channel: str = "contextual"):
|
||||
"""Return a session-scoped buffer proxy for the given user+session.
|
||||
|
||||
The channel kwarg is accepted for forward-compatibility but not used for
|
||||
namespacing yet (session ids are UUIDs so collisions are negligible).
|
||||
Returns a _ContextualBufferProxy that exposes append_system_message().
|
||||
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(
|
||||
@@ -472,7 +473,7 @@ async def _handle_contextual_scope_update(
|
||||
session_id: str = frame.get("session_id") or str(uuid4())
|
||||
scope = ContextualScope.model_validate(frame.get("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(
|
||||
f"User navigated to a new view. {block} Treat this as the new active context."
|
||||
)
|
||||
|
||||
@@ -54,6 +54,43 @@ class _SessionBuffer:
|
||||
with self._lock:
|
||||
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
|
||||
session_buffer = _SessionBuffer()
|
||||
|
||||
Reference in New Issue
Block a user