Complete snapshot of /opt/microdao-daarion/ from NODE1 (144.76.224.179).
This represents the actual running production code that has diverged
significantly from the previous main branch.
Key changes from old main:
- Gateway (http_api.py): expanded from ~40KB to 164KB with full agent support
- Router: new /v1/agents/{id}/infer endpoint with vision + DeepSeek routing
- Behavior Policy: SOWA v2.2 (3-level: FULL/ACK/SILENT)
- Agent Registry: config/agent_registry.yml as single source of truth
- 13 agents configured (was 3)
- Memory service integration
- CrewAI teams and roles
Excluded from snapshot: venv/, .env, data/, backups, .tgz archives
Co-authored-by: Cursor <cursoragent@cursor.com>
372 lines
12 KiB
Python
372 lines
12 KiB
Python
"""
|
|
CrewAI Service - Canonical Multi-Role Orchestration v2.0
|
|
Variant A: Profiles per top-level agent
|
|
"""
|
|
import os
|
|
import json
|
|
import time
|
|
import asyncio
|
|
import logging
|
|
import httpx
|
|
from typing import Dict, Any, List, Optional
|
|
from fastapi import FastAPI, HTTPException
|
|
from pydantic import BaseModel
|
|
|
|
# Setup logging
|
|
logging.basicConfig(level=logging.INFO)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Import registry loader (v2 with profiles)
|
|
from registry_loader import (
|
|
load_teams_config,
|
|
get_team_members, get_synthesis_config, get_delegation_config,
|
|
get_team_settings, can_delegate_to, is_orchestrator,
|
|
get_agent_profiles, get_default_profile, select_profile,
|
|
get_profile_config, get_all_agents_summary
|
|
)
|
|
|
|
app = FastAPI(title="CrewAI Service", version="2.0.0")
|
|
|
|
# Configuration
|
|
ROUTER_URL = os.getenv("ROUTER_URL", "http://dagi-staging-router:8000")
|
|
DEFAULT_MAX_CONCURRENCY = int(os.getenv("MAX_CONCURRENT_ROLES", "3"))
|
|
LLM_TIMEOUT = int(os.getenv("LLM_TIMEOUT", "120"))
|
|
|
|
|
|
# Request/Response models
|
|
class CrewRunRequest(BaseModel):
|
|
orchestrator_id: str
|
|
task: str
|
|
context: Dict[str, Any] = {}
|
|
profile: Optional[str] = None # NEW: explicit profile selection
|
|
|
|
class CrewRunResponse(BaseModel):
|
|
success: bool
|
|
result: Optional[str] = None
|
|
error: Optional[str] = None
|
|
meta: Dict[str, Any] = {}
|
|
|
|
class DelegationRequest(BaseModel):
|
|
orchestrator_id: str
|
|
target_agent_id: str
|
|
task: str
|
|
context: Dict[str, Any] = {}
|
|
hops_remaining: int = 2
|
|
|
|
|
|
async def call_internal_llm(
|
|
prompt: str,
|
|
system_prompt: str = None,
|
|
role_context: str = None,
|
|
llm_profile: str = "reasoning"
|
|
) -> str:
|
|
"""Call Router internal LLM endpoint for a single role"""
|
|
url = f"{ROUTER_URL}/internal/llm/complete"
|
|
|
|
payload = {
|
|
"prompt": prompt,
|
|
"llm_profile": llm_profile,
|
|
"max_tokens": 2048,
|
|
"temperature": 0.3
|
|
}
|
|
if system_prompt:
|
|
payload["system_prompt"] = system_prompt
|
|
if role_context:
|
|
payload["role_context"] = role_context
|
|
|
|
async with httpx.AsyncClient(timeout=LLM_TIMEOUT) as client:
|
|
try:
|
|
resp = await client.post(url, json=payload)
|
|
resp.raise_for_status()
|
|
data = resp.json()
|
|
return data.get("text", "")
|
|
except Exception as e:
|
|
logger.error(f"Internal LLM call failed: {e}")
|
|
raise
|
|
|
|
|
|
async def delegate_to_agent(
|
|
orchestrator_id: str,
|
|
target_agent_id: str,
|
|
task: str,
|
|
context: Dict[str, Any] = {},
|
|
hops_remaining: int = 2,
|
|
profile: str = None
|
|
) -> Optional[str]:
|
|
"""Delegate task to another top-level agent via Router"""
|
|
if not can_delegate_to(orchestrator_id, target_agent_id, profile):
|
|
logger.warning(f"Delegation not allowed: {orchestrator_id} -> {target_agent_id}")
|
|
return None
|
|
|
|
if hops_remaining <= 0:
|
|
logger.warning(f"Max delegation hops reached for {orchestrator_id}")
|
|
return None
|
|
|
|
url = f"{ROUTER_URL}/v1/agents/{target_agent_id}/infer"
|
|
|
|
delegation_cfg = get_delegation_config(orchestrator_id, profile) or {}
|
|
attach_headers = delegation_cfg.get("attach_headers", {})
|
|
|
|
payload = {
|
|
"prompt": task,
|
|
"metadata": {
|
|
"handoff_from": attach_headers.get("handoff_from", orchestrator_id),
|
|
"hops_remaining": hops_remaining - 1,
|
|
}
|
|
}
|
|
|
|
logger.info(f"DELEGATION: {orchestrator_id} -> {target_agent_id} (hops={hops_remaining})")
|
|
|
|
async with httpx.AsyncClient(timeout=180) as client:
|
|
try:
|
|
resp = await client.post(url, json=payload)
|
|
resp.raise_for_status()
|
|
data = resp.json()
|
|
return data.get("response", "")
|
|
except Exception as e:
|
|
logger.error(f"Delegation to {target_agent_id} failed: {e}")
|
|
return None
|
|
|
|
|
|
async def execute_role(
|
|
role_config: Dict[str, Any],
|
|
task: str,
|
|
context: Dict[str, Any],
|
|
semaphore: asyncio.Semaphore
|
|
) -> Dict[str, Any]:
|
|
"""Execute a single role with rate limiting"""
|
|
role_id = role_config.get("id", "unknown")
|
|
role_context = role_config.get("role_context", role_id)
|
|
llm_profile = role_config.get("llm_profile", "reasoning")
|
|
system_prompt = role_config.get("system_prompt", "")
|
|
|
|
memory_brief = context.get("memory_brief", {})
|
|
memory_str = json.dumps(memory_brief, ensure_ascii=False)[:500] if memory_brief else ""
|
|
|
|
prompt = f"Task: {task}\n\nContext: {memory_str}\n\nYour role: {role_context}\n\nProvide your analysis and recommendations."
|
|
|
|
async with semaphore:
|
|
t0 = time.time()
|
|
try:
|
|
logger.info(f"ROLE START: {role_context} (profile={llm_profile})")
|
|
result = await call_internal_llm(
|
|
prompt=prompt,
|
|
system_prompt=system_prompt,
|
|
role_context=role_context,
|
|
llm_profile=llm_profile
|
|
)
|
|
elapsed = time.time() - t0
|
|
logger.info(f"ROLE DONE: {role_context} ({elapsed:.1f}s)")
|
|
return {
|
|
"role_id": role_id,
|
|
"role_context": role_context,
|
|
"result": result,
|
|
"elapsed_seconds": elapsed,
|
|
"success": True
|
|
}
|
|
except Exception as e:
|
|
elapsed = time.time() - t0
|
|
logger.error(f"ROLE ERROR: {role_context}: {e}")
|
|
return {
|
|
"role_id": role_id,
|
|
"role_context": role_context,
|
|
"result": None,
|
|
"error": str(e),
|
|
"elapsed_seconds": elapsed,
|
|
"success": False
|
|
}
|
|
|
|
|
|
async def run_crew_canonical(
|
|
orchestrator_id: str,
|
|
task: str,
|
|
context: Dict[str, Any],
|
|
profile: str = None
|
|
) -> CrewRunResponse:
|
|
"""Execute multi-role orchestration for an agent with profile selection"""
|
|
|
|
# Select profile (auto or explicit)
|
|
if profile is None:
|
|
profile = select_profile(orchestrator_id, task)
|
|
|
|
logger.info(f"CREW RUN: {orchestrator_id} profile={profile}")
|
|
|
|
# Get team config for selected profile
|
|
team_members = get_team_members(orchestrator_id, profile)
|
|
synthesis_config = get_synthesis_config(orchestrator_id, profile)
|
|
team_settings = get_team_settings(orchestrator_id, profile)
|
|
|
|
if not team_members:
|
|
return CrewRunResponse(
|
|
success=False,
|
|
error=f"No team members for {orchestrator_id}.{profile}",
|
|
meta={"orchestrator_id": orchestrator_id, "profile": profile}
|
|
)
|
|
|
|
team_name = team_settings.get("team_name", f"{orchestrator_id} team")
|
|
parallel_roles = team_settings.get("parallel_roles", True)
|
|
max_concurrency = team_settings.get("max_concurrency", DEFAULT_MAX_CONCURRENCY)
|
|
|
|
logger.info(f"Team: {team_name}, Roles: {len(team_members)}, Parallel: {parallel_roles}, MaxConc: {max_concurrency}")
|
|
|
|
t0 = time.time()
|
|
role_results = []
|
|
|
|
if parallel_roles:
|
|
# Parallel execution with semaphore
|
|
semaphore = asyncio.Semaphore(max_concurrency)
|
|
tasks = [
|
|
execute_role(member, task, context, semaphore)
|
|
for member in team_members
|
|
]
|
|
role_results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
role_results = [r if isinstance(r, dict) else {"error": str(r), "success": False} for r in role_results]
|
|
else:
|
|
# Sequential execution
|
|
semaphore = asyncio.Semaphore(1)
|
|
for member in team_members:
|
|
res = await execute_role(member, task, context, semaphore)
|
|
role_results.append(res)
|
|
|
|
# Build synthesis context
|
|
successful_results = [r for r in role_results if r.get("success")]
|
|
failed_results = [r for r in role_results if not r.get("success")]
|
|
|
|
synthesis_context = "\n\n".join([
|
|
f"## {r['role_context']}\n{r['result']}"
|
|
for r in successful_results
|
|
])
|
|
|
|
# Synthesis
|
|
synthesis_prompt = synthesis_config.get("system_prompt", "")
|
|
synthesis_role = synthesis_config.get("role_context", "Synthesis")
|
|
synthesis_llm = synthesis_config.get("llm_profile", "reasoning")
|
|
|
|
final_prompt = f"""Task: {task}
|
|
|
|
Team Analysis:
|
|
{synthesis_context}
|
|
|
|
Synthesize the above into a coherent, actionable response. Include:
|
|
- Key findings
|
|
- Recommendations
|
|
- Risks/limitations
|
|
- Next steps"""
|
|
|
|
try:
|
|
final_result = await call_internal_llm(
|
|
prompt=final_prompt,
|
|
system_prompt=synthesis_prompt,
|
|
role_context=synthesis_role,
|
|
llm_profile=synthesis_llm
|
|
)
|
|
except Exception as e:
|
|
final_result = f"Synthesis failed: {e}\n\nRaw team results:\n{synthesis_context}"
|
|
|
|
elapsed = time.time() - t0
|
|
|
|
return CrewRunResponse(
|
|
success=True,
|
|
result=final_result,
|
|
meta={
|
|
"orchestrator_id": orchestrator_id,
|
|
"profile": profile,
|
|
"team_name": team_name,
|
|
"roles_count": len(team_members),
|
|
"roles_success": len(successful_results),
|
|
"roles_failed": len(failed_results),
|
|
"elapsed_seconds": round(elapsed, 2),
|
|
"parallel": parallel_roles
|
|
}
|
|
)
|
|
|
|
|
|
# === ENDPOINTS ===
|
|
|
|
@app.get("/health")
|
|
async def health():
|
|
return {"status": "ok", "version": "2.0.0", "variant": "A-profiles"}
|
|
|
|
|
|
@app.get("/crew/agents")
|
|
async def list_agents():
|
|
"""List all agents with their profiles"""
|
|
return get_all_agents_summary()
|
|
|
|
|
|
@app.get("/crew/teams")
|
|
async def list_teams():
|
|
"""List all teams (for backwards compatibility, returns profiles view)"""
|
|
summary = get_all_agents_summary()
|
|
result = {}
|
|
|
|
for agent_id, info in summary.items():
|
|
for profile_name in info.get("profiles", []):
|
|
key = f"{agent_id}" if profile_name == "default" else f"{agent_id}.{profile_name}"
|
|
settings = get_team_settings(agent_id, profile_name)
|
|
deleg = get_delegation_config(agent_id, profile_name)
|
|
members = get_team_members(agent_id, profile_name)
|
|
|
|
result[key] = {
|
|
"agent_id": agent_id,
|
|
"profile": profile_name,
|
|
"team_name": settings.get("team_name"),
|
|
"member_count": len(members),
|
|
"members": [m.get("role_context") for m in members],
|
|
"parallel_roles": settings.get("parallel_roles"),
|
|
"max_concurrency": settings.get("max_concurrency"),
|
|
"has_delegation": deleg.get("enabled", False)
|
|
}
|
|
|
|
return result
|
|
|
|
|
|
@app.post("/crew/run", response_model=CrewRunResponse)
|
|
async def run_crew(request: CrewRunRequest):
|
|
"""Execute multi-role orchestration for an agent"""
|
|
orchestrator_id = request.orchestrator_id
|
|
|
|
if not is_orchestrator(orchestrator_id):
|
|
raise HTTPException(status_code=404, detail=f"Agent {orchestrator_id} not found or not an orchestrator")
|
|
|
|
return await run_crew_canonical(
|
|
orchestrator_id=orchestrator_id,
|
|
task=request.task,
|
|
context=request.context,
|
|
profile=request.profile
|
|
)
|
|
|
|
|
|
@app.post("/crew/delegate")
|
|
async def delegate(request: DelegationRequest):
|
|
"""Delegate task to another top-level agent (for DAARWIZZ)"""
|
|
result = await delegate_to_agent(
|
|
orchestrator_id=request.orchestrator_id,
|
|
target_agent_id=request.target_agent_id,
|
|
task=request.task,
|
|
context=request.context,
|
|
hops_remaining=request.hops_remaining
|
|
)
|
|
|
|
if result is None:
|
|
raise HTTPException(status_code=400, detail="Delegation failed or not allowed")
|
|
|
|
return {"success": True, "result": result}
|
|
|
|
|
|
@app.on_event("startup")
|
|
async def startup():
|
|
config = load_teams_config()
|
|
version = config.get("version", "unknown")
|
|
summary = get_all_agents_summary()
|
|
|
|
total_profiles = sum(len(info.get("profiles", [])) for info in summary.values())
|
|
|
|
logger.info(f"=== CrewAI Service v2.0.0 (Variant A) ===")
|
|
logger.info(f"Config version: {version}")
|
|
logger.info(f"Agents: {len(summary)}, Total profiles: {total_profiles}")
|
|
|
|
for agent_id, info in summary.items():
|
|
profiles = info.get("profiles", [])
|
|
logger.info(f" {agent_id}: {profiles}")
|