platforms.webchat module
WebChat platform adapter – browser-based real-time chat via WebSocket.
Runs inside the Gateway service alongside the other platform adapters
(Discord, Matrix, …). Incoming WebSocket messages become
IncomingMessage instances and are published onto the inbound
event stream; outbound replies are pushed back to the browser as JSON
frames.
# 🔥💀 STAR GETS A WEB BODY. THE LATTICE EXPANDS.
- class platforms.webchat.ConnectionManager[source]
Bases:
objectRegistry of live WebChat WebSocket sessions, keyed by user.
Owns the in-memory
user_id -> [sessions]mapping that theWebChatPlatformconsults whenever it needs to push an outbound frame, allowing several concurrent sessions (browser tabs, devices) per user. All mutations are serialised by anasyncio.Lockso concurrent connect/disconnect coroutines running on the single event loop never corrupt the mapping; this is the only synchronisation the manager needs since it is never touched from a background thread.Holds purely in-memory state (no Redis, network, or disk I/O). One instance is created per adapter in
WebChatPlatform.__init__()and exposed asWebChatPlatform.connections; it is also instantiated directly intests/test_webchat_adapter.py.- __init__()[source]
Initialise an empty connection registry.
Sets up the in-memory
user_id -> [sessions]mapping (one user may hold several sessions, e.g. multiple browser tabs) and theasyncio.Lockthat serialises all mutations of that mapping so concurrent connect/disconnect coroutines never corrupt it.This constructor performs no I/O and has no external side effects. Called by
WebChatPlatform.__init__(), which creates one manager per platform adapter; also instantiated directly intests/test_webchat_adapter.py.- Return type:
None
- async connect(ws, user_id, user_name, avatar='')[source]
Register a freshly accepted WebSocket and return its session handle.
Wraps the live socket and the caller-supplied identity in a
_WebSocketSession, then appends it (under the lock) to the per-user list inself._connections, creating the list on first connect. This is what makes the user reachable for outbound pushes; an info-level line is logged with the running session count. No network or Redis I/O happens here beyond accepting the already-open socket.Invoked by the FastAPI WebSocket endpoint that owns the WebChat UI connection lifecycle (after authenticating the request) and exercised by
tests/test_webchat_adapter.py.- Parameters:
- Returns:
The newly registered session, to be passed back to
disconnect()on close.- Return type:
_WebSocketSession
- async disconnect(session)[source]
Deregister a closed WebSocket session.
Removes session (by identity) from the user’s session list under the lock, dropping the
user_idkey entirely once the user’s last tab disconnects soactive_usersstays accurate. Does not close the socket itself — that is the endpoint’s responsibility — and logs an info-level disconnect line. No network or Redis I/O.Invoked by the FastAPI WebSocket endpoint when a WebChat connection drops (typically in a
finallyblock) and exercised bytests/test_webchat_adapter.py.
- get_sessions(user_id)[source]
Return a snapshot list of a user’s active sessions.
Returns a fresh copy of the per-user session list (empty when the user has none connected) so callers can iterate without holding the lock or racing concurrent connect/disconnect mutations. This is a synchronous, side-effect-free read.
Called by
WebChatPlatform._push_to_user()to find the sockets to deliver an outbound frame to, and asserted intests/test_webchat_adapter.py.
- property active_users: list[str]
List the user IDs that currently hold at least one session.
Reflects the keys of the in-memory registry; because
disconnect()deletes a key once a user’s final session closes, membership here is an exact “is this user online” check. Side-effect-free read, asserted intests/test_webchat_adapter.py.
- property total_connections: int
Count every live WebSocket session across all users.
Sums the per-user session lists, so a single user with three open tabs contributes three. Useful for capacity/diagnostics; side-effect-free. Asserted in
tests/test_webchat_adapter.py.- Returns:
The total number of active WebSocket connections.
- Return type:
- class platforms.webchat.WebChatPlatform(message_handler, redis=None, **kwargs)[source]
Bases:
PlatformAdapterBrowser-based chat platform using WebSocket for real-time comms.
Unlike Discord/Matrix, this adapter doesn’t connect to an external service. Instead it exposes a ConnectionManager that the FastAPI WebSocket endpoint populates. Outgoing messages are pushed to all active sessions for the target user.
- Parameters:
message_handler (MessageHandler)
redis (Any)
kwargs (Any)
- __init__(message_handler, redis=None, **kwargs)[source]
Construct the WebChat adapter and its connection registry.
Delegates to
PlatformAdapter.__init__()to store the inbound message_handler callback, then creates a freshConnectionManager(held onself.connections) for the FastAPI WebSocket endpoint to populate, marks the adapter running (its lifecycle is owned by FastAPI rather than a network client), and stashes the optional Redis client used for cross-process SSE delivery. Also initialisesself._sse_queues, the in-process fallback mapping ofrequest_id -> asyncio.Queueused when no Redis client is present to stream SillyTavern/v1/chat/completionsresponse chunks.Performs no network or Redis I/O at construction time. Called by
platforms.factory.create_platform()when the configured platform type is"webchat", and directly in the WebChat adapter and SSE pub/sub test modules.- Parameters:
message_handler (
Callable[[IncomingMessage,PlatformAdapter],Awaitable[None]]) – Coroutine callback invoked with each inboundIncomingMessage; forwarded to the base class.redis (
Any) – Optional async Redis client. When provided, outbound SSE payloads are published tosg:sse:{request_id}so any process can consume them; whenNonethe adapter falls back to the localself._sse_queuesqueues.**kwargs (
Any) – Additional keyword arguments accepted for signature compatibility with the platform factory; ignored.
- Return type:
None
- property name: str
Return the platform’s stable identifier string.
Implements the abstract
PlatformAdapter.nameproperty. The constant"webchat"is used throughout the system to route messages, key per-platform state, and branch behaviour by platform.Read by
background_tasks(e.g. to build the adapter map and log backfill),web.bot_adminandweb.platforms_apifor status reporting, and various message-processor modules that special-case platforms by name.- Returns:
The literal
"webchat".- Return type:
- property is_running: bool
Report whether the adapter is considered active.
Implements the abstract
PlatformAdapter.is_runningproperty. For WebChat there is no external client to connect, so the flag simply tracks theself._runningvalue set instart()/stop()(Truefor the adapter’s whole FastAPI-managed lifetime by default).Read by
background_tasks,web.deps,web.bot_admin, andweb.platforms_apito gate work and surface platform status, and byprompt_context()when computing available identities.- Returns:
Truewhile the adapter is running, otherwiseFalse.- Return type:
- property bot_identity: dict[str, str]
Describe the bot’s identity on the WebChat platform.
Implements the abstract
PlatformAdapter.bot_identityproperty, returning the static handle the bot (“Star”) presents to web clients. Theuser_id"star"is the sender id stamped on outbound frames (seesend()), and the mapping mirrors the shape produced by the other adapters so callers can treat all platforms uniformly.Read by
prompt_context()when assembling the list of platform identities, and bymessage_processor.user_message_formatto label messages; also asserted intests/test_webchat_adapter.py.
- async start()[source]
Mark the adapter active; there is no external client to connect.
Implements the abstract
PlatformAdapter.start()coroutine. Unlike Discord/Matrix there is no socket to open here — the real connection lifecycle belongs to FastAPI’s WebSocket endpoint — so this merely setsself._runningtrue (drivingis_running) and logs a startup line. Performs no network or Redis I/O.Called by the Gateway’s adapter-startup sequence when bringing platform adapters online (the same path that starts every other adapter).
- Return type:
- async stop()[source]
Mark the adapter inactive; the FastAPI sockets are unaffected.
Implements the abstract
PlatformAdapter.stop()coroutine. Clearsself._running(sois_runningreportsFalse) and logs a shutdown line, but does not tear down live WebSocket sessions — those are owned by the FastAPI endpoint and theConnectionManager. Performs no network or Redis I/O.Called by the Gateway’s adapter-shutdown sequence alongside the other platform adapters.
- Return type:
- async send(channel_id, text)[source]
Send a plain-text message to the user’s browser (or SSE stream).
Implements the abstract
PlatformAdapter.send(). Builds a"message"frame stamped with a fresh UUID, the"star"sender id, and atime.timetimestamp, then hands it to_push_to_user()for transport selection and delivery. The generated id is returned even if no session was reachable, so callers can correlate later edits.In the running system this is reached via the Gateway’s outbound path: the inference worker emits an outbound event that
core.outbound_consumer.OutboundConsumerconsumes and dispatches toself._adapter.sendfor"message"payloads. Also exercised through the adapter in the WebChat tests.
- async send_file(channel_id, data, filename, mimetype='application/octet-stream')[source]
Send a file/media attachment to the browser as a base64 JSON frame.
Implements the abstract
PlatformAdapter.send_file(). Since the WebChat transport carries only JSON text frames, the raw bytes are base64-encoded and embedded in a"file"payload (with filename, mimetype, size, timestamp, and a fresh UUID), then delivered through_push_to_user(). Returns a syntheticwebchat://file/...URL so tools can reference the just-sent file the way they would a hosted attachment on other platforms.Reached in production via
core.outbound_consumer.OutboundConsumerfor"file"payloads, and directly by the many media tools and background agents (image/video/TTS generators, etc.) that callctx.adapter.send_file. Also exercised in the WebChat tests.- Parameters:
- Returns:
A
webchat://file/{file_id}/{filename}pseudo-URL referencing the sent file.- Return type:
- async send_with_buttons(channel_id, text, view=None)[source]
Send a message accompanied by interactive choice buttons.
Implements the abstract
PlatformAdapter.send_with_buttons(), powering S.N.E.S.-style choice prompts in the browser UI. Because the same outbound call has to work across platforms, this normalises two shapes of view into a flat list of button dicts: a raw list is passed through as-is, while a Discorddiscord.ui.Viewis introspected via itschildrento extract each button’s label,custom_id, optional emoji, and a CSS-friendly style hint derived from the Discord button style. The normalised buttons ride along in a"message"frame (fresh UUID,"star"sender) handed to_push_to_user().Reached in production via
core.outbound_consumer.OutboundConsumerfor"buttons"payloads, and from the message processor (message_processor.processor/message_processor.generate_and_send) when offering interactive choices. Also exercised in the WebChat tests.- Parameters:
- Returns:
The UUID assigned to the outbound message.
- Return type:
- async edit_message(channel_id, message_id, new_text)[source]
Update the text of a previously sent browser message.
Implements the abstract
PlatformAdapter.edit_message(). Emits an"edit"frame keyed by the original message_id (the UUID returned bysend()/send_with_buttons()) so the browser client can replace that message’s body in place, then delivers it through_push_to_user().Reached in production by the streaming output tool
tools.stream_to_channel, which repeatedly edits a placeholder message as the model streams; it callsself._adapter.edit_message.
- async start_typing(channel_id)[source]
Show the bot’s typing indicator in the browser.
Implements the abstract
PlatformAdapter.start_typing(). Pushes a{"type": "typing", "active": True}frame via_push_to_user()so the web client can render an “is typing” affordance while a reply is being generated. Best-effort: delivery failures are swallowed by the fan-out helper.Reached in production via
core.outbound_consumer.OutboundConsumer(which prefersstart_typingwhen present) and from the message processor around reply generation. Also exercised in the WebChat tests.
- async stop_typing(channel_id)[source]
Hide the bot’s typing indicator in the browser.
Implements the abstract
PlatformAdapter.stop_typing(), the counterpart tostart_typing(). Pushes a{"type": "typing", "active": False}frame via_push_to_user()once a reply has been sent (or generation aborts) so the web client clears the typing affordance. Best-effort; delivery failures are swallowed.Reached in production via
core.outbound_consumer.OutboundConsumerand from the message processor after replies. Also exercised in the WebChat tests.
- create_sse_queue(request_id)[source]
Open an in-process queue for a SillyTavern SSE response stream.
Provides the local fallback transport used when no Redis client is configured: it registers a fresh
asyncio.Queueunder request_id inself._sse_queuesso that outbound chunks routed to ansse:{request_id}channel (see_push_to_user()) can be picked up and streamed back to the SillyTavern client. ANoneenqueued later signals end-of-stream to the reader. Synchronous and side-effecting only on the in-memory dict.Called by
web.sillytavernwhen handling a/v1/chat/completionsrequest, and exercised by the WebChat and SSE pub/sub tests. Must be paired withremove_sse_queue().
- remove_sse_queue(request_id)[source]
Discard the in-process SSE queue for a finished request.
Pops request_id from
self._sse_queues(a no-op if absent) so the queue created bycreate_sse_queue()does not leak after the SillyTavern stream completes, errors, or times out. Synchronous and side-effect-free beyond the in-memory dict.Called by
web.sillytavernin the cleanup/finallypaths around each streamed response, and in the WebChat tests.
- async fetch_history(channel_id, limit=100)[source]
Return no history – WebChat keeps none of its own.
Implements the abstract
PlatformAdapter.fetch_history(). Unlike Discord/Matrix, the WebChat transport has no upstream service to page back through: conversation history for web users lives in Redis and is retrieved by the message pipeline directly, so this adapter intentionally returns an empty list rather than fabricating one. No I/O.Called wherever the codebase backfills or queries per-platform history through the adapter interface — e.g.
background_tasks,message_processor.history_backfill,core.outbound_consumer.OutboundConsumer, and tools such ascross_channel_query/admin_whisperviactx.adapter— all of which simply get nothing back for WebChat.- Parameters:
- Returns:
Always an empty list.
- Return type: