221 lines
7.7 KiB
Python
Raw Normal View History

2025-11-04 05:17:27 +01:00
import re
2025-11-04 08:09:12 +01:00
import time
2025-11-04 05:17:27 +01:00
from concurrent.futures import ThreadPoolExecutor, as_completed
2025-11-04 08:09:12 +01:00
from typing import Any, Callable, Dict, List, Optional
from .workflow_definition import ExecutionMode, Workflow, WorkflowStep
2025-11-04 05:17:27 +01:00
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]):
2025-11-04 08:09:12 +01:00
self.execution_log.append(
{
"timestamp": time.time(),
"event_type": event_type,
"step_id": step_id,
"details": details,
}
)
2025-11-04 05:17:27 +01:00
class WorkflowEngine:
def __init__(self, tool_executor: Callable, max_workers: int = 5):
self.tool_executor = tool_executor
self.max_workers = max_workers
2025-11-04 08:09:12 +01:00
def _evaluate_condition(
self, condition: str, context: WorkflowExecutionContext
) -> bool:
2025-11-04 05:17:27 +01:00
if not condition:
return True
try:
safe_locals = {
2025-11-04 08:09:12 +01:00
"variables": context.variables,
"results": context.step_results,
2025-11-04 05:17:27 +01:00
}
return eval(condition, {"__builtins__": {}}, safe_locals)
except Exception:
return False
2025-11-04 08:09:12 +01:00
def _substitute_variables(
self, arguments: Dict[str, Any], context: WorkflowExecutionContext
) -> Dict[str, Any]:
2025-11-04 05:17:27 +01:00
substituted = {}
for key, value in arguments.items():
if isinstance(value, str):
2025-11-04 08:09:12 +01:00
pattern = r"\$\{([^}]+)\}"
2025-11-04 05:17:27 +01:00
matches = re.findall(pattern, value)
for match in matches:
2025-11-04 08:09:12 +01:00
if match.startswith("step."):
step_id = match.split(".", 1)[1]
2025-11-04 05:17:27 +01:00
replacement = context.get_step_result(step_id)
if replacement is not None:
2025-11-04 08:09:12 +01:00
value = value.replace(f"${{{match}}}", str(replacement))
elif match.startswith("var."):
var_name = match.split(".", 1)[1]
2025-11-04 05:17:27 +01:00
replacement = context.get_variable(var_name)
if replacement is not None:
2025-11-04 08:09:12 +01:00
value = value.replace(f"${{{match}}}", str(replacement))
2025-11-04 05:17:27 +01:00
substituted[key] = value
else:
substituted[key] = value
return substituted
2025-11-04 08:09:12 +01:00
def _execute_step(
self, step: WorkflowStep, context: WorkflowExecutionContext
) -> Dict[str, Any]:
2025-11-04 05:17:27 +01:00
if not self._evaluate_condition(step.condition, context):
2025-11-04 08:09:12 +01:00
context.log_event("skipped", step.step_id, {"reason": "condition_not_met"})
return {"status": "skipped", "step_id": step.step_id}
2025-11-04 05:17:27 +01:00
arguments = self._substitute_variables(step.arguments, context)
start_time = time.time()
retry_attempts = 0
last_error = None
while retry_attempts <= step.retry_count:
try:
2025-11-04 08:09:12 +01:00
context.log_event(
"executing",
step.step_id,
{
"tool": step.tool_name,
"arguments": arguments,
"attempt": retry_attempts + 1,
},
)
2025-11-04 05:17:27 +01:00
result = self.tool_executor(step.tool_name, arguments)
execution_time = time.time() - start_time
context.set_step_result(step.step_id, result)
2025-11-04 08:09:12 +01:00
context.log_event(
"completed",
step.step_id,
{
"execution_time": execution_time,
"result_size": len(str(result)) if result else 0,
},
)
2025-11-04 05:17:27 +01:00
return {
2025-11-04 08:09:12 +01:00
"status": "success",
"step_id": step.step_id,
"result": result,
"execution_time": execution_time,
2025-11-04 05:17:27 +01:00
}
except Exception as e:
last_error = str(e)
retry_attempts += 1
if retry_attempts <= step.retry_count:
time.sleep(1 * retry_attempts)
2025-11-04 08:09:12 +01:00
context.log_event("failed", step.step_id, {"error": last_error})
2025-11-04 05:17:27 +01:00
return {
2025-11-04 08:09:12 +01:00
"status": "failed",
"step_id": step.step_id,
"error": last_error,
"execution_time": time.time() - start_time,
2025-11-04 05:17:27 +01:00
}
2025-11-04 08:09:12 +01:00
def _get_next_steps(
self, completed_step: WorkflowStep, result: Dict[str, Any], workflow: Workflow
) -> List[WorkflowStep]:
2025-11-04 05:17:27 +01:00
next_steps = []
2025-11-04 08:09:12 +01:00
if result["status"] == "success" and completed_step.on_success:
2025-11-04 05:17:27 +01:00
for step_id in completed_step.on_success:
step = workflow.get_step(step_id)
if step:
next_steps.append(step)
2025-11-04 08:09:12 +01:00
elif result["status"] == "failed" and completed_step.on_failure:
2025-11-04 05:17:27 +01:00
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
2025-11-04 08:09:12 +01:00
def execute_workflow(
self, workflow: Workflow, initial_variables: Optional[Dict[str, Any]] = None
) -> WorkflowExecutionContext:
2025-11-04 05:17:27 +01:00
context = WorkflowExecutionContext()
if initial_variables:
context.variables.update(initial_variables)
if workflow.variables:
context.variables.update(workflow.variables)
2025-11-04 08:09:12 +01:00
context.log_event("workflow_started", "workflow", {"name": workflow.name})
2025-11-04 05:17:27 +01:00
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()
2025-11-04 08:09:12 +01:00
context.log_event("step_completed", step.step_id, result)
2025-11-04 05:17:27 +01:00
except Exception as e:
2025-11-04 08:09:12 +01:00
context.log_event(
"step_failed", step.step_id, {"error": str(e)}
)
2025-11-04 05:17:27 +01:00
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)
2025-11-04 08:09:12 +01:00
context.log_event(
"workflow_completed",
"workflow",
{
"total_steps": len(context.step_results),
"executed_steps": list(context.step_results.keys()),
},
)
2025-11-04 05:17:27 +01:00
return context