""" matrix-bridge-dagi — Phase M1 scaffold Bridges Matrix/Element rooms to DAGI agents via Gateway. M1 scope: 1 room ↔ 1 agent (Sofiia), audit via sofiia-console internal endpoint. """ import logging import os import time from contextlib import asynccontextmanager from typing import Any, Dict from fastapi import FastAPI, Response from fastapi.middleware.cors import CORSMiddleware try: from prometheus_client import ( Counter, Histogram, Gauge, generate_latest, CONTENT_TYPE_LATEST, CollectorRegistry, REGISTRY, ) _PROM_OK = True except ImportError: # pragma: no cover _PROM_OK = False from .config import BridgeConfig, load_config logging.basicConfig( level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s %(message)s", ) logger = logging.getLogger("matrix-bridge-dagi") # ── Prometheus metrics ──────────────────────────────────────────────────────── if _PROM_OK: _messages_received = Counter( "matrix_bridge_messages_received_total", "Total Matrix messages received", ["room_id", "agent_id"], ) _messages_replied = Counter( "matrix_bridge_messages_replied_total", "Total agent replies sent to Matrix", ["room_id", "agent_id", "status"], ) _gateway_errors = Counter( "matrix_bridge_gateway_errors_total", "Errors calling DAGI gateway", ["error_type"], ) _invoke_latency = Histogram( "matrix_bridge_invoke_duration_seconds", "Duration of DAGI invoke call", ["agent_id"], ) _bridge_up = Gauge( "matrix_bridge_up", "1 if bridge started successfully", ) # ── Startup state ───────────────────────────────────────────────────────────── _START_TIME = time.monotonic() _cfg: BridgeConfig | None = None _config_error: str | None = None # ── Lifespan ────────────────────────────────────────────────────────────────── @asynccontextmanager async def lifespan(app_: Any): global _cfg, _config_error try: _cfg = load_config() logger.info( "✅ matrix-bridge-dagi started | node=%s build=%s homeserver=%s room=%s agents=%s", _cfg.node_id, _cfg.build_sha, _cfg.matrix_homeserver_url, _cfg.sofiia_room_id, list(_cfg.bridge_allowed_agents), ) if _PROM_OK: _bridge_up.set(1) except RuntimeError as exc: _config_error = str(exc) logger.error("❌ Config error: %s", _config_error) if _PROM_OK: _bridge_up.set(0) yield logger.info("matrix-bridge-dagi shutting down") # ── App ─────────────────────────────────────────────────────────────────────── app = FastAPI( title="matrix-bridge-dagi", version="0.1.0", lifespan=lifespan, ) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["GET", "POST"], allow_headers=["*"], ) # ── Health ──────────────────────────────────────────────────────────────────── @app.get("/health") async def health() -> Dict[str, Any]: uptime = int(time.monotonic() - _START_TIME) if _config_error or _cfg is None: return { "ok": False, "service": "matrix-bridge-dagi", "version": "0.1.0", "build": os.getenv("BUILD_SHA", "dev"), "uptime_s": uptime, "error": _config_error or "service not initialised", } return { "ok": True, "service": "matrix-bridge-dagi", "version": "0.1.0", "build": _cfg.build_sha, "build_time": _cfg.build_time, "env": os.getenv("ENV", "dev"), "uptime_s": uptime, "node_id": _cfg.node_id, "homeserver": _cfg.matrix_homeserver_url, "bridge_user": _cfg.matrix_user_id, "sofiia_room_id": _cfg.sofiia_room_id, "allowed_agents": list(_cfg.bridge_allowed_agents), "gateway": _cfg.dagi_gateway_url, "config_ok": True, } # ── Metrics ─────────────────────────────────────────────────────────────────── @app.get("/metrics") async def metrics(): if not _PROM_OK: return Response("# prometheus_client not available\n", media_type="text/plain") return Response(generate_latest(REGISTRY), media_type=CONTENT_TYPE_LATEST)