test orgs 3
This commit is contained in:
@@ -0,0 +1,84 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from wf_authoring import build_async_registry, node
|
||||||
|
from wf_core import RuntimeContext, RunStatus, execute_workflow_async
|
||||||
|
|
||||||
|
from .test_demo_workflow import (
|
||||||
|
build_authoring_demo_workflow,
|
||||||
|
combine_summaries_spec,
|
||||||
|
drive_list_files_spec,
|
||||||
|
mark_email_skipped_spec,
|
||||||
|
send_email_spec,
|
||||||
|
summarize_document_spec,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class InferredEchoInput(BaseModel):
|
||||||
|
value: str
|
||||||
|
|
||||||
|
|
||||||
|
class InferredEchoOutput(BaseModel):
|
||||||
|
echoed: str
|
||||||
|
|
||||||
|
|
||||||
|
class InferredAsyncInput(BaseModel):
|
||||||
|
value: str
|
||||||
|
|
||||||
|
|
||||||
|
class InferredAsyncOutput(BaseModel):
|
||||||
|
echoed: str
|
||||||
|
|
||||||
|
|
||||||
|
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"}}
|
||||||
|
|
||||||
|
|
||||||
|
def test_execute_workflow_async_runs_with_async_registry() -> None:
|
||||||
|
workflow, _ = build_authoring_demo_workflow()
|
||||||
|
registry = build_async_registry(
|
||||||
|
drive_list_files_spec,
|
||||||
|
summarize_document_spec,
|
||||||
|
combine_summaries_spec,
|
||||||
|
send_email_spec,
|
||||||
|
mark_email_skipped_spec,
|
||||||
|
)
|
||||||
|
|
||||||
|
run = asyncio.run(
|
||||||
|
execute_workflow_async(
|
||||||
|
workflow,
|
||||||
|
{"folder_id": "demo-folder", "should_email": False},
|
||||||
|
registry,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert run.status == RunStatus.COMPLETED
|
||||||
|
assert run.output["email_status"] == "skipped"
|
||||||
|
assert run.interrupt is None
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from wf_authoring import exists, expr, state_path
|
||||||
|
|
||||||
|
|
||||||
|
def test_condition_dsl_compiles_to_core_condition() -> None:
|
||||||
|
condition = expr(state_path("should_email")).eq(True) & exists(
|
||||||
|
state_path("summary")
|
||||||
|
)
|
||||||
|
|
||||||
|
assert condition.to_condition().model_dump() == {
|
||||||
|
"op": "and",
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"op": "eq",
|
||||||
|
"left": {"path": "state.should_email"},
|
||||||
|
"right": {"value": True},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "exists",
|
||||||
|
"path": "state.summary",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
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 (
|
||||||
@@ -9,7 +7,6 @@ from wf_core import (
|
|||||||
FrameStatus,
|
FrameStatus,
|
||||||
RuntimeContext,
|
RuntimeContext,
|
||||||
RunStatus,
|
RunStatus,
|
||||||
execute_workflow_async,
|
|
||||||
execute_workflow,
|
execute_workflow,
|
||||||
resume_workflow,
|
resume_workflow,
|
||||||
step_workflow,
|
step_workflow,
|
||||||
@@ -20,17 +17,13 @@ from wf_authoring import (
|
|||||||
NodeReturn,
|
NodeReturn,
|
||||||
WorkflowBuilder,
|
WorkflowBuilder,
|
||||||
bind_fields,
|
bind_fields,
|
||||||
build_async_registry,
|
|
||||||
bind_state,
|
bind_state,
|
||||||
build_registry,
|
build_registry,
|
||||||
expr,
|
|
||||||
exists,
|
|
||||||
state,
|
state,
|
||||||
state_path,
|
state_path,
|
||||||
context_path,
|
context_path,
|
||||||
input_path,
|
input_path,
|
||||||
node,
|
node,
|
||||||
subgraph_node,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -74,30 +67,6 @@ class MarkEmailSkippedOutput(BaseModel):
|
|||||||
email_status: str
|
email_status: str
|
||||||
|
|
||||||
|
|
||||||
class InferredEchoInput(BaseModel):
|
|
||||||
value: str
|
|
||||||
|
|
||||||
|
|
||||||
class InferredEchoOutput(BaseModel):
|
|
||||||
echoed: str
|
|
||||||
|
|
||||||
|
|
||||||
class InferredOutcomeInput(BaseModel):
|
|
||||||
value: str
|
|
||||||
|
|
||||||
|
|
||||||
class InferredOutcomeOutput(BaseModel):
|
|
||||||
echoed: str
|
|
||||||
|
|
||||||
|
|
||||||
class InferredAsyncInput(BaseModel):
|
|
||||||
value: str
|
|
||||||
|
|
||||||
|
|
||||||
class InferredAsyncOutput(BaseModel):
|
|
||||||
echoed: str
|
|
||||||
|
|
||||||
|
|
||||||
@node(
|
@node(
|
||||||
name="drive_list_files",
|
name="drive_list_files",
|
||||||
input_model=DriveListFilesInput,
|
input_model=DriveListFilesInput,
|
||||||
@@ -437,161 +406,3 @@ def test_async_node_spec_cannot_export_sync_registry_handler() -> None:
|
|||||||
else:
|
else:
|
||||||
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"}}
|
|
||||||
|
|
||||||
|
|
||||||
def test_execute_workflow_async_runs_with_async_registry() -> None:
|
|
||||||
workflow, _ = build_authoring_demo_workflow()
|
|
||||||
registry = build_async_registry(
|
|
||||||
drive_list_files_spec,
|
|
||||||
summarize_document_spec,
|
|
||||||
combine_summaries_spec,
|
|
||||||
send_email_spec,
|
|
||||||
mark_email_skipped_spec,
|
|
||||||
)
|
|
||||||
|
|
||||||
run = asyncio.run(
|
|
||||||
execute_workflow_async(
|
|
||||||
workflow,
|
|
||||||
{"folder_id": "demo-folder", "should_email": False},
|
|
||||||
registry,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
assert run.status == RunStatus.COMPLETED
|
|
||||||
assert run.output["email_status"] == "skipped"
|
|
||||||
assert run.interrupt is None
|
|
||||||
|
|
||||||
|
|
||||||
@node()
|
|
||||||
def inferred_echo(
|
|
||||||
payload: InferredEchoInput,
|
|
||||||
ctx: RuntimeContext,
|
|
||||||
) -> InferredEchoOutput:
|
|
||||||
return InferredEchoOutput(echoed=payload.value)
|
|
||||||
|
|
||||||
|
|
||||||
@node(outcomes=("done", "retry"))
|
|
||||||
def inferred_echo_with_outcome(
|
|
||||||
payload: InferredOutcomeInput,
|
|
||||||
ctx: RuntimeContext,
|
|
||||||
) -> NodeReturn[InferredOutcomeOutput]:
|
|
||||||
return NodeReturn(
|
|
||||||
outcome="done",
|
|
||||||
output=InferredOutcomeOutput(echoed=payload.value),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@node()
|
|
||||||
async def inferred_async_echo(
|
|
||||||
payload: InferredAsyncInput,
|
|
||||||
ctx: RuntimeContext,
|
|
||||||
) -> InferredAsyncOutput:
|
|
||||||
return InferredAsyncOutput(echoed=payload.value)
|
|
||||||
|
|
||||||
|
|
||||||
def test_node_decorator_infers_models_from_annotations() -> None:
|
|
||||||
assert inferred_echo.input_model is InferredEchoInput
|
|
||||||
assert inferred_echo.output_model is InferredEchoOutput
|
|
||||||
assert inferred_echo.outcomes == ("ok",)
|
|
||||||
assert inferred_echo.is_async is False
|
|
||||||
|
|
||||||
registry = build_registry(inferred_echo)
|
|
||||||
result = registry["inferred_echo"](
|
|
||||||
{"value": "hello"},
|
|
||||||
RuntimeContext(current_node_id="x"),
|
|
||||||
)
|
|
||||||
assert result == {"outcome": "ok", "output": {"echoed": "hello"}}
|
|
||||||
|
|
||||||
|
|
||||||
def test_node_decorator_infers_nodereturn_output_model() -> None:
|
|
||||||
assert inferred_echo_with_outcome.input_model is InferredOutcomeInput
|
|
||||||
assert inferred_echo_with_outcome.output_model is InferredOutcomeOutput
|
|
||||||
|
|
||||||
registry = build_registry(inferred_echo_with_outcome)
|
|
||||||
result = registry["inferred_echo_with_outcome"](
|
|
||||||
{"value": "hello"},
|
|
||||||
RuntimeContext(current_node_id="x"),
|
|
||||||
)
|
|
||||||
assert result == {"outcome": "done", "output": {"echoed": "hello"}}
|
|
||||||
|
|
||||||
|
|
||||||
def test_node_decorator_detects_async_automatically() -> None:
|
|
||||||
assert inferred_async_echo.is_async is True
|
|
||||||
|
|
||||||
|
|
||||||
def test_condition_dsl_compiles_to_core_condition() -> None:
|
|
||||||
condition = expr(state_path("should_email")).eq(True) & exists(
|
|
||||||
state_path("summary")
|
|
||||||
)
|
|
||||||
|
|
||||||
assert condition.to_condition().model_dump() == {
|
|
||||||
"op": "and",
|
|
||||||
"args": [
|
|
||||||
{
|
|
||||||
"op": "eq",
|
|
||||||
"left": {"path": "state.should_email"},
|
|
||||||
"right": {"value": True},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"op": "exists",
|
|
||||||
"path": "state.summary",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_subgraph_node_wraps_compiled_workflow() -> None:
|
|
||||||
class ChildInput(BaseModel):
|
|
||||||
folder_id: str
|
|
||||||
should_email: bool
|
|
||||||
|
|
||||||
class ChildOutput(BaseModel):
|
|
||||||
summary: str
|
|
||||||
email_status: str
|
|
||||||
|
|
||||||
child_workflow = build_demo_workflow()
|
|
||||||
child_registry = build_demo_registry()
|
|
||||||
wrapped = subgraph_node(
|
|
||||||
name="wrapped_demo",
|
|
||||||
workflow=child_workflow,
|
|
||||||
registry=child_registry,
|
|
||||||
input_model=ChildInput,
|
|
||||||
output_model=ChildOutput,
|
|
||||||
)
|
|
||||||
|
|
||||||
registry = build_registry(wrapped)
|
|
||||||
result = registry["wrapped_demo"](
|
|
||||||
{"folder_id": "demo-folder", "should_email": False},
|
|
||||||
RuntimeContext(current_node_id="parent"),
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result["outcome"] == "ok"
|
|
||||||
assert result["output"]["email_status"] == "skipped"
|
|
||||||
assert "summary" in result["output"]
|
|
||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from wf_authoring import Nothing, build_registry, node, outcome
|
from wf_authoring import NodeReturn, Nothing, build_registry, node, outcome
|
||||||
from wf_core import RuntimeContext
|
from wf_core import RuntimeContext
|
||||||
|
|
||||||
|
|
||||||
@@ -14,6 +14,57 @@ class AliasOutput(BaseModel):
|
|||||||
value: str
|
value: str
|
||||||
|
|
||||||
|
|
||||||
|
class InferredEchoInput(BaseModel):
|
||||||
|
value: str
|
||||||
|
|
||||||
|
|
||||||
|
class InferredEchoOutput(BaseModel):
|
||||||
|
echoed: str
|
||||||
|
|
||||||
|
|
||||||
|
class InferredOutcomeInput(BaseModel):
|
||||||
|
value: str
|
||||||
|
|
||||||
|
|
||||||
|
class InferredOutcomeOutput(BaseModel):
|
||||||
|
echoed: str
|
||||||
|
|
||||||
|
|
||||||
|
class InferredAsyncInput(BaseModel):
|
||||||
|
value: str
|
||||||
|
|
||||||
|
|
||||||
|
class InferredAsyncOutput(BaseModel):
|
||||||
|
echoed: str
|
||||||
|
|
||||||
|
|
||||||
|
@node()
|
||||||
|
def inferred_echo(
|
||||||
|
payload: InferredEchoInput,
|
||||||
|
ctx: RuntimeContext,
|
||||||
|
) -> InferredEchoOutput:
|
||||||
|
return InferredEchoOutput(echoed=payload.value)
|
||||||
|
|
||||||
|
|
||||||
|
@node(outcomes=("done", "retry"))
|
||||||
|
def inferred_echo_with_outcome(
|
||||||
|
payload: InferredOutcomeInput,
|
||||||
|
ctx: RuntimeContext,
|
||||||
|
) -> NodeReturn[InferredOutcomeOutput]:
|
||||||
|
return NodeReturn(
|
||||||
|
outcome="done",
|
||||||
|
output=InferredOutcomeOutput(echoed=payload.value),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@node()
|
||||||
|
async def inferred_async_echo(
|
||||||
|
payload: InferredAsyncInput,
|
||||||
|
ctx: RuntimeContext,
|
||||||
|
) -> InferredAsyncOutput:
|
||||||
|
return InferredAsyncOutput(echoed=payload.value)
|
||||||
|
|
||||||
|
|
||||||
def test_node_decorator_can_alias_existing_node_spec() -> None:
|
def test_node_decorator_can_alias_existing_node_spec() -> None:
|
||||||
@node(name="test.alias", description="Alias spec.")
|
@node(name="test.alias", description="Alias spec.")
|
||||||
@node(name="test.original", description="Original spec.")
|
@node(name="test.original", description="Original spec.")
|
||||||
@@ -76,3 +127,33 @@ def test_outcome_can_wrap_explicit_output() -> None:
|
|||||||
|
|
||||||
assert result.outcome == "ok"
|
assert result.outcome == "ok"
|
||||||
assert result.output is output
|
assert result.output is output
|
||||||
|
|
||||||
|
|
||||||
|
def test_node_decorator_infers_models_from_annotations() -> None:
|
||||||
|
assert inferred_echo.input_model is InferredEchoInput
|
||||||
|
assert inferred_echo.output_model is InferredEchoOutput
|
||||||
|
assert inferred_echo.outcomes == ("ok",)
|
||||||
|
assert inferred_echo.is_async is False
|
||||||
|
|
||||||
|
registry = build_registry(inferred_echo)
|
||||||
|
result = registry["inferred_echo"](
|
||||||
|
{"value": "hello"},
|
||||||
|
RuntimeContext(current_node_id="x"),
|
||||||
|
)
|
||||||
|
assert result == {"outcome": "ok", "output": {"echoed": "hello"}}
|
||||||
|
|
||||||
|
|
||||||
|
def test_node_decorator_infers_nodereturn_output_model() -> None:
|
||||||
|
assert inferred_echo_with_outcome.input_model is InferredOutcomeInput
|
||||||
|
assert inferred_echo_with_outcome.output_model is InferredOutcomeOutput
|
||||||
|
|
||||||
|
registry = build_registry(inferred_echo_with_outcome)
|
||||||
|
result = registry["inferred_echo_with_outcome"](
|
||||||
|
{"value": "hello"},
|
||||||
|
RuntimeContext(current_node_id="x"),
|
||||||
|
)
|
||||||
|
assert result == {"outcome": "done", "output": {"echoed": "hello"}}
|
||||||
|
|
||||||
|
|
||||||
|
def test_node_decorator_detects_async_automatically() -> None:
|
||||||
|
assert inferred_async_echo.is_async is True
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from wf_authoring import build_registry, subgraph_node
|
||||||
|
from wf_core import RuntimeContext
|
||||||
|
from wf_core.demo_workflow import build_demo_registry, build_demo_workflow
|
||||||
|
|
||||||
|
|
||||||
|
def test_subgraph_node_wraps_compiled_workflow() -> None:
|
||||||
|
class ChildInput(BaseModel):
|
||||||
|
folder_id: str
|
||||||
|
should_email: bool
|
||||||
|
|
||||||
|
class ChildOutput(BaseModel):
|
||||||
|
summary: str
|
||||||
|
email_status: str
|
||||||
|
|
||||||
|
child_workflow = build_demo_workflow()
|
||||||
|
child_registry = build_demo_registry()
|
||||||
|
wrapped = subgraph_node(
|
||||||
|
name="wrapped_demo",
|
||||||
|
workflow=child_workflow,
|
||||||
|
registry=child_registry,
|
||||||
|
input_model=ChildInput,
|
||||||
|
output_model=ChildOutput,
|
||||||
|
)
|
||||||
|
|
||||||
|
registry = build_registry(wrapped)
|
||||||
|
result = registry["wrapped_demo"](
|
||||||
|
{"folder_id": "demo-folder", "should_email": False},
|
||||||
|
RuntimeContext(current_node_id="parent"),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["outcome"] == "ok"
|
||||||
|
assert result["output"]["email_status"] == "skipped"
|
||||||
|
assert "summary" in result["output"]
|
||||||
Reference in New Issue
Block a user