runtime: sync router/gateway/config policy and clan role registry
This commit is contained in:
245
tools/agents
245
tools/agents
@@ -15,6 +15,7 @@ import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Any, Optional
|
||||
@@ -31,6 +32,7 @@ REGISTRY_PATH = BASE_DIR / "config" / "agent_registry.yml"
|
||||
GATEWAY_DIR = BASE_DIR / "gateway-bot"
|
||||
ROUTER_CONFIG = BASE_DIR / "services" / "router" / "router-config.yml"
|
||||
CREWAI_DIR = BASE_DIR / "services" / "crewai-service" / "app"
|
||||
CREWAI_TEAMS_GENERATED = BASE_DIR / "config" / "crewai_teams.generated.yml"
|
||||
|
||||
|
||||
class Colors:
|
||||
@@ -52,6 +54,97 @@ def load_registry() -> Dict[str, Any]:
|
||||
return yaml.safe_load(f)
|
||||
|
||||
|
||||
def _slugify(value: str) -> str:
|
||||
s = (value or "").strip().lower()
|
||||
s = re.sub(r"[^a-z0-9]+", "_", s)
|
||||
s = re.sub(r"_+", "_", s).strip("_")
|
||||
return s or "role"
|
||||
|
||||
|
||||
def _legacy_crewai_to_orchestration(agent: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Backward-compatible adapter from legacy `crewai` block to new `orchestration`.
|
||||
"""
|
||||
legacy = agent.get("crewai", {}) or {}
|
||||
enabled = bool(legacy.get("enabled", False))
|
||||
orchestrator = bool(legacy.get("orchestrator", False))
|
||||
team = legacy.get("team", []) or []
|
||||
|
||||
if not enabled or not orchestrator:
|
||||
mode = "llm_only"
|
||||
elif team:
|
||||
mode = "hybrid"
|
||||
else:
|
||||
# legacy "enabled but no team" usually means orchestration via A2A only
|
||||
mode = "hybrid"
|
||||
|
||||
default_profile = {
|
||||
"team_name": f"{agent.get('display_name', agent.get('id', 'agent'))} Team",
|
||||
"parallel_roles": True,
|
||||
"max_concurrency": 3,
|
||||
"synthesis": {
|
||||
"role_context": f"{agent.get('display_name', agent.get('id', 'agent'))} Orchestrator",
|
||||
"llm_profile": agent.get("llm_profile", "reasoning"),
|
||||
},
|
||||
"team": [],
|
||||
"delegation": {
|
||||
"enabled": bool(legacy.get("can_delegate_to_all", False)),
|
||||
"forbid_self": True,
|
||||
"max_hops": 2,
|
||||
"allow_top_level_agents": [],
|
||||
},
|
||||
}
|
||||
for member in team:
|
||||
role_name = member.get("role", "") if isinstance(member, dict) else str(member)
|
||||
default_profile["team"].append(
|
||||
{
|
||||
"id": _slugify(role_name),
|
||||
"role_context": role_name,
|
||||
"llm_profile": "reasoning",
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"mode": mode,
|
||||
"crew": {
|
||||
"enabled": enabled and orchestrator,
|
||||
"default_profile": "default",
|
||||
"profiles": {"default": default_profile},
|
||||
},
|
||||
"a2a": {
|
||||
"enabled": bool(legacy.get("can_delegate_to_all", False)),
|
||||
"allow_top_level_agents": ["all_top_level"] if legacy.get("can_delegate_to_all", False) else [],
|
||||
"max_hops": 2,
|
||||
"forbid_self": True,
|
||||
},
|
||||
"response_contract": {
|
||||
"user_visible_speaker": "self",
|
||||
"crew_roles_user_visible": False,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def get_orchestration(agent: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Return normalized orchestration object.
|
||||
Prefers `orchestration`, falls back to legacy `crewai`.
|
||||
"""
|
||||
if isinstance(agent.get("orchestration"), dict):
|
||||
return agent["orchestration"]
|
||||
return _legacy_crewai_to_orchestration(agent)
|
||||
|
||||
|
||||
def get_default_profile_config(orchestration: Dict[str, Any]) -> Dict[str, Any]:
|
||||
crew = orchestration.get("crew", {}) if isinstance(orchestration, dict) else {}
|
||||
profiles = crew.get("profiles", {}) if isinstance(crew, dict) else {}
|
||||
default_profile = crew.get("default_profile", "default")
|
||||
if isinstance(profiles, dict) and default_profile in profiles:
|
||||
return profiles[default_profile] or {}
|
||||
if isinstance(profiles, dict) and "default" in profiles:
|
||||
return profiles["default"] or {}
|
||||
return {}
|
||||
|
||||
|
||||
def cmd_list(args):
|
||||
registry = load_registry()
|
||||
agents = registry.get("agents", [])
|
||||
@@ -157,10 +250,38 @@ def cmd_validate(args):
|
||||
if kw_count < 3:
|
||||
warnings.append(f"{agent_id}: Only {kw_count} routing keywords (recommend >= 3)")
|
||||
|
||||
# top_level must be CrewAI orchestrator
|
||||
crewai = agent.get("crewai", {})
|
||||
if not crewai.get("orchestrator", False):
|
||||
errors.append(f"{agent_id}: top_level agent must have crewai.orchestrator=true")
|
||||
orchestration = get_orchestration(agent)
|
||||
mode = orchestration.get("mode", "llm_only")
|
||||
crew = orchestration.get("crew", {}) if isinstance(orchestration, dict) else {}
|
||||
crew_enabled = bool(crew.get("enabled", False))
|
||||
profiles = crew.get("profiles", {}) if isinstance(crew, dict) else {}
|
||||
default_profile = crew.get("default_profile", "default")
|
||||
|
||||
if mode not in ["llm_only", "crew_only", "hybrid"]:
|
||||
errors.append(f"{agent_id}: Invalid orchestration.mode '{mode}'")
|
||||
|
||||
if mode in ["crew_only", "hybrid"] and not crew_enabled:
|
||||
errors.append(f"{agent_id}: mode={mode} requires orchestration.crew.enabled=true")
|
||||
|
||||
if crew_enabled:
|
||||
if not isinstance(profiles, dict) or not profiles:
|
||||
errors.append(f"{agent_id}: crew.enabled=true but no crew.profiles defined")
|
||||
elif default_profile not in profiles:
|
||||
errors.append(f"{agent_id}: default_profile '{default_profile}' missing in crew.profiles")
|
||||
else:
|
||||
p = profiles.get(default_profile) or {}
|
||||
team = p.get("team", []) if isinstance(p, dict) else []
|
||||
delegation = p.get("delegation", {}) if isinstance(p, dict) else {}
|
||||
# allow delegation-only orchestrators, but otherwise team must exist
|
||||
if not team and not delegation.get("enabled", False):
|
||||
errors.append(
|
||||
f"{agent_id}: default crew profile has empty team and delegation disabled "
|
||||
f"(nothing to orchestrate)"
|
||||
)
|
||||
|
||||
rc = orchestration.get("response_contract", {}) if isinstance(orchestration, dict) else {}
|
||||
if rc and rc.get("crew_roles_user_visible", False):
|
||||
errors.append(f"{agent_id}: response_contract.crew_roles_user_visible must be false")
|
||||
|
||||
if errors:
|
||||
print(f"{Colors.RED}ERRORS ({len(errors)}):{Colors.RESET}")
|
||||
@@ -257,34 +378,126 @@ def cmd_generate(args):
|
||||
"workers": [],
|
||||
"teams": {}
|
||||
}
|
||||
existing_crewai = {}
|
||||
existing_crewai_path = BASE_DIR / "config" / "crewai_agents.json"
|
||||
if existing_crewai_path.exists():
|
||||
try:
|
||||
with open(existing_crewai_path, "r", encoding="utf-8") as f:
|
||||
existing_crewai = json.load(f)
|
||||
except Exception:
|
||||
existing_crewai = {}
|
||||
|
||||
for agent in agents:
|
||||
crewai = agent.get("crewai", {})
|
||||
if crewai.get("enabled", False):
|
||||
has_explicit_orchestration = isinstance(agent.get("orchestration"), dict)
|
||||
if has_explicit_orchestration:
|
||||
orchestration = get_orchestration(agent)
|
||||
mode = orchestration.get("mode", "llm_only")
|
||||
crew = orchestration.get("crew", {}) if isinstance(orchestration, dict) else {}
|
||||
crew_enabled = bool(crew.get("enabled", False)) and mode in ["crew_only", "hybrid"]
|
||||
else:
|
||||
# Strict backward compatibility for legacy registry entries.
|
||||
legacy = agent.get("crewai", {}) or {}
|
||||
mode = "hybrid" if legacy.get("enabled", False) else "llm_only"
|
||||
crew_enabled = bool(legacy.get("enabled", False))
|
||||
|
||||
if crew_enabled:
|
||||
agent_entry = {
|
||||
"id": agent["id"],
|
||||
"display_name": agent.get("display_name"),
|
||||
"role": agent.get("canonical_role"),
|
||||
"can_orchestrate": crewai.get("orchestrator", False),
|
||||
"can_orchestrate": bool(
|
||||
get_orchestration(agent).get("mode", "llm_only") != "llm_only"
|
||||
if has_explicit_orchestration
|
||||
else (agent.get("crewai", {}) or {}).get("orchestrator", False)
|
||||
),
|
||||
"domains": agent.get("domains", []),
|
||||
}
|
||||
|
||||
if crewai.get("orchestrator"):
|
||||
|
||||
if has_explicit_orchestration:
|
||||
is_orchestrator = agent.get("class") == "top_level"
|
||||
else:
|
||||
is_orchestrator = bool((agent.get("crewai", {}) or {}).get("orchestrator", False))
|
||||
|
||||
if is_orchestrator:
|
||||
crewai_config["orchestrators"].append(agent_entry)
|
||||
else:
|
||||
crewai_config["workers"].append(agent_entry)
|
||||
|
||||
if crewai.get("team"):
|
||||
dname = agent.get('display_name', '')
|
||||
crewai_config["teams"][agent["id"]] = {
|
||||
"team_name": f"{dname} Team",
|
||||
"members": crewai["team"]
|
||||
}
|
||||
|
||||
if has_explicit_orchestration:
|
||||
orchestration = get_orchestration(agent)
|
||||
profile_cfg = get_default_profile_config(orchestration)
|
||||
team_members = profile_cfg.get("team", []) if isinstance(profile_cfg, dict) else []
|
||||
team_name = profile_cfg.get("team_name", f"{agent.get('display_name', agent['id'])} Team")
|
||||
if team_members:
|
||||
# Router needs lightweight list; keep role names for compatibility.
|
||||
members_summary = []
|
||||
for m in team_members:
|
||||
if isinstance(m, dict):
|
||||
members_summary.append(
|
||||
{
|
||||
"role": m.get("role_context", m.get("id", "role")),
|
||||
"skills": m.get("skills", []),
|
||||
}
|
||||
)
|
||||
else:
|
||||
members_summary.append({"role": str(m), "skills": []})
|
||||
crewai_config["teams"][agent["id"]] = {
|
||||
"team_name": team_name,
|
||||
"members": members_summary,
|
||||
}
|
||||
else:
|
||||
# Preserve legacy team payload (including skills) if present.
|
||||
legacy_team = (agent.get("crewai", {}) or {}).get("team", [])
|
||||
if existing_crewai.get("teams", {}).get(agent["id"]):
|
||||
# Keep pre-existing generated team shape to avoid accidental shrinking.
|
||||
crewai_config["teams"][agent["id"]] = existing_crewai["teams"][agent["id"]]
|
||||
elif legacy_team:
|
||||
crewai_config["teams"][agent["id"]] = {
|
||||
"team_name": f"{agent.get('display_name', agent['id'])} Team",
|
||||
"members": legacy_team,
|
||||
}
|
||||
|
||||
crewai_json = BASE_DIR / "config" / "crewai_agents.json"
|
||||
with open(crewai_json, "w") as f:
|
||||
json.dump(crewai_config, f, indent=2, ensure_ascii=False)
|
||||
generated_files.append(str(crewai_json))
|
||||
print(f" {Colors.GREEN}OK{Colors.RESET} {crewai_json}")
|
||||
|
||||
if flags.get("generate_crewai_teams", False):
|
||||
teams_doc = {
|
||||
"schema_version": 1,
|
||||
"version": registry.get("version", "generated"),
|
||||
"description": "Generated from config/agent_registry.yml (orchestration.crew.*)",
|
||||
}
|
||||
for agent in agents:
|
||||
if agent.get("class") != "top_level":
|
||||
continue
|
||||
# Canary-safe generation: only agents with explicit orchestration block
|
||||
# are emitted to generated teams file.
|
||||
if not isinstance(agent.get("orchestration"), dict):
|
||||
continue
|
||||
orchestration = get_orchestration(agent)
|
||||
mode = orchestration.get("mode", "llm_only")
|
||||
crew = orchestration.get("crew", {}) if isinstance(orchestration, dict) else {}
|
||||
crew_enabled = bool(crew.get("enabled", False)) and mode in ["crew_only", "hybrid"]
|
||||
if not crew_enabled:
|
||||
continue
|
||||
|
||||
profiles = crew.get("profiles", {})
|
||||
if not isinstance(profiles, dict) or not profiles:
|
||||
continue
|
||||
teams_doc[agent["id"]] = {
|
||||
"profiles": profiles,
|
||||
"default_profile": crew.get("default_profile", "default"),
|
||||
}
|
||||
hints = crew.get("profile_hints")
|
||||
if hints:
|
||||
teams_doc[agent["id"]]["profile_hints"] = hints
|
||||
|
||||
with open(CREWAI_TEAMS_GENERATED, "w") as f:
|
||||
yaml.safe_dump(teams_doc, f, sort_keys=False, allow_unicode=True)
|
||||
generated_files.append(str(CREWAI_TEAMS_GENERATED))
|
||||
print(f" {Colors.GREEN}OK{Colors.RESET} {CREWAI_TEAMS_GENERATED}")
|
||||
|
||||
print(f"\n{Colors.GREEN}Generated {len(generated_files)} files{Colors.RESET}\n")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user