Files
microdao-daarion/services/router/main.py
Apple 6bd769ef40 feat(city-map): Add 2D City Map with coordinates and agent presence
- Add migration 013_city_map_coordinates.sql with map coordinates, zones, and agents table
- Add /city/map API endpoint in city-service
- Add /city/agents and /city/agents/online endpoints
- Extend presence aggregator to include agents[] in snapshot
- Add AgentsSource for fetching agent data from DB
- Create CityMap component with interactive room tiles
- Add useCityMap hook for fetching map data
- Update useGlobalPresence to include agents
- Add map/list view toggle on /city page
- Add agent badges to room cards and map tiles
2025-11-27 07:00:47 -08:00

213 lines
6.4 KiB
Python

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import Literal, Optional, Dict, Any
import asyncio
import json
import os
import yaml
app = FastAPI(title="DAARION Router", version="1.0.0")
# Configuration
NATS_URL = os.getenv("NATS_URL", "nats://nats:4222")
# NATS client
nc = None
nats_available = False
# Models
class FilterDecision(BaseModel):
channel_id: str
message_id: Optional[str] = None
matrix_event_id: str
microdao_id: str
decision: Literal["allow", "deny", "modify"]
target_agent_id: Optional[str] = None
rewrite_prompt: Optional[str] = None
class AgentInvocation(BaseModel):
agent_id: str
entrypoint: Literal["channel_message", "direct", "cron"] = "channel_message"
payload: Dict[str, Any]
# Load config
def load_config():
config_path = "router_config.yaml"
if os.path.exists(config_path):
with open(config_path, 'r') as f:
return yaml.safe_load(f)
return {
"messaging_inbound": {
"enabled": True,
"source_subject": "agent.filter.decision",
"target_subject": "router.invoke.agent"
}
}
config = load_config()
@app.on_event("startup")
async def startup_event():
"""Initialize NATS connection and subscriptions"""
global nc, nats_available
print("🚀 DAGI Router starting up...")
# Try to connect to NATS
try:
import nats
nc = await nats.connect(NATS_URL)
nats_available = True
print(f"✅ Connected to NATS at {NATS_URL}")
# Subscribe to filter decisions if enabled
if config.get("messaging_inbound", {}).get("enabled", True):
asyncio.create_task(subscribe_to_filter_decisions())
else:
print("⚠️ Messaging inbound routing disabled in config")
except Exception as e:
print(f"⚠️ NATS not available: {e}")
print("⚠️ Running in test mode (HTTP only)")
nats_available = False
async def subscribe_to_filter_decisions():
"""Subscribe to agent.filter.decision events"""
if not nc:
return
source_subject = config.get("messaging_inbound", {}).get(
"source_subject",
"agent.filter.decision"
)
try:
sub = await nc.subscribe(source_subject)
print(f"✅ Subscribed to {source_subject}")
async for msg in sub.messages:
try:
decision_data = json.loads(msg.data.decode())
await handle_filter_decision(decision_data)
except Exception as e:
print(f"❌ Error processing decision: {e}")
import traceback
traceback.print_exc()
except Exception as e:
print(f"❌ Subscription error: {e}")
async def handle_filter_decision(decision_data: dict):
"""
Process agent.filter.decision events
Only processes 'allow' decisions
Creates AgentInvocation and publishes to router.invoke.agent
"""
try:
print(f"\n🔀 Processing filter decision")
decision = FilterDecision(**decision_data)
# Only process 'allow' decisions
if decision.decision != "allow":
print(f"⏭️ Ignoring non-allow decision: {decision.decision}")
return
if not decision.target_agent_id:
print(f"⚠️ No target agent specified, skipping")
return
print(f"✅ Decision: allow")
print(f"📝 Target: {decision.target_agent_id}")
print(f"📝 Channel: {decision.channel_id}")
# Create AgentInvocation
invocation = AgentInvocation(
agent_id=decision.target_agent_id,
entrypoint="channel_message",
payload={
"channel_id": decision.channel_id,
"message_id": decision.message_id,
"matrix_event_id": decision.matrix_event_id,
"microdao_id": decision.microdao_id,
"rewrite_prompt": decision.rewrite_prompt
}
)
print(f"🚀 Created invocation for {invocation.agent_id}")
# Publish to NATS
await publish_agent_invocation(invocation)
except Exception as e:
print(f"❌ Error handling decision: {e}")
import traceback
traceback.print_exc()
async def publish_agent_invocation(invocation: AgentInvocation):
"""Publish AgentInvocation to router.invoke.agent"""
if nc and nats_available:
target_subject = config.get("messaging_inbound", {}).get(
"target_subject",
"router.invoke.agent"
)
try:
await nc.publish(target_subject, invocation.json().encode())
print(f"✅ Published invocation to {target_subject}")
except Exception as e:
print(f"❌ Error publishing to NATS: {e}")
else:
print(f"⚠️ NATS not available, invocation not published: {invocation.json()}")
@app.get("/health")
async def health():
"""Health check endpoint"""
return {
"status": "ok",
"service": "router",
"version": "1.0.0",
"nats_connected": nats_available,
"messaging_inbound_enabled": config.get("messaging_inbound", {}).get("enabled", True)
}
@app.post("/internal/router/test-messaging", response_model=AgentInvocation)
async def test_messaging_route(decision: FilterDecision):
"""
Test endpoint for routing logic
Tests filter decision → agent invocation mapping without NATS
"""
print(f"\n🧪 Test routing request")
if decision.decision != "allow" or not decision.target_agent_id:
raise HTTPException(
status_code=400,
detail=f"Decision not routable: {decision.decision}, agent: {decision.target_agent_id}"
)
invocation = AgentInvocation(
agent_id=decision.target_agent_id,
entrypoint="channel_message",
payload={
"channel_id": decision.channel_id,
"message_id": decision.message_id,
"matrix_event_id": decision.matrix_event_id,
"microdao_id": decision.microdao_id,
"rewrite_prompt": decision.rewrite_prompt
}
)
print(f"✅ Test invocation created for {invocation.agent_id}")
return invocation
@app.on_event("shutdown")
async def shutdown_event():
"""Clean shutdown"""
global nc
if nc:
await nc.close()
print("✅ NATS connection closed")