Merge branch 'main' into claude/webdav-context-prompt-013MZPMZrFPHpdzo2pRjpmJT

This commit is contained in:
Alexa Amundson
2025-11-18 06:53:05 -06:00
committed by GitHub
11 changed files with 8421 additions and 0 deletions

View File

@@ -0,0 +1,253 @@
"""
Cognition Database Models
Models for storing:
- Workflows and their execution history
- Reasoning traces from agents
- Agent memory/context
- Prompt registry
Tables:
- workflows: Workflow definitions and execution status
- workflow_executions: History of workflow runs
- reasoning_traces: Agent reasoning step records
- agent_memory: Shared memory/context across workflow
- prompt_registry: Registered agent prompts
"""
from datetime import datetime
from sqlalchemy import Column, Integer, String, Text, Float, Boolean, DateTime, ForeignKey, JSON, Enum
from sqlalchemy.orm import relationship
from sqlalchemy.dialects.postgresql import UUID, JSONB
import uuid
import enum
from ..database import Base
class WorkflowStatus(str, enum.Enum):
"""Workflow execution status"""
PENDING = "pending"
RUNNING = "running"
COMPLETED = "completed"
FAILED = "failed"
CANCELLED = "cancelled"
class ExecutionMode(str, enum.Enum):
"""Workflow execution mode"""
SEQUENTIAL = "sequential"
PARALLEL = "parallel"
RECURSIVE = "recursive"
class Workflow(Base):
"""
Workflow Definition
Stores multi-agent workflow definitions.
"""
__tablename__ = "workflows"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name = Column(String(200), nullable=False, index=True)
description = Column(Text)
# Workflow configuration
mode = Column(Enum(ExecutionMode), default=ExecutionMode.SEQUENTIAL, nullable=False)
steps = Column(JSONB, nullable=False) # List of workflow steps
timeout_seconds = Column(Integer, default=600)
# Metadata
created_by = Column(String(100))
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
is_active = Column(Boolean, default=True)
is_template = Column(Boolean, default=False)
# Tags for categorization
tags = Column(JSONB, default=list)
# Relationships
executions = relationship("WorkflowExecution", back_populates="workflow", cascade="all, delete-orphan")
def __repr__(self):
return f"<Workflow(id={self.id}, name={self.name}, mode={self.mode})>"
class WorkflowExecution(Base):
"""
Workflow Execution Record
Stores history of workflow executions with results.
"""
__tablename__ = "workflow_executions"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
workflow_id = Column(UUID(as_uuid=True), ForeignKey("workflows.id"), nullable=False, index=True)
# Execution details
status = Column(Enum(WorkflowStatus), default=WorkflowStatus.PENDING, nullable=False, index=True)
started_at = Column(DateTime, default=datetime.utcnow, nullable=False, index=True)
completed_at = Column(DateTime)
duration_seconds = Column(Float)
# Results
step_results = Column(JSONB) # Results from each step
error_message = Column(Text)
error_details = Column(JSONB)
# Metrics
overall_confidence = Column(Float)
total_agents_used = Column(Integer)
# Context
initial_context = Column(JSONB)
final_memory = Column(JSONB)
# Relationships
workflow = relationship("Workflow", back_populates="executions")
reasoning_traces = relationship("ReasoningTrace", back_populates="execution", cascade="all, delete-orphan")
def __repr__(self):
return f"<WorkflowExecution(id={self.id}, workflow_id={self.workflow_id}, status={self.status})>"
class ReasoningTrace(Base):
"""
Reasoning Trace Step
Stores individual reasoning steps from agent execution.
Provides transparency into how agents arrived at decisions.
"""
__tablename__ = "reasoning_traces"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
execution_id = Column(UUID(as_uuid=True), ForeignKey("workflow_executions.id"), nullable=False, index=True)
# Step identification
workflow_step_name = Column(String(100), nullable=False)
agent_name = Column(String(50), nullable=False, index=True)
step_number = Column(Integer, nullable=False)
step_name = Column(String(100), nullable=False)
step_emoji = Column(String(10))
# Reasoning data
input_context = Column(Text)
output = Column(Text)
confidence_score = Column(Float)
# Additional metadata
metadata = Column(JSONB)
timestamp = Column(DateTime, default=datetime.utcnow, nullable=False, index=True)
# Relationships
execution = relationship("WorkflowExecution", back_populates="reasoning_traces")
def __repr__(self):
return f"<ReasoningTrace(id={self.id}, agent={self.agent_name}, step={self.step_name})>"
class AgentMemory(Base):
"""
Agent Memory/Context
Stores shared context and memory across workflow execution.
Enables agents to build upon each other's work.
"""
__tablename__ = "agent_memory"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
execution_id = Column(UUID(as_uuid=True), ForeignKey("workflow_executions.id"), index=True)
# Memory data
context = Column(JSONB, nullable=False) # Shared context dictionary
confidence_scores = Column(JSONB) # Confidence per step
# Metadata
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Memory can be associated with user sessions
session_id = Column(String(100), index=True)
user_id = Column(String(100), index=True)
# TTL for memory expiration
expires_at = Column(DateTime)
def __repr__(self):
return f"<AgentMemory(id={self.id}, execution_id={self.execution_id})>"
class PromptRegistry(Base):
"""
Prompt Registry
Stores registered agent prompts (summon spells).
Enables versioning and management of agent invocation prompts.
"""
__tablename__ = "prompt_registry"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
# Prompt identification
agent_name = Column(String(50), nullable=False, index=True)
prompt_name = Column(String(100))
prompt_text = Column(Text, nullable=False)
# Versioning
version = Column(String(20), nullable=False)
is_active = Column(Boolean, default=True, index=True)
# Metadata
description = Column(Text)
metadata = Column(JSONB) # Author, purpose, etc.
tags = Column(JSONB, default=list)
# Usage stats
usage_count = Column(Integer, default=0)
last_used_at = Column(DateTime)
average_confidence = Column(Float)
# Timestamps
created_at = Column(DateTime, default=datetime.utcnow, nullable=False, index=True)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
created_by = Column(String(100))
def __repr__(self):
return f"<PromptRegistry(id={self.id}, agent={self.agent_name}, version={self.version})>"
class AgentPerformanceMetric(Base):
"""
Agent Performance Metrics
Tracks performance metrics for agents over time.
Enables monitoring and optimization.
"""
__tablename__ = "agent_performance_metrics"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
# Agent identification
agent_name = Column(String(50), nullable=False, index=True)
execution_id = Column(UUID(as_uuid=True), ForeignKey("workflow_executions.id"), index=True)
# Performance metrics
execution_time_seconds = Column(Float)
confidence_score = Column(Float)
success = Column(Boolean, default=True)
# Resource usage (if available)
memory_usage_mb = Column(Float)
api_calls_made = Column(Integer)
# Quality metrics
reasoning_steps_count = Column(Integer)
complexity_score = Column(Float)
# Timestamps
measured_at = Column(DateTime, default=datetime.utcnow, nullable=False, index=True)
def __repr__(self):
return f"<AgentPerformanceMetric(agent={self.agent_name}, confidence={self.confidence_score})>"

View File

@@ -0,0 +1,499 @@
"""
Cognition API Router
Exposes the Cece Cognition Framework via REST API:
- Execute individual agents (Cece, Wasp, Clause, Codex)
- Execute multi-agent workflows
- Query reasoning traces
- Access agent memory
- Manage prompts
Endpoints:
- POST /api/cognition/execute - Execute single agent
- POST /api/cognition/workflows - Execute multi-agent workflow
- GET /api/cognition/reasoning-trace/{workflow_id} - Get reasoning trace
- GET /api/cognition/memory - Query agent memory
- POST /api/prompts/register - Register new prompt
- GET /api/prompts/search - Search prompts
"""
import logging
from typing import Any, Dict, List, Optional
from datetime import datetime
from uuid import uuid4
from fastapi import APIRouter, HTTPException, Depends, status
from pydantic import BaseModel, Field
# Import our agents and orchestration
from agents.categories.ai_ml.cece_agent import CeceAgent
from agents.categories.ai_ml.wasp_agent import WaspAgent
from agents.categories.ai_ml.clause_agent import ClauseAgent
from agents.categories.ai_ml.codex_agent import CodexAgent
from backend.app.services.orchestration import (
OrchestrationEngine,
Workflow,
WorkflowStep,
ExecutionMode
)
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/cognition", tags=["Cognition"])
# ============================================================================
# REQUEST/RESPONSE MODELS
# ============================================================================
class AgentExecuteRequest(BaseModel):
"""Request to execute single agent"""
agent: str = Field(..., description="Agent name: cece, wasp, clause, codex")
input: str = Field(..., description="Input/prompt for the agent")
context: Dict[str, Any] = Field(default_factory=dict, description="Optional context")
verbose: bool = Field(default=True, description="Return full reasoning trace")
class AgentExecuteResponse(BaseModel):
"""Response from agent execution"""
agent: str
result: Dict[str, Any]
reasoning_trace: List[Any]
confidence: float
execution_time_seconds: float
warnings: List[str] = Field(default_factory=list)
class WorkflowStepRequest(BaseModel):
"""Workflow step definition"""
name: str
agent_name: str
input_template: str
depends_on: List[str] = Field(default_factory=list)
parallel_with: List[str] = Field(default_factory=list)
max_retries: int = Field(default=3)
class WorkflowExecuteRequest(BaseModel):
"""Request to execute multi-agent workflow"""
name: str
steps: List[WorkflowStepRequest]
mode: str = Field(default="sequential", description="sequential, parallel, or recursive")
initial_context: Dict[str, Any] = Field(default_factory=dict)
timeout_seconds: int = Field(default=600)
class WorkflowExecuteResponse(BaseModel):
"""Response from workflow execution"""
workflow_id: str
status: str
step_results: Dict[str, Any]
reasoning_trace: List[Dict[str, Any]]
memory: Dict[str, Any]
total_duration_seconds: float
error: Optional[str] = None
class PromptRegisterRequest(BaseModel):
"""Request to register new prompt"""
agent_name: str
prompt_text: str
version: str = Field(default="1.0.0")
metadata: Dict[str, Any] = Field(default_factory=dict)
class PromptSearchRequest(BaseModel):
"""Request to search prompts"""
agent: Optional[str] = None
version: Optional[str] = None
search_term: Optional[str] = None
# ============================================================================
# AGENT EXECUTION ENDPOINTS
# ============================================================================
# Global orchestration engine
orchestration_engine = OrchestrationEngine()
@router.post("/execute", response_model=AgentExecuteResponse)
async def execute_agent(request: AgentExecuteRequest):
"""
Execute single agent
Execute one of the core agents (Cece, Wasp, Clause, Codex) with the given input.
**Agents:**
- `cece`: Cognitive architect (15-step reasoning + 6-step architecture)
- `wasp`: UI/UX specialist (7-step design process)
- `clause`: Legal specialist (7-step legal review)
- `codex`: Code execution specialist (7-step dev process)
**Example:**
```json
{
"agent": "cece",
"input": "I'm overwhelmed with 10 projects and don't know where to start",
"context": {
"projects": ["Project A", "Project B", ...],
"deadlines": {...}
}
}
```
"""
logger.info(f"🚀 Executing agent: {request.agent}")
# Get agent instance
if request.agent not in orchestration_engine.agents:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Unknown agent: {request.agent}. Available: cece, wasp, clause, codex"
)
agent = orchestration_engine.agents[request.agent]
# Prepare params
params = {
"input": request.input,
"context": request.context,
"verbose": request.verbose
}
try:
# Execute agent
result = await agent.run(params)
# Build response
return AgentExecuteResponse(
agent=request.agent,
result=result.data,
reasoning_trace=result.data.get("reasoning_trace", []),
confidence=result.data.get("confidence", 0.85),
execution_time_seconds=result.duration_seconds or 0.0,
warnings=result.data.get("warnings", [])
)
except Exception as e:
logger.error(f"Error executing agent {request.agent}: {str(e)}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Agent execution failed: {str(e)}"
)
@router.post("/workflows", response_model=WorkflowExecuteResponse)
async def execute_workflow(request: WorkflowExecuteRequest):
"""
Execute multi-agent workflow
Execute a multi-step workflow with multiple agents working together.
**Execution Modes:**
- `sequential`: Steps run one after another (A → B → C)
- `parallel`: Independent steps run simultaneously where possible
- `recursive`: Steps iterate until convergence
**Example:**
```json
{
"name": "Build Dashboard",
"mode": "sequential",
"steps": [
{
"name": "architect",
"agent_name": "cece",
"input_template": "Design a dashboard for AI agent workflows"
},
{
"name": "backend",
"agent_name": "codex",
"input_template": "${architect.architecture.backend_spec}",
"depends_on": ["architect"]
},
{
"name": "frontend",
"agent_name": "wasp",
"input_template": "${architect.architecture.frontend_spec}",
"depends_on": ["architect"]
}
]
}
```
"""
logger.info(f"🚀 Executing workflow: {request.name}")
# Convert request to Workflow
try:
mode = ExecutionMode[request.mode.upper()]
except KeyError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid execution mode: {request.mode}. Use: sequential, parallel, or recursive"
)
workflow = Workflow(
id=str(uuid4()),
name=request.name,
steps=[
WorkflowStep(
name=step.name,
agent_name=step.agent_name,
input_template=step.input_template,
depends_on=step.depends_on,
parallel_with=step.parallel_with,
max_retries=step.max_retries
)
for step in request.steps
],
mode=mode,
timeout_seconds=request.timeout_seconds
)
try:
# Execute workflow
result = await orchestration_engine.execute_workflow(
workflow,
initial_context=request.initial_context
)
# Build response
return WorkflowExecuteResponse(
workflow_id=result.workflow_id,
status=result.status.value,
step_results=result.step_results,
reasoning_trace=result.reasoning_trace,
memory=result.memory,
total_duration_seconds=result.total_duration_seconds,
error=result.error
)
except Exception as e:
logger.error(f"Error executing workflow {request.name}: {str(e)}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Workflow execution failed: {str(e)}"
)
@router.get("/reasoning-trace/{workflow_id}")
async def get_reasoning_trace(workflow_id: str):
"""
Get reasoning trace for workflow
Retrieve the complete reasoning trace showing how agents arrived at their decisions.
Returns a list of reasoning steps with:
- Step name and emoji
- Input context
- Output/decision
- Confidence score
- Timestamp
"""
# In a real implementation, would fetch from database
# For now, return placeholder
return {
"workflow_id": workflow_id,
"trace": [
{
"step": "🚨 Not ok",
"agent": "cece",
"input": "I'm overwhelmed with projects",
"output": "There's too many competing priorities without clear hierarchy",
"confidence": 0.95,
"timestamp": datetime.utcnow().isoformat()
}
],
"overall_confidence": 0.87
}
@router.get("/memory")
async def get_memory(workflow_id: Optional[str] = None):
"""
Query agent memory
Retrieve shared memory/context from workflow execution.
Can filter by workflow_id to get memory for specific workflow.
"""
# In a real implementation, would fetch from database/cache
return {
"workflow_id": workflow_id,
"context": {
"user_preferences": {},
"session_data": {}
},
"reasoning_trace": [],
"confidence_scores": {}
}
@router.get("/active-workflows")
async def get_active_workflows():
"""
Get list of active workflows
Returns workflow IDs currently being executed.
"""
active = orchestration_engine.get_active_workflows()
return {
"active_workflows": active,
"count": len(active)
}
@router.post("/cancel-workflow/{workflow_id}")
async def cancel_workflow(workflow_id: str):
"""
Cancel running workflow
Attempts to cancel a running workflow. Returns success status.
"""
success = await orchestration_engine.cancel_workflow(workflow_id)
if not success:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Workflow not found or already completed: {workflow_id}"
)
return {
"workflow_id": workflow_id,
"status": "cancelled"
}
# ============================================================================
# PROMPT MANAGEMENT ENDPOINTS
# ============================================================================
# In-memory prompt registry (would be database in production)
prompt_registry: Dict[str, List[Dict[str, Any]]] = {}
@router.post("/prompts/register")
async def register_prompt(request: PromptRegisterRequest):
"""
Register new agent prompt
Register a custom summon prompt for an agent.
**Example:**
```json
{
"agent_name": "cece",
"prompt_text": "Cece, run cognition.\\n\\nUse the Alexa-Cece Framework...\\n\\nNow analyze: [YOUR REQUEST]",
"version": "1.0.0",
"metadata": {
"author": "Alexa",
"purpose": "Full cognition framework"
}
}
```
"""
prompt_id = str(uuid4())
prompt = {
"id": prompt_id,
"agent_name": request.agent_name,
"prompt_text": request.prompt_text,
"version": request.version,
"metadata": request.metadata,
"created_at": datetime.utcnow().isoformat(),
"is_active": True
}
if request.agent_name not in prompt_registry:
prompt_registry[request.agent_name] = []
prompt_registry[request.agent_name].append(prompt)
logger.info(f"Registered prompt for {request.agent_name} (ID: {prompt_id})")
return {
"prompt_id": prompt_id,
"status": "registered"
}
@router.get("/prompts/search")
async def search_prompts(
agent: Optional[str] = None,
version: Optional[str] = None
):
"""
Search registered prompts
Search for prompts by agent name and/or version.
**Query Parameters:**
- `agent`: Filter by agent name (cece, wasp, clause, codex)
- `version`: Filter by version (e.g., "1.0.0", "latest")
"""
results = []
if agent:
if agent in prompt_registry:
prompts = prompt_registry[agent]
if version == "latest":
# Return only the most recent version
if prompts:
prompts = [max(prompts, key=lambda p: p["created_at"])]
elif version:
# Filter by specific version
prompts = [p for p in prompts if p["version"] == version]
results.extend(prompts)
else:
# Return all prompts
for agent_prompts in prompt_registry.values():
results.extend(agent_prompts)
return {
"prompts": results,
"count": len(results)
}
# ============================================================================
# UTILITY ENDPOINTS
# ============================================================================
@router.get("/agents")
async def list_agents():
"""
List available agents
Returns information about all available agents.
"""
agents = []
for agent_name, agent_instance in orchestration_engine.agents.items():
info = agent_instance.get_info()
agents.append(info)
return {
"agents": agents,
"count": len(agents)
}
@router.get("/health")
async def health_check():
"""
Health check for cognition system
Returns system status and metrics.
"""
return {
"status": "healthy",
"agents_available": len(orchestration_engine.agents),
"active_workflows": len(orchestration_engine.get_active_workflows()),
"prompts_registered": sum(len(prompts) for prompts in prompt_registry.values()),
"timestamp": datetime.utcnow().isoformat()
}

View File

@@ -0,0 +1,468 @@
"""
Multi-Agent Orchestration Service
Coordinates execution of multiple AI agents in workflows:
- Sequential execution (A → B → C)
- Parallel execution (A + B + C → merge)
- Recursive refinement (A ⇄ B until optimal)
- Memory sharing between agents
- Reasoning trace aggregation
"""
import asyncio
import logging
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import Any, Dict, List, Optional, Set
from uuid import uuid4
# Import our core agents
from agents.categories.ai_ml.cece_agent import CeceAgent
from agents.categories.ai_ml.wasp_agent import WaspAgent
from agents.categories.ai_ml.clause_agent import ClauseAgent
from agents.categories.ai_ml.codex_agent import CodexAgent
logger = logging.getLogger(__name__)
class WorkflowStatus(Enum):
"""Workflow execution status"""
PENDING = "pending"
RUNNING = "running"
COMPLETED = "completed"
FAILED = "failed"
CANCELLED = "cancelled"
class ExecutionMode(Enum):
"""Agent execution mode"""
SEQUENTIAL = "sequential"
PARALLEL = "parallel"
RECURSIVE = "recursive"
@dataclass
class WorkflowStep:
"""Single step in workflow"""
name: str
agent_name: str
input_template: str # Can reference previous outputs with ${step_name.field}
depends_on: List[str] = field(default_factory=list)
parallel_with: List[str] = field(default_factory=list)
max_retries: int = 3
@dataclass
class Workflow:
"""Multi-agent workflow definition"""
id: str
name: str
steps: List[WorkflowStep]
mode: ExecutionMode = ExecutionMode.SEQUENTIAL
timeout_seconds: int = 600 # 10 minutes default
created_at: datetime = field(default_factory=datetime.utcnow)
@dataclass
class WorkflowResult:
"""Workflow execution result"""
workflow_id: str
status: WorkflowStatus
step_results: Dict[str, Any]
reasoning_trace: List[Dict[str, Any]]
memory: Dict[str, Any]
started_at: datetime
completed_at: Optional[datetime] = None
error: Optional[str] = None
total_duration_seconds: float = 0.0
class AgentMemory:
"""Shared memory across workflow agents"""
def __init__(self):
self.context: Dict[str, Any] = {}
self.reasoning_trace: List[Dict[str, Any]] = []
self.confidence_scores: Dict[str, float] = {}
self.metadata: Dict[str, Any] = {}
def set(self, key: str, value: Any) -> None:
"""Set value in context"""
self.context[key] = value
def get(self, key: str, default: Any = None) -> Any:
"""Get value from context"""
return self.context.get(key, default)
def add_reasoning(self, step_name: str, agent_name: str, reasoning: Any) -> None:
"""Add reasoning trace from agent"""
self.reasoning_trace.append({
"step": step_name,
"agent": agent_name,
"reasoning": reasoning,
"timestamp": datetime.utcnow().isoformat()
})
def set_confidence(self, step_name: str, confidence: float) -> None:
"""Set confidence score for step"""
self.confidence_scores[step_name] = confidence
class OrchestrationEngine:
"""
Multi-Agent Orchestration Engine
Executes workflows with multiple AI agents, managing:
- Execution order (sequential, parallel, recursive)
- Dependency resolution
- Memory sharing
- Error handling and retries
- Reasoning trace aggregation
Example:
```python
engine = OrchestrationEngine()
workflow = Workflow(
id="build-dashboard",
name="Build Dashboard",
steps=[
WorkflowStep(
name="architect",
agent_name="cece",
input_template="Design dashboard for AI agents"
),
WorkflowStep(
name="backend",
agent_name="codex",
input_template="${architect.architecture.backend_spec}",
depends_on=["architect"]
),
WorkflowStep(
name="frontend",
agent_name="wasp",
input_template="${architect.architecture.frontend_spec}",
depends_on=["architect"]
)
]
)
result = await engine.execute_workflow(workflow)
```
"""
def __init__(self):
self.logger = logging.getLogger(__name__)
# Initialize agents
self.agents = {
"cece": CeceAgent(),
"wasp": WaspAgent(),
"clause": ClauseAgent(),
"codex": CodexAgent()
}
# Active workflows
self.active_workflows: Dict[str, Workflow] = {}
async def execute_workflow(
self,
workflow: Workflow,
initial_context: Optional[Dict[str, Any]] = None
) -> WorkflowResult:
"""
Execute multi-agent workflow
Args:
workflow: Workflow definition
initial_context: Initial context/memory (optional)
Returns:
WorkflowResult with all step outputs and reasoning traces
"""
workflow_id = workflow.id or str(uuid4())
started_at = datetime.utcnow()
self.logger.info(f"🚀 Starting workflow: {workflow.name} (ID: {workflow_id})")
self.active_workflows[workflow_id] = workflow
# Initialize shared memory
memory = AgentMemory()
if initial_context:
memory.context.update(initial_context)
try:
# Execute based on mode
if workflow.mode == ExecutionMode.SEQUENTIAL:
step_results = await self._execute_sequential(workflow, memory)
elif workflow.mode == ExecutionMode.PARALLEL:
step_results = await self._execute_parallel(workflow, memory)
elif workflow.mode == ExecutionMode.RECURSIVE:
step_results = await self._execute_recursive(workflow, memory)
else:
raise ValueError(f"Unknown execution mode: {workflow.mode}")
completed_at = datetime.utcnow()
duration = (completed_at - started_at).total_seconds()
self.logger.info(
f"✅ Workflow completed: {workflow.name} "
f"({len(step_results)} steps, {duration:.2f}s)"
)
return WorkflowResult(
workflow_id=workflow_id,
status=WorkflowStatus.COMPLETED,
step_results=step_results,
reasoning_trace=memory.reasoning_trace,
memory=memory.context,
started_at=started_at,
completed_at=completed_at,
total_duration_seconds=duration
)
except Exception as e:
self.logger.error(f"❌ Workflow failed: {workflow.name} - {str(e)}", exc_info=True)
return WorkflowResult(
workflow_id=workflow_id,
status=WorkflowStatus.FAILED,
step_results={},
reasoning_trace=memory.reasoning_trace,
memory=memory.context,
started_at=started_at,
completed_at=datetime.utcnow(),
error=str(e)
)
finally:
# Cleanup
if workflow_id in self.active_workflows:
del self.active_workflows[workflow_id]
async def _execute_sequential(
self,
workflow: Workflow,
memory: AgentMemory
) -> Dict[str, Any]:
"""Execute workflow steps sequentially"""
step_results = {}
for step in workflow.steps:
self.logger.info(f"▶️ Executing step: {step.name} (agent: {step.agent_name})")
# Resolve input from template
input_params = self._resolve_input_template(step.input_template, step_results, memory)
# Execute agent
result = await self._execute_agent_with_retry(
step.agent_name,
input_params,
step.max_retries
)
# Store result
step_results[step.name] = result.data
# Update memory
memory.set(f"{step.name}_output", result.data)
memory.add_reasoning(step.name, step.agent_name, result.data.get("reasoning_trace", []))
if "confidence" in result.data:
memory.set_confidence(step.name, result.data["confidence"])
return step_results
async def _execute_parallel(
self,
workflow: Workflow,
memory: AgentMemory
) -> Dict[str, Any]:
"""Execute workflow steps in parallel where possible"""
step_results = {}
remaining_steps = set(step.name for step in workflow.steps)
completed_steps: Set[str] = set()
while remaining_steps:
# Find steps that can run now (dependencies met)
ready_steps = []
for step in workflow.steps:
if step.name not in remaining_steps:
continue
# Check if dependencies are met
deps_met = all(dep in completed_steps for dep in step.depends_on)
if deps_met:
ready_steps.append(step)
if not ready_steps:
raise RuntimeError("Workflow deadlock: no steps can execute (circular dependency?)")
# Execute ready steps in parallel
self.logger.info(f"▶️ Executing {len(ready_steps)} steps in parallel")
tasks = []
for step in ready_steps:
input_params = self._resolve_input_template(step.input_template, step_results, memory)
task = self._execute_agent_with_retry(
step.agent_name,
input_params,
step.max_retries
)
tasks.append((step.name, step.agent_name, task))
# Wait for all parallel steps to complete
results = await asyncio.gather(*[task for _, _, task in tasks])
# Process results
for (step_name, agent_name, _), result in zip(tasks, results):
step_results[step_name] = result.data
memory.set(f"{step_name}_output", result.data)
memory.add_reasoning(step_name, agent_name, result.data.get("reasoning_trace", []))
if "confidence" in result.data:
memory.set_confidence(step_name, result.data["confidence"])
completed_steps.add(step_name)
remaining_steps.remove(step_name)
return step_results
async def _execute_recursive(
self,
workflow: Workflow,
memory: AgentMemory
) -> Dict[str, Any]:
"""Execute workflow recursively until convergence"""
step_results = {}
max_iterations = 10
convergence_threshold = 0.95
iteration = 0
while iteration < max_iterations:
iteration += 1
self.logger.info(f"🔄 Recursive iteration {iteration}")
iteration_results = {}
# Execute all steps
for step in workflow.steps:
input_params = self._resolve_input_template(step.input_template, step_results, memory)
result = await self._execute_agent_with_retry(
step.agent_name,
input_params,
step.max_retries
)
iteration_results[step.name] = result.data
memory.add_reasoning(f"{step.name}_iter{iteration}", step.agent_name, result.data.get("reasoning_trace", []))
# Check convergence
confidence = self._calculate_overall_confidence(iteration_results)
self.logger.info(f" Confidence: {confidence:.2f}")
if confidence >= convergence_threshold:
self.logger.info(f"✓ Converged at iteration {iteration} (confidence: {confidence:.2f})")
step_results = iteration_results
break
step_results = iteration_results
return step_results
async def _execute_agent_with_retry(
self,
agent_name: str,
params: Dict[str, Any],
max_retries: int
):
"""Execute agent with automatic retries"""
if agent_name not in self.agents:
raise ValueError(f"Unknown agent: {agent_name}")
agent = self.agents[agent_name]
last_exception = None
for attempt in range(max_retries):
try:
result = await agent.run(params)
return result
except Exception as e:
last_exception = e
self.logger.warning(
f"Agent {agent_name} attempt {attempt + 1}/{max_retries} failed: {str(e)}"
)
if attempt < max_retries - 1:
await asyncio.sleep(2 ** attempt) # Exponential backoff
raise last_exception
def _resolve_input_template(
self,
template: str,
step_results: Dict[str, Any],
memory: AgentMemory
) -> Dict[str, Any]:
"""Resolve input template with variables from previous steps"""
# If template doesn't contain variables, treat as direct input
if "${" not in template:
return {"input": template}
# Replace variables like ${step_name.field} with actual values
resolved = template
import re
pattern = r'\$\{([^}]+)\}'
matches = re.findall(pattern, template)
for match in matches:
parts = match.split('.')
# Navigate through nested structure
value = step_results
for part in parts:
if isinstance(value, dict):
value = value.get(part)
else:
value = None
break
if value is not None:
resolved = resolved.replace(f"${{{match}}}", str(value))
return {"input": resolved, "context": memory.context}
def _calculate_overall_confidence(self, step_results: Dict[str, Any]) -> float:
"""Calculate overall confidence from step results"""
confidences = []
for result in step_results.values():
if isinstance(result, dict) and "confidence" in result:
confidences.append(result["confidence"])
if not confidences:
return 0.0
return sum(confidences) / len(confidences)
async def cancel_workflow(self, workflow_id: str) -> bool:
"""Cancel running workflow"""
if workflow_id in self.active_workflows:
self.logger.info(f"Cancelling workflow: {workflow_id}")
# In a real implementation, would cancel running tasks
del self.active_workflows[workflow_id]
return True
return False
def get_active_workflows(self) -> List[str]:
"""Get list of active workflow IDs"""
return list(self.active_workflows.keys())