i can now Use our system programmatically

This commit is contained in:
lda
2026-04-28 16:52:49 +07:00 Verified
parent dc38b87ddb
commit 2cc0ed60f2
15 changed files with 670 additions and 14 deletions
+11
View File
@@ -0,0 +1,11 @@
from .builder import WorkflowBuilder
from .catalog import NodeCatalog, NodeCatalogEntry
from .spec import NodeSpec, node
__all__ = [
"NodeCatalog",
"NodeCatalogEntry",
"NodeSpec",
"WorkflowBuilder",
"node",
]
+113
View File
@@ -0,0 +1,113 @@
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Literal
from wf_core import (
ConditionNode,
Edge,
ForeachNode,
InterruptNode,
NodeUse,
SchemaRef,
StateSchema,
Workflow,
)
from .spec import NodeSpec
@dataclass(slots=True)
class WorkflowBuilder:
name: str
input_schema: SchemaRef
state_schema: StateSchema
output_schema: SchemaRef
start: str
node_specs: dict[str, NodeSpec[Any, Any]] = field(default_factory=dict)
nodes: list[Any] = field(default_factory=list)
edges: list[Edge] = field(default_factory=list)
def use(
self,
spec: NodeSpec[Any, Any],
*,
id: str,
in_map: dict[str, str] | None = None,
out_map: dict[str, str] | None = None,
desc: str | None = None,
) -> NodeUse:
self.node_specs[spec.name] = spec
node = NodeUse(
id=id,
type="node",
node=spec.name,
desc=desc or spec.description,
in_map=in_map or {},
out_map=out_map or {},
)
self.nodes.append(node)
return node
def condition(self, *, id: str, check: Any) -> ConditionNode:
node = ConditionNode(id=id, type="condition", check=check)
self.nodes.append(node)
return node
def foreach(
self,
*,
id: str,
over: str,
as_: str,
mode: Literal["serial", "parallel"] = "serial",
on_item_error: Literal["fail", "collect", "skip"] = "fail",
) -> ForeachNode:
node = ForeachNode.model_validate(
{
"id": id,
"type": "foreach",
"over": over,
"as": as_,
"mode": mode,
"on_item_error": on_item_error,
}
)
self.nodes.append(node)
return node
def interrupt(
self,
*,
id: str,
kind: str,
request_map: dict[str, str] | None = None,
out_map: dict[str, str] | None = None,
outcomes: list[str] | None = None,
) -> InterruptNode:
node = InterruptNode(
id=id,
type="interrupt",
kind=kind,
request_map=request_map or {},
out_map=out_map or {},
outcomes=outcomes or ["submitted"],
)
self.nodes.append(node)
return node
def connect(self, from_: str, outcome: str, to: str) -> None:
self.edges.append(Edge.model_validate({"from": from_, "outcome": outcome, "to": to}))
def compile(self) -> Workflow:
node_defs = [spec.to_node_def() for spec in self.node_specs.values()]
return Workflow(
name=self.name,
input_schema=self.input_schema,
state_schema=self.state_schema,
output_schema=self.output_schema,
node_defs=node_defs,
start=self.start,
nodes=self.nodes,
edges=self.edges,
)
+51
View File
@@ -0,0 +1,51 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from .spec import NodeSpec
@dataclass(slots=True)
class NodeCatalogEntry:
name: str
description: str | None
outcomes: tuple[str, ...]
input_schema: dict[str, Any]
output_schema: dict[str, Any]
@classmethod
def from_spec(cls, spec: NodeSpec[Any, Any]) -> "NodeCatalogEntry":
return cls(
name=spec.name,
description=spec.description,
outcomes=spec.outcomes,
input_schema=spec.input_model.model_json_schema(),
output_schema=spec.output_model.model_json_schema(),
)
@dataclass(slots=True)
class NodeCatalog:
specs: dict[str, NodeSpec[Any, Any]]
@classmethod
def from_specs(cls, *specs: NodeSpec[Any, Any]) -> "NodeCatalog":
return cls(specs={spec.name: spec for spec in specs})
def entries(self) -> list[NodeCatalogEntry]:
return [NodeCatalogEntry.from_spec(spec) for spec in self.specs.values()]
def as_mcp_payload(self) -> dict[str, Any]:
return {
"nodes": [
{
"name": entry.name,
"description": entry.description,
"outcomes": list(entry.outcomes),
"input_schema": entry.input_schema,
"output_schema": entry.output_schema,
}
for entry in self.entries()
]
}
+84
View File
@@ -0,0 +1,84 @@
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any, Generic, TypeVar
from pydantic import BaseModel
from wf_core import NodeDef, RuntimeContext, SchemaRef
InputT = TypeVar("InputT", bound=BaseModel)
OutputT = TypeVar("OutputT", bound=BaseModel)
def _schema_ref_for(model_type: type[BaseModel]) -> SchemaRef:
return SchemaRef.model_validate(model_type.model_json_schema())
@dataclass(slots=True)
class NodeSpec(Generic[InputT, OutputT]):
name: str
input_model: type[InputT]
output_model: type[OutputT]
outcomes: tuple[str, ...]
fn: Callable[[InputT, RuntimeContext], OutputT | dict[str, Any]]
description: str | None = None
is_async: bool = False
def __call__(
self,
payload: InputT,
ctx: RuntimeContext,
) -> OutputT | dict[str, Any]:
return self.fn(payload, ctx)
def to_node_def(self) -> NodeDef:
return NodeDef(
name=self.name,
input_schema=_schema_ref_for(self.input_model),
output_schema=_schema_ref_for(self.output_model),
outcomes=list(self.outcomes),
)
def to_registry_handler(self) -> Callable[[dict[str, Any], RuntimeContext], dict[str, Any]]:
def handler(payload: dict[str, Any], ctx: RuntimeContext) -> dict[str, Any]:
parsed = self.input_model.model_validate(payload)
raw = self.fn(parsed, ctx)
if isinstance(raw, self.output_model):
return {"outcome": self.outcomes[0], "output": raw.model_dump()}
if isinstance(raw, dict):
return raw
raise TypeError(
f"node {self.name!r} returned unsupported value {type(raw)!r}"
)
return handler
def node(
*,
name: str | None = None,
input_model: type[InputT],
output_model: type[OutputT],
outcomes: tuple[str, ...] = ("ok",),
description: str | None = None,
) -> Callable[
[Callable[[InputT, RuntimeContext], OutputT | dict[str, Any]]],
NodeSpec[InputT, OutputT],
]:
def decorator(
fn: Callable[[InputT, RuntimeContext], OutputT | dict[str, Any]],
) -> NodeSpec[InputT, OutputT]:
resolved_name = name or getattr(fn, "__name__", "node")
return NodeSpec(
name=resolved_name,
input_model=input_model,
output_model=output_model,
outcomes=outcomes,
fn=fn,
description=description or fn.__doc__,
is_async=False,
)
return decorator