async ver, starting high level

This commit is contained in:
lda
2026-04-29 16:21:22 +07:00 Verified
parent b62d5eb29f
commit 565a790b4a
3 changed files with 105 additions and 17 deletions
+31
View File
@@ -1,5 +1,7 @@
from __future__ import annotations from __future__ import annotations
import asyncio
from pydantic import BaseModel from pydantic import BaseModel
from wf_core import ( from wf_core import (
@@ -17,6 +19,7 @@ from wf_authoring import (
NodeReturn, NodeReturn,
WorkflowBuilder, WorkflowBuilder,
bind_fields, bind_fields,
build_async_registry,
bind_state, bind_state,
build_registry, build_registry,
expr, expr,
@@ -431,6 +434,34 @@ def test_async_node_spec_cannot_export_sync_registry_handler() -> None:
raise AssertionError("expected async node export to fail for sync registry") raise AssertionError("expected async node export to fail for sync registry")
def test_async_registry_accepts_sync_and_async_specs() -> None:
@node()
def sync_echo(
payload: InferredEchoInput,
ctx: RuntimeContext,
) -> InferredEchoOutput:
return InferredEchoOutput(echoed=payload.value)
@node()
async def async_echo(
payload: InferredAsyncInput,
ctx: RuntimeContext,
) -> InferredAsyncOutput:
return InferredAsyncOutput(echoed=f"async:{payload.value}")
registry = build_async_registry(sync_echo, async_echo)
ctx = RuntimeContext(current_node_id="demo")
async def run_handler(name: str, value: str) -> dict[str, object]:
return await registry[name]({"value": value}, ctx)
sync_result = asyncio.run(run_handler("sync_echo", "hello"))
async_result = asyncio.run(run_handler("async_echo", "world"))
assert sync_result == {"outcome": "ok", "output": {"echoed": "hello"}}
assert async_result == {"outcome": "ok", "output": {"echoed": "async:world"}}
@node() @node()
def inferred_echo( def inferred_echo(
payload: InferredEchoInput, payload: InferredEchoInput,
+12 -1
View File
@@ -3,7 +3,15 @@ from .catalog import NodeCatalog, NodeCatalogEntry
from .conditions import context, exists, expr, input, state from .conditions import context, exists, expr, input, state
from .mapping import bind_fields, bind_state, merge_maps from .mapping import bind_fields, bind_state, merge_maps
from .paths import GraphPath, context_path, graph_path, input_path, state_path from .paths import GraphPath, context_path, graph_path, input_path, state_path
from .spec import NodeReturn, NodeSpec, build_registry, node from .spec import (
AsyncRegistryHandler,
NodeReturn,
NodeSpec,
SyncRegistryHandler,
build_async_registry,
build_registry,
node,
)
from .subgraph import subgraph_node from .subgraph import subgraph_node
__all__ = [ __all__ = [
@@ -12,8 +20,11 @@ __all__ = [
"GraphPath", "GraphPath",
"NodeReturn", "NodeReturn",
"NodeSpec", "NodeSpec",
"AsyncRegistryHandler",
"SyncRegistryHandler",
"WorkflowBuilder", "WorkflowBuilder",
"bind_fields", "bind_fields",
"build_async_registry",
"build_registry", "build_registry",
"bind_state", "bind_state",
"merge_maps", "merge_maps",
+61 -15
View File
@@ -15,6 +15,10 @@ NodeCallable = Callable[[InputT, RuntimeContext], "NodeReturn[OutputT] | OutputT
AsyncNodeCallable = Callable[ AsyncNodeCallable = Callable[
[InputT, RuntimeContext], Awaitable["NodeReturn[OutputT] | OutputT"] [InputT, RuntimeContext], Awaitable["NodeReturn[OutputT] | OutputT"]
] ]
SyncRegistryHandler = Callable[[dict[str, Any], RuntimeContext], dict[str, Any]]
AsyncRegistryHandler = Callable[
[dict[str, Any], RuntimeContext], Awaitable[dict[str, Any]]
]
def _schema_ref_for(model_type: type[BaseModel]) -> SchemaRef: def _schema_ref_for(model_type: type[BaseModel]) -> SchemaRef:
@@ -27,6 +31,28 @@ class NodeReturn(Generic[OutputT]):
output: OutputT output: OutputT
def _coerce_registry_result(
*,
node_name: str,
output_model: type[BaseModel],
default_outcome: str,
raw: NodeReturn[BaseModel] | BaseModel,
) -> dict[str, Any]:
if isinstance(raw, NodeReturn):
if not isinstance(raw.output, output_model):
raise TypeError(
f"node {node_name!r} returned NodeReturn with unsupported output "
f"{type(raw.output)!r}"
)
return {
"outcome": raw.outcome,
"output": raw.output.model_dump(),
}
if isinstance(raw, output_model):
return {"outcome": default_outcome, "output": raw.model_dump()}
raise TypeError(f"node {node_name!r} returned unsupported value {type(raw)!r}")
def _is_basemodel_subclass(value: object) -> bool: def _is_basemodel_subclass(value: object) -> bool:
return isinstance(value, type) and issubclass(value, BaseModel) return isinstance(value, type) and issubclass(value, BaseModel)
@@ -113,7 +139,7 @@ class NodeSpec(Generic[InputT, OutputT]):
outcomes=list(self.outcomes), outcomes=list(self.outcomes),
) )
def to_registry_handler(self) -> Callable[[dict[str, Any], RuntimeContext], dict[str, Any]]: def to_registry_handler(self) -> SyncRegistryHandler:
if self.is_async: if self.is_async:
raise TypeError( raise TypeError(
f"node {self.name!r} is async and cannot be exported to the sync registry" f"node {self.name!r} is async and cannot be exported to the sync registry"
@@ -122,20 +148,34 @@ class NodeSpec(Generic[InputT, OutputT]):
def handler(payload: dict[str, Any], ctx: RuntimeContext) -> dict[str, Any]: def handler(payload: dict[str, Any], ctx: RuntimeContext) -> dict[str, Any]:
parsed = self.input_model.model_validate(payload) parsed = self.input_model.model_validate(payload)
raw = self.fn(parsed, ctx) raw = self.fn(parsed, ctx)
if isinstance(raw, NodeReturn): return _coerce_registry_result(
if not isinstance(raw.output, self.output_model): node_name=self.name,
raise TypeError( output_model=self.output_model,
f"node {self.name!r} returned NodeReturn with unsupported output " default_outcome=self.outcomes[0],
f"{type(raw.output)!r}" raw=cast(NodeReturn[BaseModel] | BaseModel, raw),
) )
return {
"outcome": raw.outcome, return handler
"output": raw.output.model_dump(),
} def to_async_registry_handler(self) -> AsyncRegistryHandler:
if isinstance(raw, self.output_model): async def handler(
return {"outcome": self.outcomes[0], "output": raw.model_dump()} payload: dict[str, Any],
raise TypeError( ctx: RuntimeContext,
f"node {self.name!r} returned unsupported value {type(raw)!r}" ) -> dict[str, Any]:
parsed = self.input_model.model_validate(payload)
raw_result = self.fn(parsed, ctx)
if self.is_async:
raw = await cast(
Awaitable[NodeReturn[OutputT] | OutputT],
raw_result,
)
else:
raw = cast(NodeReturn[OutputT] | OutputT, raw_result)
return _coerce_registry_result(
node_name=self.name,
output_model=self.output_model,
default_outcome=self.outcomes[0],
raw=cast(NodeReturn[BaseModel] | BaseModel, raw),
) )
return handler return handler
@@ -217,5 +257,11 @@ def node(
def build_registry( def build_registry(
*specs: NodeSpec[Any, Any], *specs: NodeSpec[Any, Any],
) -> dict[str, Callable[[dict[str, Any], RuntimeContext], dict[str, Any]]]: ) -> dict[str, SyncRegistryHandler]:
return {spec.name: spec.to_registry_handler() for spec in specs} return {spec.name: spec.to_registry_handler() for spec in specs}
def build_async_registry(
*specs: NodeSpec[Any, Any],
) -> dict[str, AsyncRegistryHandler]:
return {spec.name: spec.to_async_registry_handler() for spec in specs}