i can now Use our system programmatically
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
from .builder import WorkflowBuilder
|
||||
from .catalog import NodeCatalog, NodeCatalogEntry
|
||||
from .spec import NodeSpec, node
|
||||
|
||||
__all__ = [
|
||||
"NodeCatalog",
|
||||
"NodeCatalogEntry",
|
||||
"NodeSpec",
|
||||
"WorkflowBuilder",
|
||||
"node",
|
||||
]
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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()
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user