import time import re from typing import Dict, Any, List, Callable, Optional from concurrent.futures import ThreadPoolExecutor, as_completed from .workflow_definition import Workflow, WorkflowStep, ExecutionMode class WorkflowExecutionContext: def __init__(self): self.variables: Dict[str, Any] = {} self.step_results: Dict[str, Any] = {} self.execution_log: List[Dict[str, Any]] = [] def set_variable(self, name: str, value: Any): self.variables[name] = value def get_variable(self, name: str, default: Any = None) -> Any: return self.variables.get(name, default) def set_step_result(self, step_id: str, result: Any): self.step_results[step_id] = result def get_step_result(self, step_id: str) -> Any: return self.step_results.get(step_id) def log_event(self, event_type: str, step_id: str, details: Dict[str, Any]): self.execution_log.append({ 'timestamp': time.time(), 'event_type': event_type, 'step_id': step_id, 'details': details }) class WorkflowEngine: def __init__(self, tool_executor: Callable, max_workers: int = 5): self.tool_executor = tool_executor self.max_workers = max_workers def _evaluate_condition(self, condition: str, context: WorkflowExecutionContext) -> bool: if not condition: return True try: safe_locals = { 'variables': context.variables, 'results': context.step_results } return eval(condition, {"__builtins__": {}}, safe_locals) except Exception: return False def _substitute_variables(self, arguments: Dict[str, Any], context: WorkflowExecutionContext) -> Dict[str, Any]: substituted = {} for key, value in arguments.items(): if isinstance(value, str): pattern = r'\$\{([^}]+)\}' matches = re.findall(pattern, value) for match in matches: if match.startswith('step.'): step_id = match.split('.', 1)[1] replacement = context.get_step_result(step_id) if replacement is not None: value = value.replace(f'${{{match}}}', str(replacement)) elif match.startswith('var.'): var_name = match.split('.', 1)[1] replacement = context.get_variable(var_name) if replacement is not None: value = value.replace(f'${{{match}}}', str(replacement)) substituted[key] = value else: substituted[key] = value return substituted def _execute_step(self, step: WorkflowStep, context: WorkflowExecutionContext) -> Dict[str, Any]: if not self._evaluate_condition(step.condition, context): context.log_event('skipped', step.step_id, {'reason': 'condition_not_met'}) return {'status': 'skipped', 'step_id': step.step_id} arguments = self._substitute_variables(step.arguments, context) start_time = time.time() retry_attempts = 0 last_error = None while retry_attempts <= step.retry_count: try: context.log_event('executing', step.step_id, { 'tool': step.tool_name, 'arguments': arguments, 'attempt': retry_attempts + 1 }) result = self.tool_executor(step.tool_name, arguments) execution_time = time.time() - start_time context.set_step_result(step.step_id, result) context.log_event('completed', step.step_id, { 'execution_time': execution_time, 'result_size': len(str(result)) if result else 0 }) return { 'status': 'success', 'step_id': step.step_id, 'result': result, 'execution_time': execution_time } except Exception as e: last_error = str(e) retry_attempts += 1 if retry_attempts <= step.retry_count: time.sleep(1 * retry_attempts) context.log_event('failed', step.step_id, {'error': last_error}) return { 'status': 'failed', 'step_id': step.step_id, 'error': last_error, 'execution_time': time.time() - start_time } def _get_next_steps(self, completed_step: WorkflowStep, result: Dict[str, Any], workflow: Workflow) -> List[WorkflowStep]: next_steps = [] if result['status'] == 'success' and completed_step.on_success: for step_id in completed_step.on_success: step = workflow.get_step(step_id) if step: next_steps.append(step) elif result['status'] == 'failed' and completed_step.on_failure: for step_id in completed_step.on_failure: step = workflow.get_step(step_id) if step: next_steps.append(step) elif workflow.execution_mode == ExecutionMode.SEQUENTIAL: current_index = workflow.steps.index(completed_step) if current_index + 1 < len(workflow.steps): next_steps.append(workflow.steps[current_index + 1]) return next_steps def execute_workflow(self, workflow: Workflow, initial_variables: Optional[Dict[str, Any]] = None) -> WorkflowExecutionContext: context = WorkflowExecutionContext() if initial_variables: context.variables.update(initial_variables) if workflow.variables: context.variables.update(workflow.variables) context.log_event('workflow_started', 'workflow', {'name': workflow.name}) if workflow.execution_mode == ExecutionMode.PARALLEL: with ThreadPoolExecutor(max_workers=self.max_workers) as executor: futures = { executor.submit(self._execute_step, step, context): step for step in workflow.steps } for future in as_completed(futures): step = futures[future] try: result = future.result() context.log_event('step_completed', step.step_id, result) except Exception as e: context.log_event('step_failed', step.step_id, {'error': str(e)}) else: pending_steps = workflow.get_initial_steps() executed_step_ids = set() while pending_steps: step = pending_steps.pop(0) if step.step_id in executed_step_ids: continue result = self._execute_step(step, context) executed_step_ids.add(step.step_id) next_steps = self._get_next_steps(step, result, workflow) pending_steps.extend(next_steps) context.log_event('workflow_completed', 'workflow', { 'total_steps': len(context.step_results), 'executed_steps': list(context.step_results.keys()) }) return context