Files
virtual_board_member/app/services/autonomous_workflow_engine.py
Jonathan Pressnell 5b5714e4c2 feat: Complete Week 5 implementation - Agentic RAG & Multi-Agent Orchestration
- Implement Autonomous Workflow Engine with dynamic task decomposition
- Add Multi-Agent Communication Protocol with message routing
- Create Enhanced Reasoning Chains (CoT, ToT, Multi-Step, Parallel, Hybrid)
- Add comprehensive REST API endpoints for all Week 5 features
- Include 26/26 passing tests with full coverage
- Add complete documentation and API guides
- Update development plan to mark Week 5 as completed

Features:
- Dynamic task decomposition and parallel execution
- Agent registration, messaging, and coordination
- 5 reasoning methods with validation and learning
- Robust error handling and monitoring
- Multi-tenant support and security
- Production-ready architecture

Files added/modified:
- app/services/autonomous_workflow_engine.py
- app/services/agent_communication.py
- app/services/enhanced_reasoning.py
- app/api/v1/endpoints/week5_features.py
- tests/test_week5_features.py
- docs/week5_api_documentation.md
- docs/week5_readme.md
- WEEK5_COMPLETION_SUMMARY.md
- DEVELOPMENT_PLAN.md (updated)

All tests passing: 26/26
2025-08-10 09:25:46 -04:00

542 lines
19 KiB
Python

"""
Autonomous Workflow Engine - Week 5 Implementation
Handles dynamic task decomposition, parallel execution, and workflow orchestration.
"""
from __future__ import annotations
import asyncio
import logging
from typing import Any, Dict, List, Optional, Set
from dataclasses import dataclass, field
from enum import Enum
import uuid
from datetime import datetime
from concurrent.futures import ThreadPoolExecutor
import json
from app.services.agentic_rag_service import AgentTask, AgentType
from app.core.cache import cache_service
logger = logging.getLogger(__name__)
class WorkflowStatus(Enum):
"""Workflow execution status."""
PENDING = "pending"
RUNNING = "running"
COMPLETED = "completed"
FAILED = "failed"
CANCELLED = "cancelled"
class TaskStatus(Enum):
"""Task execution status."""
PENDING = "pending"
RUNNING = "running"
COMPLETED = "completed"
FAILED = "failed"
BLOCKED = "blocked"
@dataclass
class WorkflowDefinition:
"""Definition of a workflow with tasks and dependencies."""
id: str
name: str
description: str
tasks: List[AgentTask]
dependencies: Dict[str, List[str]] = field(default_factory=dict)
max_parallel_tasks: int = 5
timeout_seconds: int = 300
created_at: datetime = field(default_factory=datetime.utcnow)
@dataclass
class WorkflowExecution:
"""Represents an execution instance of a workflow."""
id: str
workflow_definition: WorkflowDefinition
tenant_id: str
status: WorkflowStatus = WorkflowStatus.PENDING
task_results: Dict[str, Any] = field(default_factory=dict)
task_status: Dict[str, TaskStatus] = field(default_factory=dict)
start_time: Optional[datetime] = None
end_time: Optional[datetime] = None
error: Optional[str] = None
metadata: Dict[str, Any] = field(default_factory=dict)
class TaskDecomposer:
"""Decomposes complex tasks into subtasks."""
def __init__(self):
self.decomposition_strategies = {
"research": self._decompose_research_task,
"analysis": self._decompose_analysis_task,
"synthesis": self._decompose_synthesis_task,
"validation": self._decompose_validation_task
}
async def decompose_task(self, task: AgentTask, context: Dict[str, Any]) -> List[AgentTask]:
"""Decompose a complex task into subtasks."""
strategy = self.decomposition_strategies.get(task.agent_type.value, self._decompose_generic_task)
return await strategy(task, context)
async def _decompose_research_task(self, task: AgentTask, context: Dict[str, Any]) -> List[AgentTask]:
"""Decompose research task into subtasks."""
query = task.input_data.get("query", "")
subtasks = []
# Task 1: Query analysis
subtasks.append(AgentTask(
id=f"{task.id}_analysis",
agent_type=AgentType.RESEARCH,
description=f"Analyze query: {query}",
input_data={"query": query, "task": "query_analysis"},
dependencies=[],
priority=task.priority,
created_at=datetime.utcnow()
))
# Task 2: Strategy selection
subtasks.append(AgentTask(
id=f"{task.id}_strategy",
agent_type=AgentType.RESEARCH,
description=f"Select retrieval strategy for: {query}",
input_data={"query": query, "task": "strategy_selection"},
dependencies=[f"{task.id}_analysis"],
priority=task.priority,
created_at=datetime.utcnow()
))
# Task 3: Information retrieval
subtasks.append(AgentTask(
id=f"{task.id}_retrieval",
agent_type=AgentType.RESEARCH,
description=f"Retrieve information for: {query}",
input_data={"query": query, "task": "information_retrieval"},
dependencies=[f"{task.id}_strategy"],
priority=task.priority,
created_at=datetime.utcnow()
))
return subtasks
async def _decompose_analysis_task(self, task: AgentTask, context: Dict[str, Any]) -> List[AgentTask]:
"""Decompose analysis task into subtasks."""
query = task.input_data.get("query", "")
subtasks = []
# Task 1: Data validation
subtasks.append(AgentTask(
id=f"{task.id}_validation",
agent_type=AgentType.ANALYSIS,
description=f"Validate data for: {query}",
input_data={"query": query, "task": "data_validation"},
dependencies=[],
priority=task.priority,
created_at=datetime.utcnow()
))
# Task 2: Pattern recognition
subtasks.append(AgentTask(
id=f"{task.id}_patterns",
agent_type=AgentType.ANALYSIS,
description=f"Identify patterns for: {query}",
input_data={"query": query, "task": "pattern_recognition"},
dependencies=[f"{task.id}_validation"],
priority=task.priority,
created_at=datetime.utcnow()
))
# Task 3: Insight generation
subtasks.append(AgentTask(
id=f"{task.id}_insights",
agent_type=AgentType.ANALYSIS,
description=f"Generate insights for: {query}",
input_data={"query": query, "task": "insight_generation"},
dependencies=[f"{task.id}_patterns"],
priority=task.priority,
created_at=datetime.utcnow()
))
return subtasks
async def _decompose_synthesis_task(self, task: AgentTask, context: Dict[str, Any]) -> List[AgentTask]:
"""Decompose synthesis task into subtasks."""
query = task.input_data.get("query", "")
subtasks = []
# Task 1: Information synthesis
subtasks.append(AgentTask(
id=f"{task.id}_synthesis",
agent_type=AgentType.SYNTHESIS,
description=f"Synthesize information for: {query}",
input_data={"query": query, "task": "information_synthesis"},
dependencies=[],
priority=task.priority,
created_at=datetime.utcnow()
))
# Task 2: Response generation
subtasks.append(AgentTask(
id=f"{task.id}_response",
agent_type=AgentType.SYNTHESIS,
description=f"Generate response for: {query}",
input_data={"query": query, "task": "response_generation"},
dependencies=[f"{task.id}_synthesis"],
priority=task.priority,
created_at=datetime.utcnow()
))
return subtasks
async def _decompose_validation_task(self, task: AgentTask, context: Dict[str, Any]) -> List[AgentTask]:
"""Decompose validation task into subtasks."""
query = task.input_data.get("query", "")
subtasks = []
# Task 1: Quality assessment
subtasks.append(AgentTask(
id=f"{task.id}_quality",
agent_type=AgentType.VALIDATION,
description=f"Assess quality for: {query}",
input_data={"query": query, "task": "quality_assessment"},
dependencies=[],
priority=task.priority,
created_at=datetime.utcnow()
))
# Task 2: Consistency check
subtasks.append(AgentTask(
id=f"{task.id}_consistency",
agent_type=AgentType.VALIDATION,
description=f"Check consistency for: {query}",
input_data={"query": query, "task": "consistency_check"},
dependencies=[f"{task.id}_quality"],
priority=task.priority,
created_at=datetime.utcnow()
))
return subtasks
async def _decompose_generic_task(self, task: AgentTask, context: Dict[str, Any]) -> List[AgentTask]:
"""Generic task decomposition."""
return [task]
class WorkflowExecutor:
"""Executes workflows with parallel task execution."""
def __init__(self, max_workers: int = 10):
self.max_workers = max_workers
self.executor = ThreadPoolExecutor(max_workers=max_workers)
self.active_executions: Dict[str, WorkflowExecution] = {}
async def execute_workflow(
self,
workflow_definition: WorkflowDefinition,
tenant_id: str,
agents: Dict[AgentType, Any]
) -> WorkflowExecution:
"""Execute a workflow with parallel task execution."""
execution = WorkflowExecution(
id=str(uuid.uuid4()),
workflow_definition=workflow_definition,
tenant_id=tenant_id
)
self.active_executions[execution.id] = execution
execution.status = WorkflowStatus.RUNNING
execution.start_time = datetime.utcnow()
try:
# Initialize task status
for task in workflow_definition.tasks:
execution.task_status[task.id] = TaskStatus.PENDING
# Execute tasks with dependency management
await self._execute_tasks_with_dependencies(execution, agents)
# Check if any tasks failed
failed_tasks = [task_id for task_id, status in execution.task_status.items()
if status == TaskStatus.FAILED]
if failed_tasks:
execution.status = WorkflowStatus.FAILED
execution.error = f"Tasks failed: {', '.join(failed_tasks)}"
else:
execution.status = WorkflowStatus.COMPLETED
execution.end_time = datetime.utcnow()
except Exception as e:
logger.error(f"Workflow execution failed: {e}")
execution.status = WorkflowStatus.FAILED
execution.error = str(e)
execution.end_time = datetime.utcnow()
finally:
# Clean up
if execution.id in self.active_executions:
del self.active_executions[execution.id]
return execution
async def _execute_tasks_with_dependencies(
self,
execution: WorkflowExecution,
agents: Dict[AgentType, Any]
):
"""Execute tasks respecting dependencies."""
completed_tasks: Set[str] = set()
running_tasks: Set[str] = set()
while len(completed_tasks) < len(execution.workflow_definition.tasks):
# Find ready tasks
ready_tasks = self._find_ready_tasks(
execution.workflow_definition.tasks,
execution.workflow_definition.dependencies,
completed_tasks,
running_tasks
)
if not ready_tasks and not running_tasks:
# Deadlock or no more tasks
break
# Execute ready tasks in parallel
if ready_tasks:
tasks_to_execute = ready_tasks[:execution.workflow_definition.max_parallel_tasks]
# Start tasks
for task in tasks_to_execute:
running_tasks.add(task.id)
execution.task_status[task.id] = TaskStatus.RUNNING
asyncio.create_task(self._execute_single_task(task, execution, agents))
# Wait for some tasks to complete
await asyncio.sleep(0.1)
# Update completed and failed tasks
for task_id in list(running_tasks):
if execution.task_status[task_id] in [TaskStatus.COMPLETED, TaskStatus.FAILED]:
running_tasks.remove(task_id)
completed_tasks.add(task_id)
def _find_ready_tasks(
self,
tasks: List[AgentTask],
dependencies: Dict[str, List[str]],
completed_tasks: Set[str],
running_tasks: Set[str]
) -> List[AgentTask]:
"""Find tasks that are ready to execute."""
ready_tasks = []
for task in tasks:
if task.id in completed_tasks or task.id in running_tasks:
continue
# Check if all dependencies are completed
task_dependencies = dependencies.get(task.id, [])
if all(dep in completed_tasks for dep in task_dependencies):
ready_tasks.append(task)
# Sort by priority (higher priority first)
ready_tasks.sort(key=lambda t: t.priority, reverse=True)
return ready_tasks
async def _execute_single_task(
self,
task: AgentTask,
execution: WorkflowExecution,
agents: Dict[AgentType, Any]
):
"""Execute a single task."""
try:
# Get the appropriate agent
agent = agents.get(task.agent_type)
if not agent:
raise ValueError(f"No agent found for type: {task.agent_type}")
# Execute task
result = await agent.execute(task)
# Store result
execution.task_results[task.id] = result
execution.task_status[task.id] = TaskStatus.COMPLETED
logger.info(f"Task {task.id} completed successfully")
except Exception as e:
logger.error(f"Task {task.id} failed: {e}")
execution.task_status[task.id] = TaskStatus.FAILED
execution.task_results[task.id] = {"error": str(e)}
class WorkflowMonitor:
"""Monitors workflow execution and provides metrics."""
def __init__(self):
self.execution_history: List[WorkflowExecution] = []
self.metrics = {
"total_executions": 0,
"successful_executions": 0,
"failed_executions": 0,
"average_execution_time": 0.0
}
def record_execution(self, execution: WorkflowExecution):
"""Record a workflow execution."""
self.execution_history.append(execution)
self._update_metrics(execution)
def _update_metrics(self, execution: WorkflowExecution):
"""Update execution metrics."""
self.metrics["total_executions"] += 1
if execution.status == WorkflowStatus.COMPLETED:
self.metrics["successful_executions"] += 1
elif execution.status == WorkflowStatus.FAILED:
self.metrics["failed_executions"] += 1
# Update average execution time
if execution.start_time and execution.end_time:
execution_time = (execution.end_time - execution.start_time).total_seconds()
total_executions = self.metrics["total_executions"]
current_avg = self.metrics["average_execution_time"]
self.metrics["average_execution_time"] = (
(current_avg * (total_executions - 1) + execution_time) / total_executions
)
def get_metrics(self) -> Dict[str, Any]:
"""Get current metrics."""
return self.metrics.copy()
def get_execution_history(self, limit: int = 100) -> List[WorkflowExecution]:
"""Get recent execution history."""
return self.execution_history[-limit:]
class AutonomousWorkflowEngine:
"""Main autonomous workflow engine."""
def __init__(self):
self.task_decomposer = TaskDecomposer()
self.workflow_executor = WorkflowExecutor()
self.workflow_monitor = WorkflowMonitor()
self.workflow_definitions: Dict[str, WorkflowDefinition] = {}
async def create_workflow(
self,
name: str,
description: str,
tasks: List[AgentTask],
dependencies: Optional[Dict[str, List[str]]] = None,
max_parallel_tasks: int = 5,
timeout_seconds: int = 300
) -> WorkflowDefinition:
"""Create a new workflow definition."""
# Validate inputs
if not tasks:
raise ValueError("Workflow must have at least one task")
if not name or not description:
raise ValueError("Workflow name and description are required")
workflow_id = str(uuid.uuid4())
workflow = WorkflowDefinition(
id=workflow_id,
name=name,
description=description,
tasks=tasks,
dependencies=dependencies or {},
max_parallel_tasks=max_parallel_tasks,
timeout_seconds=timeout_seconds
)
self.workflow_definitions[workflow_id] = workflow
return workflow
async def execute_workflow(
self,
workflow_id: str,
tenant_id: str,
agents: Dict[AgentType, Any],
context: Optional[Dict[str, Any]] = None
) -> WorkflowExecution:
"""Execute a workflow."""
workflow = self.workflow_definitions.get(workflow_id)
if not workflow:
raise ValueError(f"Workflow {workflow_id} not found")
# Decompose complex tasks if needed
decomposed_tasks = []
for task in workflow.tasks:
if self._is_complex_task(task):
subtasks = await self.task_decomposer.decompose_task(task, context or {})
decomposed_tasks.extend(subtasks)
else:
decomposed_tasks.append(task)
# Update workflow with decomposed tasks if needed
if decomposed_tasks != workflow.tasks:
workflow.tasks = decomposed_tasks
# Execute workflow
execution = await self.workflow_executor.execute_workflow(
workflow, tenant_id, agents
)
# Record execution
self.workflow_monitor.record_execution(execution)
return execution
def _is_complex_task(self, task: AgentTask) -> bool:
"""Determine if a task is complex and needs decomposition."""
# Simple heuristic: tasks with high priority or complex descriptions
return (
task.priority > 5 or
len(task.description) > 100 or
len(task.input_data) > 10
)
async def get_workflow_status(self, execution_id: str) -> Optional[WorkflowExecution]:
"""Get status of a workflow execution."""
# Check active executions
if execution_id in self.workflow_executor.active_executions:
return self.workflow_executor.active_executions[execution_id]
# Check history
for execution in self.workflow_monitor.execution_history:
if execution.id == execution_id:
return execution
return None
async def cancel_workflow(self, execution_id: str) -> bool:
"""Cancel a running workflow."""
if execution_id in self.workflow_executor.active_executions:
execution = self.workflow_executor.active_executions[execution_id]
execution.status = WorkflowStatus.CANCELLED
execution.end_time = datetime.utcnow()
return True
return False
async def get_metrics(self) -> Dict[str, Any]:
"""Get workflow engine metrics."""
return self.workflow_monitor.get_metrics()
async def get_execution_history(self, limit: int = 100) -> List[WorkflowExecution]:
"""Get execution history."""
return self.workflow_monitor.get_execution_history(limit)
# Global workflow engine instance
autonomous_workflow_engine = AutonomousWorkflowEngine()