feat(matrix-bridge-dagi): add mixed-room routing by slash/mention (M2.1)
- mixed_routing.py: parse BRIDGE_MIXED_ROOM_MAP, route by /slash > @mention > name: > default
- ingress.py: _try_enqueue_mixed for mixed rooms, session isolation {room}:{agent}, reply tagging
- config.py: bridge_mixed_room_map + bridge_mixed_defaults fields
- main.py: parse mixed config, pass to MatrixIngressLoop, expose in /health + /bridge/mappings
- docker-compose: BRIDGE_MIXED_ROOM_MAP / BRIDGE_MIXED_DEFAULTS env vars, BRIDGE_ALLOWED_AGENTS multi-value
- tests: 25 routing unit tests + 10 ingress integration tests (94 total pass)
Made-with: Cursor
This commit is contained in:
@@ -33,6 +33,7 @@ except ImportError: # pragma: no cover
|
||||
|
||||
from .config import BridgeConfig, load_config
|
||||
from .ingress import MatrixIngressLoop
|
||||
from .mixed_routing import MixedRoomConfig, parse_mixed_room_map
|
||||
from .rate_limit import InMemoryRateLimiter
|
||||
from .room_mapping import RoomMappingConfig, parse_room_map
|
||||
|
||||
@@ -112,6 +113,7 @@ _config_error: Optional[str] = None
|
||||
_matrix_reachable: Optional[bool] = None
|
||||
_gateway_reachable: Optional[bool] = None
|
||||
_room_map: Optional[RoomMappingConfig] = None
|
||||
_mixed_room_config: Optional[MixedRoomConfig] = None
|
||||
_rate_limiter: Optional[InMemoryRateLimiter] = None
|
||||
_ingress_loop: Optional["MatrixIngressLoop"] = None # for /health queue_size
|
||||
_ingress_task: Optional[asyncio.Task] = None
|
||||
@@ -133,16 +135,31 @@ async def _probe_url(url: str, timeout: float = 5.0) -> bool:
|
||||
@asynccontextmanager
|
||||
async def lifespan(app_: Any):
|
||||
global _cfg, _config_error, _matrix_reachable, _gateway_reachable
|
||||
global _room_map, _rate_limiter, _ingress_loop
|
||||
global _room_map, _mixed_room_config, _rate_limiter, _ingress_loop
|
||||
try:
|
||||
_cfg = load_config()
|
||||
|
||||
# Parse room mapping
|
||||
# Parse regular room mapping (M1/M2.0: 1 room → 1 agent)
|
||||
_room_map = parse_room_map(
|
||||
os.getenv("BRIDGE_ROOM_MAP", ""),
|
||||
_cfg.bridge_allowed_agents,
|
||||
)
|
||||
|
||||
# Parse mixed room mapping (M2.1: 1 room → N agents)
|
||||
if _cfg.bridge_mixed_room_map:
|
||||
_mixed_room_config = parse_mixed_room_map(
|
||||
_cfg.bridge_mixed_room_map,
|
||||
_cfg.bridge_mixed_defaults,
|
||||
_cfg.bridge_allowed_agents,
|
||||
)
|
||||
logger.info(
|
||||
"✅ Mixed room config: %d rooms, agents=%s",
|
||||
_mixed_room_config.total_rooms,
|
||||
[a for r in _mixed_room_config.rooms.values() for a in r.agents],
|
||||
)
|
||||
else:
|
||||
_mixed_room_config = None
|
||||
|
||||
# H1: Rate limiter (inmemory, per config)
|
||||
_rate_limiter = InMemoryRateLimiter(
|
||||
room_rpm=_cfg.rate_limit_room_rpm,
|
||||
@@ -153,12 +170,13 @@ async def lifespan(app_: Any):
|
||||
_cfg.rate_limit_room_rpm, _cfg.rate_limit_sender_rpm,
|
||||
)
|
||||
|
||||
mixed_count = _mixed_room_config.total_rooms if _mixed_room_config else 0
|
||||
logger.info(
|
||||
"✅ matrix-bridge-dagi started | node=%s build=%s homeserver=%s "
|
||||
"room=%s agents=%s mappings=%d",
|
||||
"agents=%s mappings=%d mixed_rooms=%d",
|
||||
_cfg.node_id, _cfg.build_sha, _cfg.matrix_homeserver_url,
|
||||
_cfg.sofiia_room_id, list(_cfg.bridge_allowed_agents),
|
||||
_room_map.total_mappings,
|
||||
list(_cfg.bridge_allowed_agents),
|
||||
_room_map.total_mappings, mixed_count,
|
||||
)
|
||||
|
||||
# Connectivity smoke probes (non-blocking failures)
|
||||
@@ -180,7 +198,10 @@ async def lifespan(app_: Any):
|
||||
_bridge_up.set(1)
|
||||
|
||||
# Start ingress loop (fire-and-forget asyncio task)
|
||||
if _room_map and _room_map.total_mappings > 0:
|
||||
_has_rooms = (_room_map and _room_map.total_mappings > 0) or (
|
||||
_mixed_room_config and _mixed_room_config.total_rooms > 0
|
||||
)
|
||||
if _has_rooms:
|
||||
_ingress_stop = asyncio.Event()
|
||||
|
||||
def _on_msg(room_id: str, agent_id: str) -> None:
|
||||
@@ -241,6 +262,7 @@ async def lifespan(app_: Any):
|
||||
queue_max_events=_cfg.queue_max_events,
|
||||
worker_concurrency=_cfg.worker_concurrency,
|
||||
queue_drain_timeout_s=_cfg.queue_drain_timeout_s,
|
||||
mixed_room_config=_mixed_room_config,
|
||||
on_message_received=_on_msg,
|
||||
on_message_replied=_on_replied,
|
||||
on_gateway_error=_on_gw_error,
|
||||
@@ -328,6 +350,7 @@ async def health() -> Dict[str, Any]:
|
||||
"gateway": _cfg.dagi_gateway_url,
|
||||
"gateway_reachable": _gateway_reachable,
|
||||
"mappings_count": _room_map.total_mappings if _room_map else 0,
|
||||
"mixed_rooms_count": _mixed_room_config.total_rooms if _mixed_room_config else 0,
|
||||
"config_ok": True,
|
||||
"rate_limiter": _rate_limiter.stats() if _rate_limiter else None,
|
||||
"queue": {
|
||||
@@ -350,12 +373,15 @@ async def bridge_mappings() -> Dict[str, Any]:
|
||||
"ok": False,
|
||||
"error": _config_error or "service not initialised",
|
||||
"mappings": [],
|
||||
"mixed_rooms": [],
|
||||
}
|
||||
return {
|
||||
"ok": True,
|
||||
"total": _room_map.total_mappings,
|
||||
"allowed_agents": list(_cfg.bridge_allowed_agents),
|
||||
"mappings": _room_map.as_summary(),
|
||||
"mixed_rooms_total": _mixed_room_config.total_rooms if _mixed_room_config else 0,
|
||||
"mixed_rooms": _mixed_room_config.as_summary() if _mixed_room_config else [],
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user