feat: complete production workflow migration
This commit is contained in:
1 parent
7f215e79c4
commit
8df5266abb
448 files changed
+56937
-14619
No files matched your search
@@ -0,0 +1,119 @@
|
||||
"""LangGraph compilation for deterministic workflow definitions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Annotated, Callable, Literal, TypedDict
|
||||
|
||||
from langgraph.graph import END, START, StateGraph
|
||||
from langgraph.graph.state import CompiledStateGraph
|
||||
|
||||
from gyxx_flow.core.context import RunContext
|
||||
|
||||
from .model import StepDefinition, WorkflowDefinition
|
||||
|
||||
GraphStepStatus = Literal["success", "failed", "skipped"]
|
||||
|
||||
|
||||
class GraphAttemptResult(TypedDict):
|
||||
"""JSON-serializable attempt result stored in graph state."""
|
||||
|
||||
attempt: int
|
||||
status: GraphStepStatus
|
||||
exit_code: int | None
|
||||
error: str | None
|
||||
reason: str | None
|
||||
|
||||
|
||||
class GraphStepResult(TypedDict):
|
||||
"""JSON-serializable step result stored in graph state."""
|
||||
|
||||
step_id: str
|
||||
status: GraphStepStatus
|
||||
attempts: list[GraphAttemptResult]
|
||||
final_exit_code: int | None
|
||||
critical: bool
|
||||
reason: str | None
|
||||
|
||||
|
||||
class WorkflowGraphState(TypedDict):
|
||||
"""Serializable state passed between one workflow's LangGraph nodes."""
|
||||
|
||||
workflow_id: str
|
||||
run_id: str
|
||||
business_date: str
|
||||
started_at: str
|
||||
shadow: bool
|
||||
dry_run: bool
|
||||
step_results: Annotated[
|
||||
dict[str, GraphStepResult],
|
||||
_merge_step_results,
|
||||
]
|
||||
|
||||
|
||||
GraphStepRunner = Callable[
|
||||
[StepDefinition, WorkflowGraphState],
|
||||
GraphStepResult,
|
||||
]
|
||||
|
||||
|
||||
def initial_workflow_graph_state(
|
||||
context: RunContext,
|
||||
*,
|
||||
dry_run: bool,
|
||||
) -> WorkflowGraphState:
|
||||
"""Create a JSON-serializable initial state from the public run context."""
|
||||
|
||||
return {
|
||||
"workflow_id": context.workflow_id,
|
||||
"run_id": context.run_id,
|
||||
"business_date": context.business_date.isoformat(),
|
||||
"started_at": context.started_at.isoformat(),
|
||||
"shadow": context.shadow,
|
||||
"dry_run": dry_run,
|
||||
"step_results": {},
|
||||
}
|
||||
|
||||
|
||||
def compile_workflow_graph(
|
||||
workflow: WorkflowDefinition,
|
||||
*,
|
||||
run_step: GraphStepRunner,
|
||||
) -> CompiledStateGraph:
|
||||
"""Compile dependencies into a real LangGraph DAG."""
|
||||
|
||||
builder = StateGraph(WorkflowGraphState)
|
||||
ordered_steps = workflow.ordered_steps()
|
||||
dependent_ids = {
|
||||
dependency
|
||||
for step in ordered_steps
|
||||
for dependency in step.depends_on
|
||||
}
|
||||
for step in ordered_steps:
|
||||
builder.add_node(step.step_id, _step_node(step, run_step))
|
||||
if step.depends_on:
|
||||
builder.add_edge(list(step.depends_on), step.step_id)
|
||||
else:
|
||||
builder.add_edge(START, step.step_id)
|
||||
if step.step_id not in dependent_ids:
|
||||
builder.add_edge(step.step_id, END)
|
||||
return builder.compile()
|
||||
|
||||
|
||||
def _step_node(
|
||||
step: StepDefinition,
|
||||
run_step: GraphStepRunner,
|
||||
) -> Callable[[WorkflowGraphState], dict[str, object]]:
|
||||
def execute(state: WorkflowGraphState) -> dict[str, object]:
|
||||
result = run_step(step, state)
|
||||
return {"step_results": {step.step_id: result}}
|
||||
|
||||
return execute
|
||||
|
||||
|
||||
def _merge_step_results(
|
||||
current: dict[str, GraphStepResult],
|
||||
update: dict[str, GraphStepResult],
|
||||
) -> dict[str, GraphStepResult]:
|
||||
"""Merge branch-local results emitted in the same LangGraph superstep."""
|
||||
|
||||
return {**current, **update}
|
||||
Reference in new issue
Block a user