332 lines
9.9 KiB
Python
332 lines
9.9 KiB
Python
"""
|
||
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()
|