""" Per-agent tool configuration. All agents have FULL standard stack + specialized tools. Each agent is a platform with own site, channels, database, users. v2: Supports default_tools merge policy via tools_rollout.yml config. Effective tools = unique(DEFAULT_TOOLS_BY_ROLE ∪ agent.tools ∪ agent.capability_tools) """ import os import logging from pathlib import Path from typing import List, Optional logger = logging.getLogger(__name__) # FULL standard stack - available to ALL agents (legacy explicit list, kept for compatibility) FULL_STANDARD_STACK = [ # Search & Knowledge (Priority 1) "memory_search", "graph_query", # Web Research (Priority 2) "web_search", "web_extract", "crawl4ai_scrape", # Memory "remember_fact", # Content Generation "image_generate", "tts_speak", # Presentations "presentation_create", "presentation_status", "presentation_download", # File artifacts "file_tool", # Repo Tool (read-only filesystem) "repo_tool", # PR Reviewer Tool "pr_reviewer_tool", # Contract Tool (OpenAPI/JSON Schema) "contract_tool", # Oncall/Runbook Tool "oncall_tool", # Observability Tool "observability_tool", # Config Linter Tool (secrets, policy) "config_linter_tool", # ThreatModel Tool (security analysis) "threatmodel_tool", # Job Orchestrator Tool (ops tasks) "job_orchestrator_tool", # Knowledge Base Tool (ADR, docs, runbooks) "kb_tool", # Drift Analyzer Tool (service/openapi/nats/tools drift) "drift_analyzer_tool", # Pieces OS integration "pieces_tool", ] # Specialized tools per agent (on top of standard stack) AGENT_SPECIALIZED_TOOLS = { # Helion - Energy platform "helion": ['comfy_generate_image', 'comfy_generate_video'], # Alateya - R&D Lab OS "alateya": ['comfy_generate_image', 'comfy_generate_video'], # Nutra - Health & Nutrition "nutra": ['comfy_generate_image', 'comfy_generate_video'], # AgroMatrix - Agriculture "agromatrix": ['comfy_generate_image', 'comfy_generate_video'], # GreenFood - Food & Eco "greenfood": ['comfy_generate_image', 'comfy_generate_video'], # Druid - Knowledge Search "druid": ['comfy_generate_image', 'comfy_generate_video'], # DaarWizz - DAO Coordination "daarwizz": ['comfy_generate_image', 'comfy_generate_video'], # Clan - Community "clan": ['comfy_generate_image', 'comfy_generate_video'], # Eonarch - Philosophy & Evolution "eonarch": ['comfy_generate_image', 'comfy_generate_video'], # SenpAI (Gordon Senpai) - Trading & Markets "senpai": ['market_data', 'comfy_generate_image', 'comfy_generate_video'], # 1OK - Window Master Assistant "oneok": [ "crm_search_client", "crm_upsert_client", "crm_upsert_site", "crm_upsert_window_unit", "crm_create_quote", "crm_update_quote", "crm_create_job", "calc_window_quote", "docs_render_quote_pdf", "docs_render_invoice_pdf", "schedule_propose_slots", "schedule_confirm_slot", ], # Soul / Athena - Spiritual Mentor "soul": ['comfy_generate_image', 'comfy_generate_video'], # Yaromir - Tech Lead "yaromir": ['comfy_generate_image', 'comfy_generate_video'], # Sofiia - Chief AI Architect "sofiia": [ 'comfy_generate_image', 'comfy_generate_video', 'risk_engine_tool', 'architecture_pressure_tool', 'backlog_tool', 'job_orchestrator_tool', 'dependency_scanner_tool', 'incident_intelligence_tool', 'cost_analyzer_tool', 'pieces_tool', 'notion_tool', 'calendar_tool', 'agent_email_tool', 'browser_tool', 'safe_code_executor_tool', 'secure_vault_tool', ], # Admin - platform operations "admin": [ 'risk_engine_tool', 'architecture_pressure_tool', 'backlog_tool', 'job_orchestrator_tool', 'dependency_scanner_tool', 'incident_intelligence_tool', 'cost_analyzer_tool', 'pieces_tool', 'notion_tool', 'calendar_tool', 'agent_email_tool', 'browser_tool', 'safe_code_executor_tool', 'secure_vault_tool', ], # Daarion - Media Generation "daarion": ['comfy_generate_image', 'comfy_generate_video'], } # CrewAI team structure per agent (future implementation) AGENT_CREW_TEAMS = { "helion": { "team_name": "Energy Specialists", "agents": ["analyst", "engineer", "market_researcher", "communicator"] }, "alateya": { "team_name": "Research Professors", "agents": ["prof_erudite", "prof_analyst", "prof_creative", "prof_optimizer", "prof_communicator"] }, "nutra": { "team_name": "Health Advisors", "agents": ["nutritionist", "biochemist", "fitness_coach", "communicator"] }, "agromatrix": { "team_name": "Agro Experts", "agents": ["agronomist", "soil_specialist", "weather_analyst", "market_analyst"] }, "greenfood": { "team_name": "Food & Eco Team", "agents": ["chef", "nutritionist", "eco_analyst", "supply_chain"] }, "druid": { "team_name": "Knowledge Seekers", "agents": ["researcher", "fact_checker", "synthesizer", "archivist"] }, "daarwizz": { "team_name": "DAO Operations", "agents": ["governance", "treasury", "community", "tech_ops"] }, "clan": { "team_name": "Community Spirits", "agents": ["welcomer", "mediator", "event_organizer", "historian"] }, "eonarch": { "team_name": "Consciousness Guides", "agents": ["philosopher", "futurist", "integrator", "storyteller"] }, } # ─── Rollout Config Loader ──────────────────────────────────────────────────── _rollout_config = None _rollout_loaded = False def _load_rollout_config() -> dict: """Load tools_rollout.yml, cache on first call.""" global _rollout_config, _rollout_loaded if _rollout_loaded: return _rollout_config or {} config_path = Path(__file__).parent.parent.parent / "config" / "tools_rollout.yml" try: import yaml with open(config_path, "r") as f: _rollout_config = yaml.safe_load(f) or {} logger.debug(f"Loaded tools_rollout.yml: {list(_rollout_config.keys())}") except Exception as e: logger.warning(f"Could not load tools_rollout.yml: {e}. Using legacy config.") _rollout_config = {} finally: _rollout_loaded = True return _rollout_config def _expand_group(group_ref: str, config: dict, seen: Optional[set] = None) -> List[str]: """Expand @group_name reference recursively. Prevents circular refs.""" if seen is None: seen = set() if group_ref.startswith("@"): group_name = group_ref[1:] if group_name in seen: logger.warning(f"Circular group reference: {group_name}") return [] seen.add(group_name) group_tools = config.get(group_name, []) result = [] for item in group_tools: result.extend(_expand_group(item, config, seen)) return result else: return [group_ref] def _get_role_tools(agent_id: str, config: dict) -> List[str]: """Get tools for agent's role via rollout config.""" agent_roles = config.get("agent_roles", {}) role = agent_roles.get(agent_id, "agent_default") role_map = config.get("role_map", {}) role_config = role_map.get(role, role_map.get("agent_default", {})) role_tool_refs = role_config.get("tools", []) tools = [] for ref in role_tool_refs: tools.extend(_expand_group(ref, config)) return tools def get_agent_tools(agent_id: str) -> List[str]: """ Get all tools for an agent using merge policy: effective_tools = unique(DEFAULT_TOOLS_BY_ROLE ∪ FULL_STANDARD_STACK ∪ agent.specialized_tools) - First try rollout config for role-based tools. - Always union with FULL_STANDARD_STACK for backward compat. - Always add agent-specific specialized tools. - Stable order: role_tools → standard_stack → specialized (deduped). """ rollout = _load_rollout_config() # 1. Role-based default tools (from rollout config) role_tools = _get_role_tools(agent_id, rollout) if rollout else [] # 2. Legacy full standard stack (guaranteed baseline) standard_tools = list(FULL_STANDARD_STACK) # 3. Agent-specific specialized tools specialized = AGENT_SPECIALIZED_TOOLS.get(agent_id, []) # Merge with stable order, deduplicate preserving first occurrence merged = [] seen = set() for tool in role_tools + standard_tools + specialized: if tool not in seen: merged.append(tool) seen.add(tool) logger.debug(f"Agent '{agent_id}' effective tools ({len(merged)}): {merged[:10]}...") return merged def is_tool_allowed(agent_id: str, tool_name: str) -> bool: """Check if a tool is allowed for an agent.""" allowed = get_agent_tools(agent_id) return tool_name in allowed def get_agent_role(agent_id: str) -> str: """Get the role assigned to an agent via rollout config.""" rollout = _load_rollout_config() agent_roles = rollout.get("agent_roles", {}) return agent_roles.get(agent_id, "agent_default") def get_agent_crew(agent_id: str) -> dict: """Get CrewAI team configuration for an agent.""" return AGENT_CREW_TEAMS.get(agent_id, {"team_name": "Default", "agents": []}) def reload_rollout_config(): """Force reload of tools_rollout.yml (for hot-reload/testing).""" global _rollout_config, _rollout_loaded _rollout_config = None _rollout_loaded = False return _load_rollout_config()