Files
microdao-daarion/services/crewai-service/app/main.py
Apple ef3473db21 snapshot: NODE1 production state 2026-02-09
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>
2026-02-09 08:46:46 -08:00

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}")