""" CrewAI Registry Loader - Variant A (Profiles per Agent) Loads team configurations from crewai_teams.yml with profile support. """ import os import json import yaml import logging from pathlib import Path from typing import Dict, Any, List logger = logging.getLogger(__name__) CREWAI_AGENTS_PATH = os.getenv("CREWAI_AGENTS_PATH", "/app/config/crewai_agents.json") CREWAI_TEAMS_PATH = os.getenv("CREWAI_TEAMS_PATH", "/app/config/crewai_teams.yml") CREWAI_TEAMS_GENERATED_PATH = os.getenv("CREWAI_TEAMS_GENERATED_PATH", "/app/config/crewai_teams.generated.yml") ROLES_BASE_PATH = os.getenv("ROLES_BASE_PATH", "/app/config/roles") # Example: "agromatrix=agx" means refs under agromatrix/* also try agx/* ROLE_NAMESPACE_OVERRIDES_RAW = os.getenv("ROLE_NAMESPACE_OVERRIDES", "agromatrix=agx") ROLE_NAMESPACE_OVERRIDES = {} for part in ROLE_NAMESPACE_OVERRIDES_RAW.split(","): part = part.strip() if not part or "=" not in part: continue src, dst = part.split("=", 1) src = src.strip() dst = dst.strip() if src and dst: ROLE_NAMESPACE_OVERRIDES[src] = dst _teams_config = None _agents_config = None def _normalize_prompt_ref(prompt_ref: str) -> str: ref = (prompt_ref or "").strip().lstrip("/") while ref.startswith("roles/"): ref = ref[len("roles/"):] return ref def _build_prompt_candidates(prompt_ref: str) -> List[Path]: base = Path(ROLES_BASE_PATH) ref = _normalize_prompt_ref(prompt_ref) candidates: List[Path] = [] if ref: candidates.append(base / ref) parts = ref.split("/", 1) if parts and parts[0] in ROLE_NAMESPACE_OVERRIDES and len(parts) > 1: mapped = f"{ROLE_NAMESPACE_OVERRIDES[parts[0]]}/{parts[1]}" candidates.append(base / mapped) # Legacy fallback (in case a ref is already relative but not under roles/*) raw_ref = (prompt_ref or "").strip().lstrip("/") if raw_ref and raw_ref != ref: candidates.append(base / raw_ref) # Deduplicate while preserving order unique: List[Path] = [] seen = set() for p in candidates: k = str(p) if k in seen: continue seen.add(k) unique.append(p) return unique def resolve_prompt_path(prompt_ref: str) -> Path: for candidate in _build_prompt_candidates(prompt_ref): if candidate.exists(): return candidate return None def load_agents_config(): """Load basic agent config from crewai_agents.json""" global _agents_config if _agents_config is None: try: with open(CREWAI_AGENTS_PATH, "r") as f: _agents_config = json.load(f) logger.info(f"Loaded agents config from {CREWAI_AGENTS_PATH}") except Exception as e: logger.error(f"Failed to load agents config: {e}") _agents_config = {} return _agents_config def load_teams_config(): """Load teams/profiles config with generated-over-legacy merge.""" global _teams_config if _teams_config is None: try: with open(CREWAI_TEAMS_PATH, "r") as f: legacy = yaml.safe_load(f) or {} merged = dict(legacy) version = merged.get("version", "unknown") logger.info(f"Loaded legacy teams config v{version} from {CREWAI_TEAMS_PATH}") generated = {} gen_path = Path(CREWAI_TEAMS_GENERATED_PATH) if gen_path.exists(): with open(gen_path, "r") as f: generated = yaml.safe_load(f) or {} logger.info(f"Loaded generated teams config from {CREWAI_TEAMS_GENERATED_PATH}") # Merge strategy: generated overrides legacy for same (agent, profile). # Missing agents/profiles continue to work from legacy file. if generated: skip_keys = {"schema_version", "version", "description"} for key, val in generated.items(): if key in skip_keys: continue if not isinstance(val, dict): merged[key] = val continue legacy_agent = merged.get(key, {}) if not isinstance(legacy_agent, dict): legacy_agent = {} merged_agent = dict(legacy_agent) gen_profiles = val.get("profiles", {}) if isinstance(gen_profiles, dict): legacy_profiles = legacy_agent.get("profiles", {}) if not isinstance(legacy_profiles, dict): legacy_profiles = {} combined_profiles = dict(legacy_profiles) for profile_name, profile_cfg in gen_profiles.items(): if profile_name in combined_profiles: logger.info( f"Generated teams override legacy profile: {key}.{profile_name}" ) combined_profiles[profile_name] = profile_cfg merged_agent["profiles"] = combined_profiles if "default_profile" in val: merged_agent["default_profile"] = val["default_profile"] if "profile_hints" in val: merged_agent["profile_hints"] = val["profile_hints"] merged[key] = merged_agent _teams_config = merged merged_version = _teams_config.get("version", "unknown") logger.info(f"Effective teams config v{merged_version} loaded (legacy+generated merge)") except Exception as e: logger.error(f"Failed to load teams config: {e}") _teams_config = {} return _teams_config def load_role_prompt(prompt_ref: str) -> str: """Load role prompt from .md file with normalized path resolution.""" if not prompt_ref: return "" resolved = resolve_prompt_path(prompt_ref) if not resolved: tried = ", ".join(str(p) for p in _build_prompt_candidates(prompt_ref)) logger.warning(f"Role prompt not found: ref={prompt_ref}; tried=[{tried}]") return f"# Role: {prompt_ref}\n(prompt file missing)" try: return resolved.read_text(encoding="utf-8") except Exception as e: logger.error(f"Error loading role prompt {resolved}: {e}") return "" def validate_required_prompts(strict: bool = False) -> Dict[str, Any]: """Validate all team/synthesis prompt refs are resolvable.""" config = load_teams_config() missing = [] skip_keys = {"schema_version", "version", "description"} for agent_id, agent_cfg in config.items(): if agent_id in skip_keys or not isinstance(agent_cfg, dict): continue profiles = agent_cfg.get("profiles", {}) if not isinstance(profiles, dict): continue for profile_name, profile_cfg in profiles.items(): if not isinstance(profile_cfg, dict): continue synthesis = profile_cfg.get("synthesis", {}) or {} synth_ref = synthesis.get("system_prompt_ref", "") if synth_ref and not resolve_prompt_path(synth_ref): missing.append(f"{agent_id}.{profile_name}.synthesis -> {synth_ref}") for member in profile_cfg.get("team", []) or []: ref = (member or {}).get("system_prompt_ref", "") if ref and not resolve_prompt_path(ref): mid = (member or {}).get("id", "unknown") missing.append(f"{agent_id}.{profile_name}.{mid} -> {ref}") if missing: msg = f"Missing CrewAI role prompts: {len(missing)}" if strict: sample = "; ".join(missing[:8]) raise RuntimeError(f"{msg}. Examples: {sample}") logger.warning(f"{msg}. Examples: {'; '.join(missing[:8])}") return { "missing_count": len(missing), "missing": missing, } def get_agent_profiles(agent_id: str) -> list: """Get list of available profiles for an agent""" config = load_teams_config() agent_cfg = config.get(agent_id, {}) profiles = agent_cfg.get("profiles", {}) return list(profiles.keys()) def get_default_profile(agent_id: str) -> str: """Get default profile name for an agent""" config = load_teams_config() agent_cfg = config.get(agent_id, {}) return agent_cfg.get("default_profile", "default") def get_profile_hints(agent_id: str) -> dict: """Get profile selection hints (keywords) for an agent""" config = load_teams_config() agent_cfg = config.get(agent_id, {}) return agent_cfg.get("profile_hints", {}) def select_profile(agent_id: str, prompt: str) -> str: """Select appropriate profile based on prompt keywords""" hints = get_profile_hints(agent_id) prompt_lower = prompt.lower() for profile_name, keywords in hints.items(): for kw in keywords: if kw.lower() in prompt_lower: logger.info(f"Selected profile {profile_name} for {agent_id} (matched: {kw})") return profile_name return get_default_profile(agent_id) def get_profile_config(agent_id: str, profile: str = None) -> dict: """Get full profile configuration for an agent""" config = load_teams_config() agent_cfg = config.get(agent_id, {}) profiles = agent_cfg.get("profiles", {}) if profile is None: profile = get_default_profile(agent_id) return profiles.get(profile, {}) def get_team_members(agent_id: str, profile: str = None) -> list: """Get team members with resolved prompts""" profile_cfg = get_profile_config(agent_id, profile) team = profile_cfg.get("team", []) resolved = [] for member in team: resolved_member = dict(member) prompt_ref = member.get("system_prompt_ref", "") resolved_member["system_prompt"] = load_role_prompt(prompt_ref) resolved.append(resolved_member) return resolved def get_synthesis_config(agent_id: str, profile: str = None) -> dict: """Get synthesis config with resolved prompt""" profile_cfg = get_profile_config(agent_id, profile) synthesis = profile_cfg.get("synthesis", {}) if synthesis: prompt_ref = synthesis.get("system_prompt_ref", "") synthesis = dict(synthesis) synthesis["system_prompt"] = load_role_prompt(prompt_ref) return synthesis def get_team_settings(agent_id: str, profile: str = None) -> dict: """Get team execution settings""" profile_cfg = get_profile_config(agent_id, profile) return { "team_name": profile_cfg.get("team_name", f"{agent_id} team"), "parallel_roles": profile_cfg.get("parallel_roles", True), "max_concurrency": profile_cfg.get("max_concurrency", 3) } def get_delegation_config(agent_id: str, profile: str = None) -> dict: """Get delegation config for an agent""" profile_cfg = get_profile_config(agent_id, profile) return profile_cfg.get("delegation", {"enabled": False}) def can_delegate_to(agent_id: str, target_agent_id: str, profile: str = None) -> bool: """Check if agent can delegate to target""" deleg = get_delegation_config(agent_id, profile) if not deleg.get("enabled"): return False if deleg.get("forbid_self") and target_agent_id == agent_id: return False allowed = deleg.get("allow_top_level_agents", []) return target_agent_id in allowed def is_orchestrator(agent_id: str) -> bool: """Check if agent has orchestrator capability""" config = load_teams_config() return agent_id in config and "profiles" in config.get(agent_id, {}) def get_all_agents_summary() -> dict: """Get summary of all agents and their profiles""" config = load_teams_config() summary = {} skip_keys = ["schema_version", "version", "description"] for agent_id, agent_cfg in config.items(): if agent_id in skip_keys: continue if not isinstance(agent_cfg, dict): continue profiles = agent_cfg.get("profiles", {}) summary[agent_id] = { "profiles": list(profiles.keys()), "default_profile": agent_cfg.get("default_profile", "default"), "has_hints": bool(agent_cfg.get("profile_hints")) } # Add role counts per profile for pname, pcfg in profiles.items(): summary[agent_id][f"{pname}_roles"] = len(pcfg.get("team", [])) return summary