Files
beaver_project/app-instance/backend/beaver/foundation/events/message_bus.py

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()