workflow_engine.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. import logging
  2. import os
  3. import time
  4. from typing import Any
  5. from app.service.engine.execution_context import ExecutionContext
  6. from app.service.engine.parameter_resolver import ParameterResolver
  7. from app.service.engine.workflow_result import WorkflowResult
  8. from app.service.spec.step import Step
  9. from app.service.spec.workflow import Workflow
  10. from app.service.task.task_executor import TaskExecutor
  11. from app.service.task.task_param import TaskParam
  12. from app.service.task.task_result import TaskResult
  13. from app.util.task_util import create_task_workspace, cleanup_workspace
  14. logger = logging.getLogger(__name__)
  15. class WorkflowEngine:
  16. def __init__(self, task_executor: TaskExecutor):
  17. self._task_executor = task_executor
  18. self._resolver = ParameterResolver()
  19. def execute(
  20. self, task_id: str, workflow: Workflow, inputs: dict[str, Any]
  21. ) -> WorkflowResult:
  22. total_steps = len(workflow.order)
  23. logger.info("========== Workflow start ========== version=%s, total_steps=%d",
  24. workflow.version, total_steps)
  25. wf_t0 = time.time()
  26. ctx = ExecutionContext(inputs)
  27. try:
  28. workspace = create_task_workspace(task_id, workflow.template_path)
  29. logger.info("Workspace created: %s", workspace)
  30. except Exception as e:
  31. logger.error("Failed to create workspace: %s", e)
  32. return WorkflowResult.failure(f"failed: {e}")
  33. for idx, step_id in enumerate(workflow.order, 1):
  34. step = workflow.steps.get(step_id)
  35. if step is None:
  36. logger.warning("Step '%s' not found in steps definition, skipping", step_id)
  37. continue
  38. logger.info("------ Step %d/%d [%s] type=%s start ------",
  39. idx, total_steps, step.id, step.type)
  40. logger.info("[%s] raw_inputs=%s", step.id, step.inputs)
  41. step_t0 = time.time()
  42. try:
  43. step_dir = os.path.join(workspace, step.id)
  44. os.makedirs(step_dir, exist_ok=True)
  45. result = self._execute_step_with_retry(task_id, workspace, step, ctx)
  46. step_elapsed = time.time() - step_t0
  47. if result.is_failure():
  48. logger.error("[%s] FAILED in %.2fs after retries. Error: %s",
  49. step_id, step_elapsed, result.error_message)
  50. return WorkflowResult.failure(
  51. f"Step {step_id} failed: {result.error_message}"
  52. )
  53. except Exception as e:
  54. step_elapsed = time.time() - step_t0
  55. logger.error("[%s] EXCEPTION in %.2fs: %s", step_id, step_elapsed, e, exc_info=True)
  56. return WorkflowResult.failure(f"Step {step_id} failed: {e}")
  57. logger.info("[%s] SUCCESS in %.2fs outputs=%s",
  58. step.id, step_elapsed, _shorten(result.outputs))
  59. ctx.put_step_result(step_id, result.outputs)
  60. logger.info("All steps completed, resolving final outputs ...")
  61. outputs: dict[str, Any] = {}
  62. if workflow.outputs:
  63. for key, value in workflow.outputs.items():
  64. outputs[key] = self._resolver.resolve(value, ctx)
  65. logger.info("Final output_keys=%s", list(outputs.keys()))
  66. cleanup_workspace(workspace, task_id)
  67. total_elapsed = time.time() - wf_t0
  68. logger.info("========== Workflow end ========== total_elapsed=%.2fs, success=True",
  69. total_elapsed)
  70. return WorkflowResult.ok(outputs)
  71. def _execute_step_with_retry(
  72. self, task_id: str, workspace: str, step: Step, ctx: ExecutionContext
  73. ) -> TaskResult:
  74. max_retries = step.retry_count
  75. last_result: TaskResult | None = None
  76. for attempt in range(max_retries + 1):
  77. step_inputs = self._resolve_inputs(step.inputs, ctx)
  78. task = self._task_executor.resolve(step.type)
  79. param = TaskParam(
  80. workspace=workspace,
  81. step_id=step.id,
  82. type=step.type,
  83. resources=step.resources,
  84. inputs=step_inputs,
  85. )
  86. if attempt > 0:
  87. logger.info("[%s] Retry attempt %d/%d",
  88. step.id, attempt, max_retries)
  89. logger.info("[%s] Executing task type=%s, resolved_input_keys=%s",
  90. step.id, step.type, list(step_inputs.keys()))
  91. attempt_t0 = time.time()
  92. last_result = task.execute(param)
  93. attempt_elapsed = time.time() - attempt_t0
  94. if last_result.is_success():
  95. logger.info("[%s] Task returned SUCCESS in %.2fs",
  96. step.id, attempt_elapsed)
  97. return last_result
  98. if attempt < max_retries:
  99. logger.warning(
  100. "[%s] Task returned FAILURE in %.2fs (attempt %d/%d), will retry. Error: %s",
  101. step.id, attempt_elapsed,
  102. attempt + 1, max_retries + 1, last_result.error_message,
  103. )
  104. time.sleep(1)
  105. else:
  106. logger.error(
  107. "[%s] Task returned FAILURE in %.2fs (final attempt %d/%d). Error: %s",
  108. step.id, attempt_elapsed,
  109. attempt + 1, max_retries + 1, last_result.error_message,
  110. )
  111. return last_result # type: ignore[return-value]
  112. def _resolve_inputs(
  113. self, inputs: dict[str, Any], ctx: ExecutionContext
  114. ) -> dict[str, Any]:
  115. return {k: self._resolver.resolve(v, ctx) for k, v in inputs.items()} if inputs else {}
  116. def _shorten(d: dict | None, max_len: int = 120) -> str:
  117. if d is None:
  118. return "null"
  119. s = str(d)
  120. return s if len(s) <= max_len else s[:max_len] + "..."