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:10:37 +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:10:37 +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
|