121 lines
3.6 KiB
Python
121 lines
3.6 KiB
Python
"""Minimal message bus for gateway-style host integration."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime, timezone
|
|
from typing import Any
|
|
from uuid import uuid4
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class ChannelIdentity:
|
|
"""Normalized channel routing identity.
|
|
|
|
`channel_id` is the Beaver adapter instance id, not the platform kind.
|
|
"""
|
|
|
|
channel_id: str
|
|
kind: str
|
|
account_id: str
|
|
peer_id: str
|
|
thread_id: str | None = None
|
|
peer_type: str = "unknown"
|
|
user_id: str | None = None
|
|
message_id: str | None = None
|
|
|
|
def validation_error(self) -> str | None:
|
|
if not self.channel_id.strip():
|
|
return "channel_id is required"
|
|
if not self.account_id.strip():
|
|
return "account_id is required"
|
|
if not self.peer_id.strip():
|
|
return "peer_id is required"
|
|
return None
|
|
|
|
def session_id(self) -> str:
|
|
parts = [self.channel_id, self.account_id, self.peer_id]
|
|
if self.thread_id:
|
|
parts.append(self.thread_id)
|
|
return ":".join(_clean_session_part(part) for part in parts)
|
|
|
|
def dedupe_key(self) -> str | None:
|
|
if not self.message_id:
|
|
return None
|
|
return f"{self.session_id()}:{_clean_session_part(self.message_id)}"
|
|
|
|
|
|
def _clean_session_part(value: str) -> str:
|
|
cleaned = str(value).strip()
|
|
if not cleaned:
|
|
return "unknown"
|
|
return cleaned.replace(":", "_")
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class InboundMessage:
|
|
"""A minimal inbound message accepted by the gateway bridge."""
|
|
|
|
channel: str
|
|
content: str
|
|
content_type: str = "text"
|
|
channel_identity: ChannelIdentity | None = None
|
|
session_id: str | None = None
|
|
user_id: str | None = None
|
|
title: str | None = None
|
|
execution_context: str | None = None
|
|
model: str | None = None
|
|
provider_name: str | None = None
|
|
embedding_model: str | None = None
|
|
message_id: str = field(default_factory=lambda: str(uuid4()))
|
|
metadata: dict[str, Any] = field(default_factory=dict)
|
|
timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class OutboundMessage:
|
|
"""A minimal outbound message produced by the gateway bridge."""
|
|
|
|
channel: str
|
|
content: str
|
|
session_id: str | None
|
|
finish_reason: str
|
|
content_type: str = "text"
|
|
channel_identity: ChannelIdentity | None = None
|
|
message_id: str = field(default_factory=lambda: str(uuid4()))
|
|
run_id: str | None = None
|
|
provider_name: str | None = None
|
|
model: str | None = None
|
|
usage: dict[str, Any] = field(default_factory=dict)
|
|
metadata: dict[str, Any] = field(default_factory=dict)
|
|
timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
|
|
|
|
class MessageBus:
|
|
"""Minimal async message bus with inbound/outbound queues."""
|
|
|
|
def __init__(self) -> None:
|
|
self.inbound: asyncio.Queue[InboundMessage] = asyncio.Queue()
|
|
self.outbound: asyncio.Queue[OutboundMessage] = asyncio.Queue()
|
|
|
|
async def publish_inbound(self, message: InboundMessage) -> None:
|
|
await self.inbound.put(message)
|
|
|
|
async def consume_inbound(self) -> InboundMessage:
|
|
return await self.inbound.get()
|
|
|
|
async def publish_outbound(self, message: OutboundMessage) -> None:
|
|
await self.outbound.put(message)
|
|
|
|
async def consume_outbound(self) -> OutboundMessage:
|
|
return await self.outbound.get()
|
|
|
|
@property
|
|
def inbound_size(self) -> int:
|
|
return self.inbound.qsize()
|
|
|
|
@property
|
|
def outbound_size(self) -> int:
|
|
return self.outbound.qsize()
|