WS Gateway:
- WebSocket lifecycle handler with RS256 JWT auth
- Redis bridge: device registry, frame publishing, tool_result routing
- Inbound routing: tool_result→LPUSH, home/floating→chat pub/sub
- Outbound: subscribes to ws:out:{user_id}, forwards to Electron
- Single-worker Dockerfile (long-lived WS connections)
Chat Service:
- Redis consumer: subscribes to chat:request:* pattern
- Redis-based ws_context: tool_call→publish, BRPOP tool_result (30s timeout)
- deep_agent: single-agent runner with home/floating/stream variants
- memory_middleware: core/associative/episodic/proactive memory with Fernet
- Domain agents: task (8 tools), note (5), project (6), timeline (4)
- LLM factory via LiteLLM (100+ providers)
- Output formatter (StreamFormatter)
- POST /chat REST fallback with Traefik header auth
- Multi-worker Dockerfile with 120s timeout for LLM calls
105 lines
3.4 KiB
Python
105 lines
3.4 KiB
Python
"""Redis bridge — device registry + pub/sub routing.
|
|
|
|
All inter-service communication passes through Redis:
|
|
- Device registry: HSET/HDEL ws:devices:{user_id}
|
|
- Outbound frames: Subscribe ws:out:{user_id}
|
|
- Chat requests: Publish chat:request:{user_id}
|
|
- Batch requests: Publish batch:request:{user_id}
|
|
- Tool results: LPUSH tool:result:{call_id}
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
|
|
from shared.redis import (
|
|
batch_request_channel,
|
|
chat_request_channel,
|
|
device_key,
|
|
redis_client,
|
|
tool_result_key,
|
|
ws_out_channel,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Instance ID for this gateway replica (set on startup)
|
|
_GATEWAY_ID: str = ""
|
|
|
|
|
|
def set_gateway_id(gid: str) -> None:
|
|
global _GATEWAY_ID
|
|
_GATEWAY_ID = gid
|
|
|
|
|
|
def get_gateway_id() -> str:
|
|
return _GATEWAY_ID
|
|
|
|
|
|
# ── Device Registry ──────────────────────────────────────────────────
|
|
|
|
|
|
async def register_device(user_id: str, device_id: str) -> None:
|
|
"""Register a connected device in Redis."""
|
|
key = device_key(user_id)
|
|
await redis_client.hset(key, mapping={
|
|
"device_id": device_id,
|
|
"gateway_id": _GATEWAY_ID,
|
|
})
|
|
logger.info("redis_bridge: registered user=%s device=%s gateway=%s", user_id, device_id, _GATEWAY_ID)
|
|
|
|
|
|
async def unregister_device(user_id: str) -> None:
|
|
"""Remove device registration from Redis."""
|
|
key = device_key(user_id)
|
|
await redis_client.delete(key)
|
|
logger.info("redis_bridge: unregistered user=%s", user_id)
|
|
|
|
|
|
async def is_device_online(user_id: str) -> bool:
|
|
"""Check if a device is registered."""
|
|
key = device_key(user_id)
|
|
return await redis_client.exists(key) > 0
|
|
|
|
|
|
# ── Frame Routing ────────────────────────────────────────────────────
|
|
|
|
|
|
async def publish_chat_request(user_id: str, frame: dict) -> None:
|
|
"""Forward a chat request frame to the Chat Service via Redis."""
|
|
channel = chat_request_channel(user_id)
|
|
await redis_client.publish(channel, json.dumps(frame))
|
|
logger.debug("redis_bridge: published chat_request user=%s", user_id)
|
|
|
|
|
|
async def publish_batch_request(user_id: str, frame: dict) -> None:
|
|
"""Forward a batch request frame to the Batch Agent Service via Redis."""
|
|
channel = batch_request_channel(user_id)
|
|
await redis_client.publish(channel, json.dumps(frame))
|
|
logger.debug("redis_bridge: published batch_request user=%s", user_id)
|
|
|
|
|
|
async def push_tool_result(call_id: str, result: dict) -> None:
|
|
"""Push a tool_result to the Redis list for the waiting service.
|
|
|
|
Chat/Batch services do BRPOP on this key with a 30s timeout.
|
|
"""
|
|
key = tool_result_key(call_id)
|
|
await redis_client.lpush(key, json.dumps(result))
|
|
# Auto-expire after 60s to prevent stale keys
|
|
await redis_client.expire(key, 60)
|
|
logger.debug("redis_bridge: pushed tool_result call_id=%s", call_id)
|
|
|
|
|
|
async def subscribe_outbound(user_id: str):
|
|
"""Return an async pubsub subscription for frames to send to Electron.
|
|
|
|
Chat/Batch services publish to ws:out:{user_id} and this gateway
|
|
forwards them to the connected WebSocket.
|
|
"""
|
|
channel = ws_out_channel(user_id)
|
|
pubsub = redis_client.pubsub()
|
|
await pubsub.subscribe(channel)
|
|
return pubsub
|