feat: implement channel runtime connectors
This commit is contained in:
@ -13,6 +13,7 @@ from .schema import (
|
||||
AuthzConfig,
|
||||
BackendIdentityConfig,
|
||||
BeaverConfig,
|
||||
ChannelConfig,
|
||||
EmbeddingConfig,
|
||||
MCPServerConfig,
|
||||
ProviderConfig,
|
||||
@ -73,6 +74,7 @@ def load_config(
|
||||
embedding=_parse_embedding(data),
|
||||
tools=_parse_tools(data.get("tools")),
|
||||
authz=_parse_authz(data.get("authz")),
|
||||
channels=_parse_channels(data.get("channels")),
|
||||
backend_identity=_parse_backend_identity(data.get("backend_identity") or data.get("backendIdentity")),
|
||||
config_path=path,
|
||||
)
|
||||
@ -196,6 +198,48 @@ def _parse_authz(raw: Any) -> AuthzConfig:
|
||||
)
|
||||
|
||||
|
||||
def _parse_channels(raw: Any) -> dict[str, ChannelConfig]:
|
||||
channels: dict[str, ChannelConfig] = {}
|
||||
for channel_id, payload in _as_dict(raw).items():
|
||||
cleaned_id = str(channel_id).strip()
|
||||
if not cleaned_id:
|
||||
continue
|
||||
channels[cleaned_id] = _parse_channel_config(payload)
|
||||
return channels
|
||||
|
||||
|
||||
def _parse_channel_config(payload: Any) -> ChannelConfig:
|
||||
data = _as_dict(payload)
|
||||
return ChannelConfig(
|
||||
enabled=_bool(data.get("enabled"), default=False),
|
||||
kind=_string(data.get("kind")) or "",
|
||||
mode=_string(data.get("mode")) or "webhook",
|
||||
account_id=_string(data.get("accountId") or data.get("account_id")) or "",
|
||||
display_name=_string(data.get("displayName") or data.get("display_name")) or "",
|
||||
config=_normalize_config_map(data.get("config")),
|
||||
secrets=_string_dict(data.get("secrets")),
|
||||
)
|
||||
|
||||
|
||||
def _normalize_config_map(value: Any) -> dict[str, Any]:
|
||||
if not isinstance(value, dict):
|
||||
return {}
|
||||
return {
|
||||
_camel_to_snake_key(str(key)): item
|
||||
for key, item in value.items()
|
||||
if str(key).strip()
|
||||
}
|
||||
|
||||
|
||||
def _camel_to_snake_key(value: str) -> str:
|
||||
result: list[str] = []
|
||||
for char in value:
|
||||
if char.isupper() and result:
|
||||
result.append("_")
|
||||
result.append(char.lower())
|
||||
return "".join(result)
|
||||
|
||||
|
||||
def _parse_backend_identity(raw: Any) -> BackendIdentityConfig:
|
||||
data = _as_dict(raw)
|
||||
return BackendIdentityConfig(
|
||||
|
||||
@ -91,6 +91,19 @@ class AuthzConfig:
|
||||
outlook_mcp_url: str = ""
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ChannelConfig:
|
||||
"""One configured channel adapter instance."""
|
||||
|
||||
enabled: bool = False
|
||||
kind: str = ""
|
||||
mode: str = "webhook"
|
||||
account_id: str = ""
|
||||
display_name: str = ""
|
||||
config: dict[str, Any] = field(default_factory=dict)
|
||||
secrets: dict[str, str] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class BackendIdentityConfig:
|
||||
"""This backend's AuthZ client identity."""
|
||||
@ -111,6 +124,7 @@ class BeaverConfig:
|
||||
embedding: EmbeddingConfig = field(default_factory=EmbeddingConfig)
|
||||
tools: ToolsConfig = field(default_factory=ToolsConfig)
|
||||
authz: AuthzConfig = field(default_factory=AuthzConfig)
|
||||
channels: dict[str, ChannelConfig] = field(default_factory=dict)
|
||||
backend_identity: BackendIdentityConfig = field(default_factory=BackendIdentityConfig)
|
||||
config_path: Path | None = None
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
"""Event contracts and dispatch helpers."""
|
||||
|
||||
from .message_bus import InboundMessage, MessageBus, OutboundMessage
|
||||
from .message_bus import ChannelIdentity, InboundMessage, MessageBus, OutboundMessage
|
||||
|
||||
__all__ = ["InboundMessage", "MessageBus", "OutboundMessage"]
|
||||
__all__ = ["ChannelIdentity", "InboundMessage", "MessageBus", "OutboundMessage"]
|
||||
|
||||
@ -9,12 +9,58 @@ 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
|
||||
@ -35,6 +81,8 @@ class OutboundMessage:
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user