generated from coulomb/repo-seed
Workflow layer: gates, decisions, lineage audits, workflow test
This commit is contained in:
@@ -43,8 +43,14 @@ from .relationships import (
|
||||
from .retrieval_feedback import RetrievalFeedbackLabel, RetrievalFeedbackRecord
|
||||
from .transformations import TransformationOperation, TransformationRun, TransformationRunStatus
|
||||
from .workflow_jobs import (
|
||||
WorkflowExceptionKind,
|
||||
WorkflowExceptionRecord,
|
||||
WorkflowExceptionStatus,
|
||||
WorkflowInputDefinition,
|
||||
WorkflowInputKind,
|
||||
WorkflowReviewDecisionType,
|
||||
WorkflowReviewStatus,
|
||||
WorkflowReviewTask,
|
||||
WorkflowRun,
|
||||
WorkflowRunStatus,
|
||||
WorkflowStepDefinition,
|
||||
@@ -97,8 +103,14 @@ __all__ = [
|
||||
"TransformationRun",
|
||||
"TransformationRunStatus",
|
||||
"VersionChangeType",
|
||||
"WorkflowExceptionKind",
|
||||
"WorkflowExceptionRecord",
|
||||
"WorkflowExceptionStatus",
|
||||
"WorkflowInputDefinition",
|
||||
"WorkflowInputKind",
|
||||
"WorkflowReviewDecisionType",
|
||||
"WorkflowReviewStatus",
|
||||
"WorkflowReviewTask",
|
||||
"WorkflowRun",
|
||||
"WorkflowRunStatus",
|
||||
"WorkflowStepDefinition",
|
||||
|
||||
@@ -38,6 +38,37 @@ class WorkflowStepRunStatus(str, Enum):
|
||||
CANCELED = "canceled"
|
||||
|
||||
|
||||
class WorkflowReviewDecisionType(str, Enum):
|
||||
CONTINUE = "continue"
|
||||
REJECT = "reject"
|
||||
CORRECT = "correct"
|
||||
RETRY = "retry"
|
||||
ESCALATE = "escalate"
|
||||
|
||||
|
||||
class WorkflowReviewStatus(str, Enum):
|
||||
OPEN = "open"
|
||||
CONTINUED = "continued"
|
||||
REJECTED = "rejected"
|
||||
CORRECTED = "corrected"
|
||||
RETRY_REQUESTED = "retry_requested"
|
||||
ESCALATED = "escalated"
|
||||
|
||||
|
||||
class WorkflowExceptionKind(str, Enum):
|
||||
FAILED = "failed"
|
||||
BLOCKED = "blocked"
|
||||
LOW_CONFIDENCE = "low_confidence"
|
||||
POLICY_CONFLICT = "policy_conflict"
|
||||
REVIEW_REQUIRED = "review_required"
|
||||
|
||||
|
||||
class WorkflowExceptionStatus(str, Enum):
|
||||
OPEN = "open"
|
||||
RESOLVED = "resolved"
|
||||
ESCALATED = "escalated"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WorkflowInputDefinition:
|
||||
name: str
|
||||
@@ -81,6 +112,7 @@ class WorkflowStepDefinition:
|
||||
parameters: dict[str, Any] = field(default_factory=dict)
|
||||
outputs: dict[str, Any] = field(default_factory=dict)
|
||||
preconditions: tuple[dict[str, Any], ...] = ()
|
||||
review_gate: dict[str, Any] = field(default_factory=dict)
|
||||
failure_behavior: str = "fail_workflow"
|
||||
required_permissions: tuple[str, ...] = ()
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
@@ -101,6 +133,7 @@ class WorkflowStepDefinition:
|
||||
"parameters": dict(self.parameters),
|
||||
"outputs": dict(self.outputs),
|
||||
"preconditions": list(self.preconditions),
|
||||
"review_gate": dict(self.review_gate),
|
||||
"failure_behavior": self.failure_behavior,
|
||||
"required_permissions": list(self.required_permissions),
|
||||
"metadata": dict(self.metadata),
|
||||
@@ -118,6 +151,7 @@ class WorkflowStepDefinition:
|
||||
parameters=dict(data.get("parameters", {})),
|
||||
outputs=dict(data.get("outputs", {})),
|
||||
preconditions=tuple(dict(item) for item in data.get("preconditions", ())),
|
||||
review_gate=dict(data.get("review_gate", {})),
|
||||
failure_behavior=data.get("failure_behavior", "fail_workflow"),
|
||||
required_permissions=tuple(data.get("required_permissions", ())),
|
||||
metadata=dict(data.get("metadata", {})),
|
||||
@@ -300,6 +334,198 @@ class WorkflowStepRun:
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WorkflowReviewTask:
|
||||
workflow_run_id: str
|
||||
step_id: str
|
||||
reason: str
|
||||
requested_by: str
|
||||
output_asset_ids: tuple[str, ...] = ()
|
||||
status: WorkflowReviewStatus = WorkflowReviewStatus.OPEN
|
||||
queue: str = "default"
|
||||
assigned_to: str | None = None
|
||||
diagnostics: tuple[dict[str, Any], ...] = ()
|
||||
exception_id: str | None = None
|
||||
decision: WorkflowReviewDecisionType | None = None
|
||||
decision_note: str = ""
|
||||
correction: dict[str, Any] = field(default_factory=dict)
|
||||
decided_by: str | None = None
|
||||
decided_at: str | None = None
|
||||
review_id: str = field(default_factory=lambda: new_id("review"))
|
||||
requested_at: str = field(default_factory=lambda: utc_now().isoformat())
|
||||
updated_at: str = field(default_factory=lambda: utc_now().isoformat())
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
object.__setattr__(self, "output_asset_ids", tuple(self.output_asset_ids))
|
||||
object.__setattr__(self, "diagnostics", tuple(self.diagnostics))
|
||||
object.__setattr__(self, "status", WorkflowReviewStatus(self.status))
|
||||
if self.decision is not None:
|
||||
object.__setattr__(self, "decision", WorkflowReviewDecisionType(self.decision))
|
||||
|
||||
def decide(
|
||||
self,
|
||||
decision: WorkflowReviewDecisionType | str,
|
||||
*,
|
||||
actor_id: str,
|
||||
note: str = "",
|
||||
correction: dict[str, Any] | None = None,
|
||||
) -> "WorkflowReviewTask":
|
||||
decision = WorkflowReviewDecisionType(decision)
|
||||
statuses = {
|
||||
WorkflowReviewDecisionType.CONTINUE: WorkflowReviewStatus.CONTINUED,
|
||||
WorkflowReviewDecisionType.REJECT: WorkflowReviewStatus.REJECTED,
|
||||
WorkflowReviewDecisionType.CORRECT: WorkflowReviewStatus.CORRECTED,
|
||||
WorkflowReviewDecisionType.RETRY: WorkflowReviewStatus.RETRY_REQUESTED,
|
||||
WorkflowReviewDecisionType.ESCALATE: WorkflowReviewStatus.ESCALATED,
|
||||
}
|
||||
now = utc_now().isoformat()
|
||||
return replace(
|
||||
self,
|
||||
status=statuses[decision],
|
||||
decision=decision,
|
||||
decision_note=note,
|
||||
correction=dict(correction or {}),
|
||||
decided_by=actor_id,
|
||||
decided_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return compact_dict(
|
||||
{
|
||||
"review_id": self.review_id,
|
||||
"workflow_run_id": self.workflow_run_id,
|
||||
"step_id": self.step_id,
|
||||
"reason": self.reason,
|
||||
"requested_by": self.requested_by,
|
||||
"output_asset_ids": list(self.output_asset_ids),
|
||||
"status": self.status.value,
|
||||
"queue": self.queue,
|
||||
"assigned_to": self.assigned_to,
|
||||
"diagnostics": list(self.diagnostics),
|
||||
"exception_id": self.exception_id,
|
||||
"decision": self.decision.value if self.decision else None,
|
||||
"decision_note": self.decision_note,
|
||||
"correction": dict(self.correction),
|
||||
"decided_by": self.decided_by,
|
||||
"decided_at": self.decided_at,
|
||||
"requested_at": self.requested_at,
|
||||
"updated_at": self.updated_at,
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "WorkflowReviewTask":
|
||||
return cls(
|
||||
review_id=data["review_id"],
|
||||
workflow_run_id=data["workflow_run_id"],
|
||||
step_id=data["step_id"],
|
||||
reason=data["reason"],
|
||||
requested_by=data["requested_by"],
|
||||
output_asset_ids=tuple(data.get("output_asset_ids", ())),
|
||||
status=WorkflowReviewStatus(data.get("status", WorkflowReviewStatus.OPEN.value)),
|
||||
queue=data.get("queue", "default"),
|
||||
assigned_to=data.get("assigned_to"),
|
||||
diagnostics=tuple(data.get("diagnostics", ())),
|
||||
exception_id=data.get("exception_id"),
|
||||
decision=WorkflowReviewDecisionType(data["decision"]) if data.get("decision") else None,
|
||||
decision_note=data.get("decision_note", ""),
|
||||
correction=dict(data.get("correction", {})),
|
||||
decided_by=data.get("decided_by"),
|
||||
decided_at=data.get("decided_at"),
|
||||
requested_at=data["requested_at"],
|
||||
updated_at=data["updated_at"],
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WorkflowExceptionRecord:
|
||||
workflow_run_id: str
|
||||
kind: WorkflowExceptionKind | str
|
||||
message: str
|
||||
step_id: str | None = None
|
||||
status: WorkflowExceptionStatus = WorkflowExceptionStatus.OPEN
|
||||
diagnostics: tuple[dict[str, Any], ...] = ()
|
||||
output_asset_ids: tuple[str, ...] = ()
|
||||
review_id: str | None = None
|
||||
assigned_to: str | None = None
|
||||
resolved_by: str | None = None
|
||||
resolution: str = ""
|
||||
exception_id: str = field(default_factory=lambda: new_id("wfx"))
|
||||
created_at: str = field(default_factory=lambda: utc_now().isoformat())
|
||||
updated_at: str = field(default_factory=lambda: utc_now().isoformat())
|
||||
resolved_at: str | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
object.__setattr__(self, "kind", WorkflowExceptionKind(self.kind))
|
||||
object.__setattr__(self, "status", WorkflowExceptionStatus(self.status))
|
||||
object.__setattr__(self, "diagnostics", tuple(self.diagnostics))
|
||||
object.__setattr__(self, "output_asset_ids", tuple(self.output_asset_ids))
|
||||
|
||||
def resolve(self, *, actor_id: str, resolution: str) -> "WorkflowExceptionRecord":
|
||||
now = utc_now().isoformat()
|
||||
return replace(
|
||||
self,
|
||||
status=WorkflowExceptionStatus.RESOLVED,
|
||||
resolved_by=actor_id,
|
||||
resolution=resolution,
|
||||
resolved_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
|
||||
def escalate(self, *, actor_id: str, resolution: str) -> "WorkflowExceptionRecord":
|
||||
now = utc_now().isoformat()
|
||||
return replace(
|
||||
self,
|
||||
status=WorkflowExceptionStatus.ESCALATED,
|
||||
resolved_by=actor_id,
|
||||
resolution=resolution,
|
||||
resolved_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return compact_dict(
|
||||
{
|
||||
"exception_id": self.exception_id,
|
||||
"workflow_run_id": self.workflow_run_id,
|
||||
"kind": self.kind.value,
|
||||
"message": self.message,
|
||||
"step_id": self.step_id,
|
||||
"status": self.status.value,
|
||||
"diagnostics": list(self.diagnostics),
|
||||
"output_asset_ids": list(self.output_asset_ids),
|
||||
"review_id": self.review_id,
|
||||
"assigned_to": self.assigned_to,
|
||||
"resolved_by": self.resolved_by,
|
||||
"resolution": self.resolution,
|
||||
"created_at": self.created_at,
|
||||
"updated_at": self.updated_at,
|
||||
"resolved_at": self.resolved_at,
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "WorkflowExceptionRecord":
|
||||
return cls(
|
||||
exception_id=data["exception_id"],
|
||||
workflow_run_id=data["workflow_run_id"],
|
||||
kind=WorkflowExceptionKind(data["kind"]),
|
||||
message=data["message"],
|
||||
step_id=data.get("step_id"),
|
||||
status=WorkflowExceptionStatus(data.get("status", WorkflowExceptionStatus.OPEN.value)),
|
||||
diagnostics=tuple(data.get("diagnostics", ())),
|
||||
output_asset_ids=tuple(data.get("output_asset_ids", ())),
|
||||
review_id=data.get("review_id"),
|
||||
assigned_to=data.get("assigned_to"),
|
||||
resolved_by=data.get("resolved_by"),
|
||||
resolution=data.get("resolution", ""),
|
||||
created_at=data["created_at"],
|
||||
updated_at=data["updated_at"],
|
||||
resolved_at=data.get("resolved_at"),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WorkflowRun:
|
||||
template_id: str
|
||||
@@ -310,6 +536,8 @@ class WorkflowRun:
|
||||
policy_context: dict[str, Any] = field(default_factory=dict)
|
||||
status: WorkflowRunStatus = WorkflowRunStatus.QUEUED
|
||||
step_runs: tuple[WorkflowStepRun, ...] = ()
|
||||
review_tasks: tuple[WorkflowReviewTask, ...] = ()
|
||||
exceptions: tuple[WorkflowExceptionRecord, ...] = ()
|
||||
output_asset_ids: tuple[str, ...] = ()
|
||||
diagnostics: tuple[dict[str, Any], ...] = ()
|
||||
retry_of_run_id: str | None = None
|
||||
@@ -323,6 +551,8 @@ class WorkflowRun:
|
||||
def __post_init__(self) -> None:
|
||||
object.__setattr__(self, "status", WorkflowRunStatus(self.status))
|
||||
object.__setattr__(self, "step_runs", tuple(self.step_runs))
|
||||
object.__setattr__(self, "review_tasks", tuple(self.review_tasks))
|
||||
object.__setattr__(self, "exceptions", tuple(self.exceptions))
|
||||
object.__setattr__(self, "output_asset_ids", tuple(self.output_asset_ids))
|
||||
object.__setattr__(self, "diagnostics", tuple(self.diagnostics))
|
||||
|
||||
@@ -416,6 +646,30 @@ class WorkflowRun:
|
||||
ordered = ordered + (step_run,)
|
||||
return replace(self, step_runs=ordered, updated_at=utc_now().isoformat())
|
||||
|
||||
def with_review_task(self, review_task: WorkflowReviewTask) -> "WorkflowRun":
|
||||
existing = {item.review_id: item for item in self.review_tasks}
|
||||
existing[review_task.review_id] = review_task
|
||||
ordered = tuple(existing[item.review_id] for item in self.review_tasks if item.review_id in existing)
|
||||
if review_task.review_id not in {item.review_id for item in self.review_tasks}:
|
||||
ordered = ordered + (review_task,)
|
||||
return replace(self, review_tasks=ordered, updated_at=utc_now().isoformat())
|
||||
|
||||
def with_exception(self, exception: WorkflowExceptionRecord) -> "WorkflowRun":
|
||||
existing = {item.exception_id: item for item in self.exceptions}
|
||||
existing[exception.exception_id] = exception
|
||||
ordered = tuple(existing[item.exception_id] for item in self.exceptions if item.exception_id in existing)
|
||||
if exception.exception_id not in {item.exception_id for item in self.exceptions}:
|
||||
ordered = ordered + (exception,)
|
||||
return replace(self, exceptions=ordered, updated_at=utc_now().isoformat())
|
||||
|
||||
@property
|
||||
def open_review_tasks(self) -> tuple[WorkflowReviewTask, ...]:
|
||||
return tuple(item for item in self.review_tasks if item.status == WorkflowReviewStatus.OPEN)
|
||||
|
||||
@property
|
||||
def open_exceptions(self) -> tuple[WorkflowExceptionRecord, ...]:
|
||||
return tuple(item for item in self.exceptions if item.status == WorkflowExceptionStatus.OPEN)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return compact_dict(
|
||||
{
|
||||
@@ -428,6 +682,8 @@ class WorkflowRun:
|
||||
"policy_context": dict(self.policy_context),
|
||||
"status": self.status.value,
|
||||
"step_runs": [item.to_dict() for item in self.step_runs],
|
||||
"review_tasks": [item.to_dict() for item in self.review_tasks],
|
||||
"exceptions": [item.to_dict() for item in self.exceptions],
|
||||
"output_asset_ids": list(self.output_asset_ids),
|
||||
"diagnostics": list(self.diagnostics),
|
||||
"retry_of_run_id": self.retry_of_run_id,
|
||||
@@ -451,6 +707,8 @@ class WorkflowRun:
|
||||
policy_context=dict(data.get("policy_context", {})),
|
||||
status=WorkflowRunStatus(data.get("status", WorkflowRunStatus.QUEUED.value)),
|
||||
step_runs=tuple(WorkflowStepRun.from_dict(item) for item in data.get("step_runs", ())),
|
||||
review_tasks=tuple(WorkflowReviewTask.from_dict(item) for item in data.get("review_tasks", ())),
|
||||
exceptions=tuple(WorkflowExceptionRecord.from_dict(item) for item in data.get("exceptions", ())),
|
||||
output_asset_ids=tuple(data.get("output_asset_ids", ())),
|
||||
diagnostics=tuple(data.get("diagnostics", ())),
|
||||
retry_of_run_id=data.get("retry_of_run_id"),
|
||||
|
||||
Reference in New Issue
Block a user