#!/usr/bin/env python3 """ Sync Agents from YAML Config to DAARION City Database This script reads config/agents_city_mapping.yaml and syncs: 1. Agents to `agents` table 2. Agent-Room bindings to `agent_room_bindings` table 3. Validates node_id against Node Registry Usage: python scripts/sync_agents_from_config.py Environment: DATABASE_URL - PostgreSQL connection string for DAARION city DB NODE_REGISTRY_URL - URL for Node Registry API (default: http://localhost:9205) """ import os import sys import yaml import logging import httpx from datetime import datetime from pathlib import Path import psycopg2 from psycopg2.extras import RealDictCursor # Setup logging logging.basicConfig( level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s' ) logger = logging.getLogger(__name__) # Configuration DATABASE_URL = os.getenv('DATABASE_URL', 'postgresql://postgres:postgres@localhost:5432/daarion') NODE_REGISTRY_URL = os.getenv('NODE_REGISTRY_URL', 'http://localhost:9205') CONFIG_PATH = Path(__file__).parent.parent / 'config' / 'agents_city_mapping.yaml' def load_config() -> dict: """Load agents configuration from YAML file.""" if not CONFIG_PATH.exists(): raise FileNotFoundError(f"Config file not found: {CONFIG_PATH}") with open(CONFIG_PATH, 'r', encoding='utf-8') as f: config = yaml.safe_load(f) logger.info(f"✅ Loaded config: {len(config.get('agents', []))} agents, {len(config.get('districts', []))} districts") return config def validate_node(node_id: str) -> bool: """Check if node exists in Node Registry.""" try: response = httpx.get(f"{NODE_REGISTRY_URL}/api/v1/nodes/{node_id}", timeout=5.0) if response.status_code == 200: logger.debug(f"✅ Node validated: {node_id}") return True else: logger.warning(f"⚠️ Node not found in registry: {node_id}") return False except Exception as e: logger.warning(f"⚠️ Cannot validate node {node_id}: {e}") return False def get_room_id_by_slug(cursor, slug: str) -> str | None: """Get room_id by slug from city_rooms.""" cursor.execute("SELECT id FROM city_rooms WHERE slug = %s", (slug,)) row = cursor.fetchone() return row['id'] if row else None def sync_agents(config: dict, conn) -> tuple[int, int, int]: """ Sync agents from config to database. Returns: Tuple of (created, updated, errors) """ cursor = conn.cursor(cursor_factory=RealDictCursor) created = 0 updated = 0 errors = 0 default_node_id = config.get('default_node_id', 'node-2-macbook-m4max') for agent in config.get('agents', []): agent_id = agent['agent_id'] node_id = agent.get('node_id', default_node_id) try: # Validate node (optional - just warning) validate_node(node_id) # Upsert agent (using 'id' as primary key, which is agent_id) cursor.execute(""" INSERT INTO agents ( id, display_name, kind, role, avatar_url, color_hint, is_active, node_id, district, primary_room_slug, model, priority, status, created_at, updated_at ) VALUES ( %(agent_id)s, %(display_name)s, %(kind)s, %(role)s, %(avatar_url)s, %(color_hint)s, true, %(node_id)s, %(district)s, %(primary_room_slug)s, %(model)s, %(priority)s, 'online', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP ) ON CONFLICT (id) DO UPDATE SET display_name = EXCLUDED.display_name, kind = EXCLUDED.kind, role = EXCLUDED.role, avatar_url = EXCLUDED.avatar_url, color_hint = EXCLUDED.color_hint, is_active = true, node_id = EXCLUDED.node_id, district = EXCLUDED.district, primary_room_slug = EXCLUDED.primary_room_slug, model = EXCLUDED.model, priority = EXCLUDED.priority, status = 'online', updated_at = CURRENT_TIMESTAMP RETURNING (xmax = 0) as is_insert """, { 'agent_id': agent_id, 'display_name': agent.get('display_name', agent_id), 'kind': agent.get('kind', 'agent'), 'role': agent.get('role', ''), 'avatar_url': agent.get('avatar_url'), 'color_hint': agent.get('color_hint', '#6366F1'), 'node_id': node_id, 'district': agent.get('district'), 'primary_room_slug': agent.get('primary_room_slug'), 'model': agent.get('model'), 'priority': agent.get('priority', 'medium'), }) result = cursor.fetchone() if result and result['is_insert']: created += 1 logger.info(f"✅ Created agent: {agent_id}") else: updated += 1 logger.debug(f"🔄 Updated agent: {agent_id}") # Create room binding room_slug = agent.get('primary_room_slug') if room_slug: room_id = get_room_id_by_slug(cursor, room_slug) if room_id: cursor.execute(""" INSERT INTO agent_room_bindings (agent_id, room_id, role, is_primary) VALUES (%(agent_id)s, %(room_id)s, 'resident', true) ON CONFLICT (agent_id, room_id) DO UPDATE SET is_primary = true, updated_at = CURRENT_TIMESTAMP """, {'agent_id': agent_id, 'room_id': room_id}) else: logger.warning(f"⚠️ Room not found for agent {agent_id}: {room_slug}") except Exception as e: errors += 1 logger.error(f"❌ Error syncing agent {agent_id}: {e}") conn.commit() return created, updated, errors def sync_districts(config: dict, conn) -> int: """Sync districts from config to database.""" cursor = conn.cursor() count = 0 for district in config.get('districts', []): try: cursor.execute(""" INSERT INTO city_districts (id, name, description, color, icon, room_slug) VALUES (%(id)s, %(name)s, %(description)s, %(color)s, %(icon)s, %(room_slug)s) ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description, color = EXCLUDED.color, icon = EXCLUDED.icon, room_slug = EXCLUDED.room_slug, updated_at = CURRENT_TIMESTAMP """, { 'id': district['id'], 'name': district['name'], 'description': district.get('description', ''), 'color': district.get('color', '#6366F1'), 'icon': district.get('icon', 'building'), 'room_slug': district.get('room_slug'), }) count += 1 except Exception as e: logger.error(f"❌ Error syncing district {district['id']}: {e}") conn.commit() logger.info(f"✅ Synced {count} districts") return count def main(): """Main entry point.""" logger.info("🚀 Starting Agent-City Sync") logger.info(f"📁 Config: {CONFIG_PATH}") logger.info(f"🗄️ Database: {DATABASE_URL.split('@')[-1] if '@' in DATABASE_URL else DATABASE_URL}") logger.info(f"📡 Node Registry: {NODE_REGISTRY_URL}") print() try: # Load config config = load_config() # Connect to database conn = psycopg2.connect(DATABASE_URL) logger.info("✅ Connected to database") # Sync districts sync_districts(config, conn) # Sync agents created, updated, errors = sync_agents(config, conn) # Summary print() logger.info("=" * 50) logger.info("📊 SYNC SUMMARY") logger.info("=" * 50) logger.info(f"✅ Agents created: {created}") logger.info(f"🔄 Agents updated: {updated}") logger.info(f"❌ Errors: {errors}") logger.info(f"📍 Total agents: {created + updated}") logger.info("=" * 50) conn.close() if errors > 0: sys.exit(1) except Exception as e: logger.error(f"❌ Fatal error: {e}") sys.exit(1) if __name__ == '__main__': main()