fix(fabric): use broadcast subject for NATS capabilities discovery
NATS wildcards (node.*.capabilities.get) only work for subscriptions, not for publish. Switch to a dedicated broadcast subject (fabric.capabilities.discover) that all NCS instances subscribe to, enabling proper scatter-gather discovery across nodes. Made-with: Cursor
This commit is contained in:
@@ -2,9 +2,19 @@
|
||||
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)
|
||||
"""
|
||||
|
||||
# FULL standard stack - available to ALL agents
|
||||
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",
|
||||
@@ -29,59 +39,74 @@ FULL_STANDARD_STACK = [
|
||||
|
||||
# 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
|
||||
# Specialized: energy calculations, solar/wind analysis
|
||||
"helion": ['comfy_generate_image', 'comfy_generate_video'],
|
||||
|
||||
# Alateya - R&D Lab OS
|
||||
# Specialized: experiment tracking, hypothesis testing
|
||||
"alateya": ['comfy_generate_image', 'comfy_generate_video'],
|
||||
|
||||
# Nutra - Health & Nutrition
|
||||
# Specialized: nutrition calculations, supplement analysis
|
||||
"nutra": ['comfy_generate_image', 'comfy_generate_video'],
|
||||
|
||||
# AgroMatrix - Agriculture
|
||||
# Specialized: crop analysis, weather integration, field mapping + plant intelligence
|
||||
"agromatrix": [
|
||||
'comfy_generate_image',
|
||||
'comfy_generate_video',
|
||||
'plantnet_lookup',
|
||||
'nature_id_identify',
|
||||
'gbif_species_lookup',
|
||||
'agrovoc_lookup',
|
||||
],
|
||||
"agromatrix": ['comfy_generate_image', 'comfy_generate_video'],
|
||||
|
||||
# GreenFood - Food & Eco
|
||||
# Specialized: recipe analysis, eco-scoring
|
||||
"greenfood": ['comfy_generate_image', 'comfy_generate_video'],
|
||||
|
||||
# Druid - Knowledge Search
|
||||
# Specialized: deep RAG, document comparison
|
||||
"druid": ['comfy_generate_image', 'comfy_generate_video'],
|
||||
|
||||
# DaarWizz - DAO Coordination
|
||||
# Specialized: governance tools, voting, treasury
|
||||
"daarwizz": ['comfy_generate_image', 'comfy_generate_video'],
|
||||
|
||||
# Clan - Community
|
||||
# Specialized: event management, polls, member tracking
|
||||
"clan": ['comfy_generate_image', 'comfy_generate_video'],
|
||||
|
||||
# Eonarch - Philosophy & Evolution
|
||||
# Specialized: concept mapping, timeline analysis
|
||||
"eonarch": ['comfy_generate_image', 'comfy_generate_video'],
|
||||
|
||||
# SenpAI (Gordon Senpai) - Trading & Markets
|
||||
# Specialized: real-time market data, features, signals
|
||||
"senpai": ['market_data', 'comfy_generate_image', 'comfy_generate_video'],
|
||||
|
||||
# 1OK - Window Master Assistant
|
||||
# Specialized: CRM flow, quoting, PDF docs, scheduling
|
||||
"oneok": [
|
||||
"crm_search_client",
|
||||
"crm_upsert_client",
|
||||
@@ -104,7 +129,32 @@ AGENT_SPECIALIZED_TOOLS = {
|
||||
"yaromir": ['comfy_generate_image', 'comfy_generate_video'],
|
||||
|
||||
# Sofiia - Chief AI Architect
|
||||
"sofiia": ['comfy_generate_image', 'comfy_generate_video'],
|
||||
"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',
|
||||
],
|
||||
|
||||
# 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',
|
||||
],
|
||||
|
||||
# Daarion - Media Generation
|
||||
"daarion": ['comfy_generate_image', 'comfy_generate_video'],
|
||||
@@ -150,11 +200,99 @@ AGENT_CREW_TEAMS = {
|
||||
},
|
||||
}
|
||||
|
||||
# ─── Rollout Config Loader ────────────────────────────────────────────────────
|
||||
|
||||
def get_agent_tools(agent_id: str) -> list:
|
||||
"""Get all tools for an agent: standard stack + specialized."""
|
||||
_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, [])
|
||||
return FULL_STANDARD_STACK + specialized
|
||||
|
||||
# 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:
|
||||
@@ -163,6 +301,21 @@ def is_tool_allowed(agent_id: str, tool_name: str) -> bool:
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user