""" Hermes Agent Orchestrator - Enhanced with true workflow orchestration capabilities Implements workflow parsing, parallel execution, and skill-based automation """ import asyncio import json import uuid from appPublic.uniqueID import getID from typing import Dict, Any, List, Optional, Tuple from datetime import datetime from dataclasses import dataclass # Import required dependencies try: from ahserver.serverenv import ServerEnv from appPublic.worker import awaitify from sqlor.dbpools import DBPools except ImportError: # For standalone testing class ServerEnv: def __init__(self): pass def awaitify(func): async def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper class DBPools: def __init__(self): pass def getConfig(): class Config: databases = None return Config() @dataclass class TaskDefinition: """Task definition structure for workflow execution""" id: str task_name: str task_type: str skill_name: Optional[str] = None tool_name: Optional[str] = None parameters: Dict[str, Any] = None depends_on: Optional[str] = None parallel_group: Optional[str] = None timeout_seconds: int = 300 retry_count: int = 2 order_index: int = 0 @dataclass class WorkflowDefinition: """Workflow definition structure""" id: str name: str description: str = "" workflow_type: str = "sequential" max_concurrent_tasks: int = 3 timeout_seconds: int = 1800 retry_count: int = 2 tasks: List[TaskDefinition] = None class HermesOrchestrator: """Core orchestrator implementation with workflow execution capabilities""" def __init__(self, harnessed_agent_instance): self.harnessed_agent = harnessed_agent_instance def _get_current_user_id(self, context: Dict[str, Any]) -> str: """Get current user ID from request context""" user_id = context.get('user_id') or context.get('userid') if not user_id: raise ValueError("User ID not found in context. User must be authenticated.") return str(user_id) async def create_workflow(self, name: str, description: str = "", workflow_type: str = "sequential", max_concurrent_tasks: int = 3, timeout_seconds: int = 1800, retry_count: int = 2, context: Dict[str, Any] = None) -> Dict[str, Any]: """Create a new workflow definition""" user_id = self._get_current_user_id(context) if context else "anonymous" try: workflow_id = getID() env = ServerEnv() dbname = env.get_module_dbname('harnessed_agent') config = getConfig() db = DBPools() db.databases = config.databases async with db.sqlorContext(dbname) as sor: data = { 'id': workflow_id, 'user_id': user_id, 'name': name, 'description': description, 'workflow_type': workflow_type, 'max_concurrent_tasks': max_concurrent_tasks, 'timeout_seconds': timeout_seconds, 'retry_count': retry_count, 'status': 'active', 'created_at': datetime.now(), 'updated_at': datetime.now() } result = await sor.C('hermes_workflows', data) return {"success": True, "workflow_id": workflow_id, "user_id": user_id} except Exception as e: return {"success": False, "error": str(e), "user_id": user_id} async def add_task_to_workflow(self, workflow_id: str, task_name: str, task_type: str, skill_name: str = None, tool_name: str = None, parameters: Dict[str, Any] = None, depends_on: str = None, parallel_group: str = None, timeout_seconds: int = 300, retry_count: int = 2, order_index: int = 0, context: Dict[str, Any] = None) -> Dict[str, Any]: """Add a task to an existing workflow""" user_id = self._get_current_user_id(context) if context else "anonymous" try: # Verify workflow exists and belongs to user env = ServerEnv() dbname = env.get_module_dbname('harnessed_agent') config = getConfig() db = DBPools() db.databases = config.databases async with db.sqlorContext(dbname) as sor: workflows = await sor.R('hermes_workflows', { 'id': workflow_id, 'user_id': user_id }) if not workflows: return {"success": False, "error": "Workflow not found or access denied"} task_id = getID() data = { 'id': task_id, 'user_id': user_id, 'workflow_id': workflow_id, 'task_name': task_name, 'task_type': task_type, 'skill_name': skill_name, 'tool_name': tool_name, 'parameters_json': json.dumps(parameters) if parameters else None, 'depends_on': depends_on, 'parallel_group': parallel_group, 'timeout_seconds': timeout_seconds, 'retry_count': retry_count, 'order_index': order_index, 'created_at': datetime.now(), 'updated_at': datetime.now() } result = await sor.C('hermes_tasks', data) return {"success": True, "task_id": task_id, "workflow_id": workflow_id, "user_id": user_id} except Exception as e: return {"success": False, "error": str(e), "user_id": user_id} async def execute_workflow(self, workflow_id: str, context: Dict[str, Any] = None) -> Dict[str, Any]: """Execute a complete workflow with proper orchestration""" user_id = self._get_current_user_id(context) if context else "anonymous" try: # Load workflow definition workflow_def = await self._load_workflow_definition(workflow_id, user_id) if not workflow_def["success"]: return workflow_def workflow = workflow_def["workflow"] # Execute based on workflow type if workflow.workflow_type == "sequential": result = await self._execute_sequential_workflow(workflow, user_id, context) elif workflow.workflow_type == "parallel": result = await self._execute_parallel_workflow(workflow, user_id, context) elif workflow.workflow_type == "hybrid": result = await self._execute_hybrid_workflow(workflow, user_id, context) else: return {"success": False, "error": f"Unknown workflow type: {workflow.workflow_type}"} return result except Exception as e: return {"success": False, "error": str(e), "user_id": user_id} async def _load_workflow_definition(self, workflow_id: str, user_id: str) -> Dict[str, Any]: """Load complete workflow definition with all tasks""" try: env = ServerEnv() dbname = env.get_module_dbname('harnessed_agent') config = getConfig() db = DBPools() db.databases = config.databases async with db.sqlorContext(dbname) as sor: # Load workflow workflows = await sor.R('hermes_workflows', { 'id': workflow_id, 'user_id': user_id }) if not workflows: return {"success": False, "error": "Workflow not found"} workflow_data = workflows[0] # Load tasks tasks = await sor.R('hermes_tasks', { 'workflow_id': workflow_id, 'user_id': user_id, 'sort': 'order_index asc' }) # Convert to TaskDefinition objects task_definitions = [] for task_data in tasks: task_def = TaskDefinition( id=task_data['id'], task_name=task_data['task_name'], task_type=task_data['task_type'], skill_name=task_data.get('skill_name'), tool_name=task_data.get('tool_name'), parameters=json.loads(task_data['parameters_json']) if task_data.get('parameters_json') else {}, depends_on=task_data.get('depends_on'), parallel_group=task_data.get('parallel_group'), timeout_seconds=task_data['timeout_seconds'], retry_count=task_data['retry_count'], order_index=task_data['order_index'] ) task_definitions.append(task_def) workflow_def = WorkflowDefinition( id=workflow_data['id'], name=workflow_data['name'], description=workflow_data['description'], workflow_type=workflow_data['workflow_type'], max_concurrent_tasks=workflow_data['max_concurrent_tasks'], timeout_seconds=workflow_data['timeout_seconds'], retry_count=workflow_data['retry_count'], tasks=task_definitions ) return {"success": True, "workflow": workflow_def} except Exception as e: return {"success": False, "error": str(e)} async def _execute_sequential_workflow(self, workflow: WorkflowDefinition, user_id: str, context: Dict[str, Any]) -> Dict[str, Any]: """Execute workflow tasks sequentially""" results = [] task_results = {} for task in workflow.tasks: # Check dependencies if task.depends_on and task.depends_on not in task_results: return {"success": False, "error": f"Dependency task {task.depends_on} not found", "user_id": user_id} if task.depends_on and not task_results.get(task.depends_on, {}).get("success"): return {"success": False, "error": f"Dependency task {task.depends_on} failed", "user_id": user_id} # Execute task with retries task_result = await self._execute_task_with_retries(task, user_id, context, workflow.retry_count) task_results[task.id] = task_result results.append(task_result) if not task_result["success"]: return {"success": False, "error": f"Task {task.task_name} failed: {task_result.get('error', 'Unknown error')}", "results": results, "user_id": user_id} return {"success": True, "results": results, "user_id": user_id} async def _execute_parallel_workflow(self, workflow: WorkflowDefinition, user_id: str, context: Dict[str, Any]) -> Dict[str, Any]: """Execute workflow tasks in parallel (up to max_concurrent_tasks)""" semaphore = asyncio.Semaphore(workflow.max_concurrent_tasks) results = [] task_futures = [] async def execute_task_limited(task): async with semaphore: return await self._execute_task_with_retries(task, user_id, context, workflow.retry_count) # Create tasks for all workflow tasks for task in workflow.tasks: future = asyncio.create_task(execute_task_limited(task)) task_futures.append((task.id, future)) # Wait for all tasks to complete for task_id, future in task_futures: try: result = await future results.append(result) if not result["success"]: # Continue to let other tasks finish, but mark overall failure pass except Exception as e: error_result = {"success": False, "error": str(e), "task_id": task_id} results.append(error_result) # Check if any task failed any_failed = any(not r["success"] for r in results) if any_failed: return {"success": False, "results": results, "user_id": user_id} else: return {"success": True, "results": results, "user_id": user_id} async def _execute_hybrid_workflow(self, workflow: WorkflowDefinition, user_id: str, context: Dict[str, Any]) -> Dict[str, Any]: """Execute hybrid workflow with both sequential and parallel groups""" # Group tasks by parallel_group groups = {} sequential_tasks = [] for task in workflow.tasks: if task.parallel_group: if task.parallel_group not in groups: groups[task.parallel_group] = [] groups[task.parallel_group].append(task) else: sequential_tasks.append(task) results = [] task_results = {} # Execute sequential tasks first (including parallel groups as single units) all_execution_units = [] # Add individual sequential tasks for task in sequential_tasks: all_execution_units.append(("sequential", task)) # Add parallel groups for group_name, group_tasks in groups.items(): all_execution_units.append(("parallel_group", group_name, group_tasks)) # Sort by order_index of first task in each unit def get_order_key(unit): if unit[0] == "sequential": return unit[1].order_index else: return min(task.order_index for task in unit[2]) all_execution_units.sort(key=get_order_key) # Execute units in order for unit in all_execution_units: if unit[0] == "sequential": task = unit[1] # Check dependencies if task.depends_on and task.depends_on not in task_results: return {"success": False, "error": f"Dependency task {task.depends_on} not found", "user_id": user_id} if task.depends_on and not task_results.get(task.depends_on, {}).get("success"): return {"success": False, "error": f"Dependency task {task.depends_on} failed", "user_id": user_id} task_result = await self._execute_task_with_retries(task, user_id, context, workflow.retry_count) task_results[task.id] = task_result results.append(task_result) if not task_result["success"]: return {"success": False, "error": f"Task {task.task_name} failed", "results": results, "user_id": user_id} else: # parallel_group group_name = unit[1] group_tasks = unit[2] # Check dependencies for all tasks in group for task in group_tasks: if task.depends_on and task.depends_on not in task_results: return {"success": False, "error": f"Dependency task {task.depends_on} not found in group {group_name}", "user_id": user_id} if task.depends_on and not task_results.get(task.depends_on, {}).get("success"): return {"success": False, "error": f"Dependency task {task.depends_on} failed in group {group_name}", "user_id": user_id} # Execute group in parallel group_results = await self._execute_parallel_task_group(group_tasks, user_id, context, workflow.retry_count) results.extend(group_results) # Store individual task results for i, task in enumerate(group_tasks): task_results[task.id] = group_results[i] # Check if any task in group failed if any(not r["success"] for r in group_results): return {"success": False, "error": f"Parallel group {group_name} failed", "results": results, "user_id": user_id} return {"success": True, "results": results, "user_id": user_id} async def _execute_parallel_task_group(self, tasks: List[TaskDefinition], user_id: str, context: Dict[str, Any], max_retries: int) -> List[Dict[str, Any]]: """Execute a group of tasks in parallel""" semaphore = asyncio.Semaphore(len(tasks)) # Allow all tasks in group to run concurrently async def execute_task_limited(task): async with semaphore: return await self._execute_task_with_retries(task, user_id, context, max_retries) futures = [asyncio.create_task(execute_task_limited(task)) for task in tasks] results = [] for future in futures: try: result = await future results.append(result) except Exception as e: results.append({"success": False, "error": str(e)}) return results async def _execute_task_with_retries(self, task: TaskDefinition, user_id: str, context: Dict[str, Any], max_retries: int) -> Dict[str, Any]: """Execute a single task with retry logic""" execution_id = getID() # Record execution start await self._record_execution_start(execution_id, user_id, task, context) last_error = None for attempt in range(max_retries + 1): try: if attempt > 0: # Wait before retry (exponential backoff) await asyncio.sleep(2 ** attempt) # Execute the actual task result = await self._execute_single_task(task, user_id, context) # Record successful execution await self._record_execution_end(execution_id, user_id, "completed", result, None, attempt) return result except Exception as e: last_error = str(e) if attempt < max_retries: continue else: # Record failed execution await self._record_execution_end(execution_id, user_id, "failed", None, last_error, attempt) return {"success": False, "error": last_error, "task_id": task.id, "attempts": attempt + 1} # This should never be reached return {"success": False, "error": "Unexpected execution state", "task_id": task.id} async def _execute_single_task(self, task: TaskDefinition, user_id: str, context: Dict[str, Any]) -> Dict[str, Any]: """Execute a single task based on its type""" if task.task_type == "skill": if not task.skill_name: return {"success": False, "error": "Skill name required for skill task type"} return await self.harnessed_agent.manage_skills("view", task.skill_name, context=context) elif task.task_type == "tool": if not task.tool_name: return {"success": False, "error": "Tool name required for tool task type"} return await self.harnessed_agent.execute_tool_call(task.tool_name, task.parameters or {}, context=context) elif task.task_type == "memory": # Memory operations require specific action parameter action = task.parameters.get("action") if task.parameters else None if not action: return {"success": False, "error": "Memory action required (add/replace/remove)"} return await self.harnessed_agent.manage_memory( action, task.parameters.get("target", "memory"), task.parameters.get("content", ""), task.parameters.get("old_text", ""), context=context, priority=task.parameters.get("priority") ) elif task.task_type == "session_search": query = task.parameters.get("query", "") if task.parameters else "" limit = task.parameters.get("limit", 3) if task.parameters else 3 return await self.harnessed_agent.search_sessions(query, limit, context=context) elif task.task_type == "custom": # Custom script execution would go here return {"success": True, "result": "Custom task executed", "task_id": task.id} else: return {"success": False, "error": f"Unknown task type: {task.task_type}"} async def _record_execution_start(self, execution_id: str, user_id: str, task: TaskDefinition, context: Dict[str, Any]): """Record execution start in database""" try: env = ServerEnv() dbname = env.get_module_dbname('harnessed_agent') config = getConfig() db = DBPools() db.databases = config.databases async with db.sqlorContext(dbname) as sor: data = { 'id': execution_id, 'user_id': user_id, 'workflow_id': task.workflow_id if hasattr(task, 'workflow_id') else "", 'task_id': task.id, 'execution_status': 'running', 'start_time': datetime.now(), 'created_at': datetime.now(), 'updated_at': datetime.now() } await sor.C('hermes_executions', data) except Exception: # Silently ignore recording errors pass async def _record_execution_end(self, execution_id: str, user_id: str, status: str, result: Dict[str, Any], error: str, retry_count: int): """Record execution end in database""" try: env = ServerEnv() dbname = env.get_module_dbname('harnessed_agent') config = getConfig() db = DBPools() db.databases = config.databases async with db.sqlorContext(dbname) as sor: end_time = datetime.now() data = { 'id': execution_id, 'user_id': user_id, 'execution_status': status, 'end_time': end_time, 'duration_seconds': None, # Will be calculated 'result_json': json.dumps(result) if result else None, 'error_message': error, 'retry_count': retry_count, 'updated_at': end_time } # Get start time to calculate duration executions = await sor.R('hermes_executions', {'id': execution_id, 'user_id': user_id}) if executions and executions[0].get('start_time'): start_time = executions[0]['start_time'] if isinstance(start_time, str): start_time = datetime.fromisoformat(start_time.replace('Z', '+00:00')) duration = (end_time - start_time).total_seconds() data['duration_seconds'] = int(duration) await sor.U('hermes_executions', data) except Exception: # Silently ignore recording errors pass # Global orchestrator instance _orchestrator_instance = None def get_hermes_orchestrator(harnessed_agent_instance): """Get or create the global orchestrator instance""" global _orchestrator_instance if _orchestrator_instance is None: _orchestrator_instance = HermesOrchestrator(harnessed_agent_instance) return _orchestrator_instance