Files
ai-video-fullstack/backend/services/workflow/models.py
2026-08-01 11:21:31 +08:00

151 lines
4.0 KiB
Python

"""Readable runtime values shared by Workflow orchestration modules."""
from __future__ import annotations
from dataclasses import dataclass
from enum import StrEnum
from typing import Any
class WorkflowStatus(StrEnum):
"""The small set of states useful to operators and future debug tooling."""
STARTING = "starting"
WAITING_USER = "waiting_user"
ROUTING = "routing"
RUNNING_AGENT = "running_agent"
RUNNING_ACTION = "running_action"
HANDOFF = "handoff"
ENDED = "ended"
class RouteStatus(StrEnum):
"""A routing error is deliberately different from a valid no-match."""
MATCHED = "matched"
NO_MATCH = "no_match"
ERROR = "error"
class ActionStatus(StrEnum):
"""Stable Action outcomes used by routing and future debug tooling."""
SUCCESS = "success"
FAILURE = "failure"
CANCELLED = "cancelled"
@dataclass(frozen=True)
class ActionError:
"""Machine-readable failure details without losing the operator message."""
code: str
message: str
retryable: bool = False
@dataclass(frozen=True)
class ActionOutcome:
"""One completed Action invocation.
``result`` remains an in-memory value because a tool response may contain
private business data. Trace events publish only its shape and variable
names, never the raw response.
"""
invocation_id: str
status: ActionStatus
duration_ms: int
result: dict[str, Any] | None = None
updated_variables: tuple[str, ...] = ()
error: ActionError | None = None
@property
def should_route(self) -> bool:
"""Cancellation is a lifecycle outcome, not a failure branch."""
return self.status != ActionStatus.CANCELLED
def trace_payload(self) -> dict[str, Any]:
"""Return a persistence-safe summary of the execution result."""
payload: dict[str, Any] = {
"invocationId": self.invocation_id,
"status": self.status.value,
"durationMs": self.duration_ms,
"updatedVariables": list(self.updated_variables),
}
if self.result is not None:
payload["resultKeys"] = sorted(str(key) for key in self.result)
if self.error is not None:
payload["error"] = {
"code": self.error.code,
"message": self.error.message,
"retryable": self.error.retryable,
}
return payload
@dataclass(frozen=True)
class UserTurn:
"""One committed user turn that may cross automatic Workflow nodes."""
id: int
text: str
@dataclass
class WorkflowRuntimeState:
"""Mutable per-call state; graph definitions remain immutable."""
current_node_id: str
status: WorkflowStatus = WorkflowStatus.STARTING
pending_user_turn: UserTurn | None = None
transition_id: int = 0
automatic_hops: int = 0
ended: bool = False
_next_turn_id: int = 1
def begin_user_turn(self, text: str) -> UserTurn:
turn = UserTurn(id=self._next_turn_id, text=text)
self._next_turn_id += 1
self.pending_user_turn = turn
self.automatic_hops = 0
return turn
def enter(self, node_id: str, status: WorkflowStatus) -> None:
self.current_node_id = node_id
self.status = status
def begin_transition(self) -> int:
self.transition_id += 1
return self.transition_id
def consume_user_turn(self) -> UserTurn | None:
turn = self.pending_user_turn
self.pending_user_turn = None
return turn
def finish(self) -> None:
self.ended = True
self.status = WorkflowStatus.ENDED
self.pending_user_turn = None
@dataclass(frozen=True)
class LLMRouteResult:
"""Control-plane result returned by the small routing LLM."""
status: RouteStatus
function_name: str | None = None
error: str | None = None
@dataclass(frozen=True)
class EdgeEvaluation:
"""Final graph-level decision after expression, LLM and default handling."""
status: RouteStatus
edge: dict[str, Any] | None = None
error: str | None = None