MVP / dev-agent /app /validators /workflow_validator.py
github-actions[bot]
Deploy application
595d60e
Raw History Blame Contribute Delete
5 kB
import re
from typing import List, Set, Dict, Any
from ..schemas.agent_spec import AgentSpec
from ..schemas.conversation import ValidationError, ValidationResult
class WorkflowValidator:
def validate(self, spec: AgentSpec) -> ValidationResult:
errors = []
steps_dict = {s.id: s for s in spec.steps}
step_ids = list(steps_dict.keys())
# 1. Duplicate step IDs
all_ids = [s.id for s in spec.steps]
if len(all_ids) != len(set(all_ids)):
errors.append(ValidationError(
code="DUPLICATE_STEP_ID",
message="Duplicate step IDs found in the workflow",
path="steps"
))
# 2. Depends_on exists
for step in spec.steps:
for dep in step.depends_on:
if dep not in steps_dict:
errors.append(ValidationError(
code="MISSING_DEPENDENCY",
message=f"Step '{step.id}' depends on missing step '{dep}'",
path=f"steps.{step.id}.depends_on"
))
# 3. Circular dependency detection (Cycle Detection using DFS)
visited = {} # state: 0=unvisited, 1=visiting, 2=visited
for s_id in step_ids:
visited[s_id] = 0
def dfs(s_id: str, path: List[str]) -> bool:
visited[s_id] = 1 # visiting
step = steps_dict[s_id]
for dep in step.depends_on:
if dep not in steps_dict:
continue
if visited[dep] == 1:
cycle_path = " -> ".join(path + [dep])
errors.append(ValidationError(
code="CIRCULAR_DEPENDENCY",
message=f"Circular dependency detected: {cycle_path}",
path=f"steps.{s_id}.depends_on"
))
return True
elif visited[dep] == 0:
if dfs(dep, path + [dep]):
return True
visited[s_id] = 2 # visited
return False
for s_id in step_ids:
if visited[s_id] == 0:
dfs(s_id, [s_id])
# 4. Helper to find all ancestors of a step
def get_all_ancestors(s_id: str, memo: Dict[str, Set[str]]) -> Set[str]:
if s_id in memo:
return memo[s_id]
ancestors = set()
step = steps_dict.get(s_id)
if step:
for dep in step.depends_on:
ancestors.add(dep)
ancestors.update(get_all_ancestors(dep, memo))
memo[s_id] = ancestors
return ancestors
ancestors_memo = {}
# 5. Data flow validation (regex check for $step_id.field references)
ref_pattern = re.compile(r"^\$(\w+)(?:\.(\w+))?$")
def scan_for_refs(val: Any) -> List[tuple]:
refs = []
if isinstance(val, str):
match = ref_pattern.match(val)
if match:
refs.append((match.group(1), match.group(2)))
elif isinstance(val, dict):
for k, v in val.items():
refs.extend(scan_for_refs(v))
elif isinstance(val, list):
for item in val:
refs.extend(scan_for_refs(item))
return refs
for step in spec.steps:
refs = scan_for_refs(step.input)
for ref_step_id, ref_field in refs:
if ref_step_id not in steps_dict:
errors.append(ValidationError(
code="INVALID_DATA_FLOW",
message=f"Step '{step.id}' references input from non-existent step '{ref_step_id}'",
path=f"steps.{step.id}.input"
))
continue
ancestors = get_all_ancestors(step.id, ancestors_memo)
if ref_step_id != step.id and ref_step_id not in ancestors:
errors.append(ValidationError(
code="INVALID_DATA_FLOW",
message=f"Step '{step.id}' references output of step '{ref_step_id}' but does not depend on it",
path=f"steps.{step.id}.depends_on"
))
ref_step = steps_dict[ref_step_id]
if ref_field and ref_step.output:
if ref_field not in ref_step.output:
errors.append(ValidationError(
code="INVALID_DATA_FLOW",
message=f"Step '{step.id}' references field '{ref_field}' which is not defined in step '{ref_step_id}' outputs",
path=f"steps.{step.id}.input"
))
return ValidationResult(valid=len(errors) == 0, errors=errors)