misc changes: returns None, root as ., add

This commit is contained in:
lda
2026-05-18 00:45:08 +07:00 Verified
parent 6ee1d8f0cf
commit acd2e2ecb4
19 changed files with 274 additions and 53 deletions
+18
View File
@@ -59,6 +59,24 @@ def test_builder_preserves_explicit_nested_node_local_maps() -> None:
assert step.out_map == {"payload.text": "state.text"}
def test_builder_preserves_explicit_root_node_local_maps() -> None:
builder = WorkflowBuilder(
name="root_local_maps",
input_schema=AutoBindInput,
state_schema=AutoBindState,
output_schema=AutoBindOutput,
)
step = builder.use(
auto_bind_node,
in_map={"state.text": "."},
out_map={".": "state.text"},
)
assert step.in_map == {"state.text": "."}
assert step.out_map == {".": "state.text"}
def test_builder_can_auto_id_node_uses_from_spec_name() -> None:
builder = WorkflowBuilder(
name="auto_id_demo",
+60 -1
View File
@@ -2,7 +2,15 @@ from __future__ import annotations
from pydantic import BaseModel, Field
from wf_authoring import NodeCatalog, NodeReturn, Nothing, build_registry, node, outcome
from wf_authoring import (
NoOutput,
NodeCatalog,
NodeReturn,
Nothing,
build_registry,
node,
outcome,
)
from wf_core import RuntimeContext
@@ -46,6 +54,10 @@ class DocumentedOutput(BaseModel):
echoed: str = Field(description="Echoed message")
class NoOutputInput(BaseModel):
value: str
@node()
def inferred_echo(
payload: InferredEchoInput,
@@ -137,6 +149,53 @@ def test_outcome_can_wrap_explicit_output() -> None:
assert result.output is output
def test_node_return_annotation_none_uses_nothing_output() -> None:
@node
def no_output(_: NoOutputInput) -> None:
return None
registry = build_registry(no_output)
result = registry["no_output"](
{"value": "hello"},
RuntimeContext(current_node_id="x"),
)
assert no_output.output_model is Nothing
assert result == {"outcome": "ok", "output": {}}
def test_bare_nodereturn_annotation_defaults_to_nothing_output() -> None:
@node(outcomes=("skip",))
def no_output_with_outcome(
_: NoOutputInput,
) -> NodeReturn: # pyright: ignore[reportMissingTypeArgument]
return outcome("skip")
registry = build_registry(no_output_with_outcome)
result = registry["no_output_with_outcome"](
{"value": "hello"},
RuntimeContext(current_node_id="x"),
)
assert no_output_with_outcome.output_model is Nothing
assert result == {"outcome": "skip", "output": {}}
def test_no_output_alias_annotates_outcome_only_nodes() -> None:
@node(outcomes=("skip",))
def no_output_alias(_: NoOutputInput) -> NoOutput:
return outcome("skip")
registry = build_registry(no_output_alias)
result = registry["no_output_alias"](
{"value": "hello"},
RuntimeContext(current_node_id="x"),
)
assert no_output_alias.output_model is Nothing
assert result == {"outcome": "skip", "output": {}}
def test_node_decorator_infers_models_from_annotations() -> None:
assert inferred_echo.input_model is InferredEchoInput
assert inferred_echo.output_model is InferredEchoOutput
+1 -1
View File
@@ -122,7 +122,7 @@ def test_state_field_accepts_configured_reducer_reference() -> None:
),
] = 0
field = State.model_fields["total"].metadata[0]
field = State.model_fields["total"].metadata[0] # pylint: disable=unsubscriptable-object
assert field.reducer.name == "wf.std.modulo_add"
assert field.reducer.config["modulus"] == 10
+53
View File
@@ -60,6 +60,59 @@ def test_missing_nested_node_output_path_fails() -> None:
)
def test_root_node_local_paths_map_whole_input_and_output_payloads() -> None:
workflow = Workflow(
name="root_mapping",
input_schema=SchemaRef.model_validate(
{
"type": "object",
"properties": {"rates": {"type": "object"}},
}
),
state_schema=StateSchema(fields={"rates": StateField(type="object")}),
output_schema=SchemaRef(type="object", properties={}),
node_defs=[
NodeDef(
name="force_rates",
input_schema=SchemaRef(type="object", properties={}),
output_schema=SchemaRef(
type="object",
properties={
"r_1": {"type": "number"},
"r_10": {"type": "number"},
},
),
outcomes=["ok"],
)
],
start="force",
nodes=[
NodeUse(
id="force",
type="node",
node="force_rates",
in_map={"input.rates": "."},
out_map={".": "state.rates"},
)
],
edges=[Edge.model_validate({"from": "force", "outcome": "ok", "to": END})],
)
run = execute_workflow(
workflow,
{"rates": {"r_1": 0.9, "r_10": 0.1}},
{
"force_rates": lambda payload, _ctx: {
"outcome": "ok",
"output": {"r_1": 0.0, "r_10": payload["r_10"]},
}
},
)
assert run.trace[0].resolved_input == {"r_1": 0.9, "r_10": 0.1}
assert run.state["rates"] == {"r_1": 0.0, "r_10": 0.1}
def _nested_mapping_workflow() -> Workflow:
return Workflow(
name="nested_mapping",
+25 -19
View File
@@ -1,6 +1,7 @@
import random
from typing import Final
from tests.rewrite.models import (
ContextInput,
Countdown,
@@ -15,7 +16,7 @@ from tests.rewrite.models import (
how_do_i_explain_this,
)
from wf_authoring import NodeReturn, node
from wf_authoring.nodes.result import Nothing, outcome
from wf_authoring.nodes.result import NoOutput, Nothing, outcome
from wf_core.tokens import END
# to the functions
@@ -24,9 +25,9 @@ from wf_core.tokens import END
@node
def init(
inp: Input,
_inp: Input,
# ) -> NodeReturn[Input | Nothing]: # JUST doesnt work if the types are above
) -> Input:
) -> None:
# why cant use basemodel? should we convert typeddicts to basemodels? Why cant use
"""i hope that this is done by default, because langgraph DOESNT. why.
@@ -41,11 +42,13 @@ def init(
# )
# if inp.pity_120_available and inp.context["type"] == "normal":
# "i doesnt care"
return inp
# return Nothing()
# return None
# return inp
@node(outcomes=("0", "65"))
def rate_booster(c: Counters) -> NodeReturn[Nothing]:
def rate_booster(c: Counters) -> NoOutput:
"""This was an edge.
Should I implement r65 here too... i think not.
@@ -80,7 +83,7 @@ class CountersContextOutputInputAhhModelType(
@node(outcomes=("240", "80", "10", "1"))
def pre_roll_router(c: CountersContextOutputInputAhhModelType) -> NodeReturn[Nothing]:
def pre_roll_router(c: CountersContextOutputInputAhhModelType) -> NoOutput:
"""another edge.
the conditional router calculates which to reset to 0 (guarantee the rest)
@@ -152,12 +155,13 @@ class RateChange:
else:
r240 = 0
r80 = br["r_80"] * (1 + rpn)
r10 = br["r_10"] # use initial rates because i dont know how this works
r1 = 1 - r240 - r80 - r10
return Rates.model_validate(
{
"rates": {
"r_1": 1 - r240 - r80 - br["r_10"],
"r_10": br["r_10"],
"r_1": r1,
"r_10": r10,
"r_80": r80,
"r_240": r240,
}
@@ -173,24 +177,27 @@ class RateChange:
class CounterUp:
@node(name="counter 6* reset")
@staticmethod
def c80(c: Counters) -> Counters:
def c80(_: Nothing) -> Counters:
return Counters.model_validate(
{
"counter": {
"c_80": 0,
"c_10": 0,
},
"simple_counter": c.simple_counter,
"simple_counter": 0,
# this is influenced by the add reducer.
# its top level. it doesnt reset. its a miracle. i hate this.
}
)
@node(name="counter 5* reset")
@staticmethod
def c10(c: Counters) -> Counters:
c80 = c.counter["c_80"]
return Counters.model_validate(
{
"counter": {"c_10": 0, "c_80": c.counter["c_80"]}, # merge with or_!
"simple_counter": c.simple_counter,
"counter": {"c_10": 0, "c_80": c80}, # merge with or_!
"simple_counter": 0,
}
)
@@ -198,9 +205,10 @@ class CounterUp:
@staticmethod
def c1(state: Counters) -> Counters:
c = state.counter
c10, c80 = c["c_10"], c["c_80"]
return Counters(
simple_counter=state.simple_counter + 1,
counter={"c_10": (c["c_10"] + 1) % 10, "c_80": (c["c_80"] + 1) % 80},
simple_counter=1,
counter={"c_10": (c10 + 1) % 10, "c_80": (c80 + 1) % 80},
)
@@ -241,10 +249,8 @@ def roll(state: CurrentPools) -> ThisStorage:
@node(outcomes=("240", "80", "10", "1")) # missed this! good job.
def post_roll_router(
state: CurrentRoll,
) -> NodeReturn[
Nothing
]: # Literal["240", "80", "10", "1"] maybe you need to encode this in node returns. Literal of strings.
return NodeReturn(state.this["category"], Nothing())
) -> NoOutput: # Literal["240", "80", "10", "1"] maybe you need to encode this in node returns. Literal of strings.
return outcome(state.this["category"])
@node(name="main")
+19 -4
View File
@@ -3,6 +3,7 @@ from typing import Annotated, Literal, TypedDict
from pydantic import BaseModel, Field
from wf_authoring.schemas import state_field
from wf_core.models.reducers import ReducerRef
class SophisticatedRates(TypedDict):
@@ -17,8 +18,20 @@ class SophisticatedRates(TypedDict):
class SophisticatedCounter(TypedDict):
c_10: Annotated[int, state_field(reducer="wf.std.add")] # how do i convey "add" sublevel? can we have plugins for this? should we cover this; since langgraph doesnt.
c_80: Annotated[int, state_field(reducer="wf.std.add")]
c_10: Annotated[
int,
state_field(
reducer=ReducerRef(name="wf.std.modulo_add", config={"modulus": 10})
),
] # add!
c_80: Annotated[
int,
state_field(
reducer=ReducerRef(name="wf.std.modulo_add", config={"modulus": 80})
),
]
# all that reducerref and then nothing touches them. only way you do rn is
# putting in/out maps on graph.use, which is cool ig. i just am not using them.
class Counters(BaseModel):
@@ -27,7 +40,7 @@ class Counters(BaseModel):
default_factory=lambda: SophisticatedCounter(c_10=0, c_80=0)
) # or, that is because of langgraph limitation,
# id prefer counter.update with dict.update override (Overwrite, not like that ever worked)
simple_counter: int # add
simple_counter: Annotated[int, state_field(reducer="wf.std.add")] # add
class Countdown(BaseModel):
@@ -120,7 +133,7 @@ class CurrentRoll(BaseModel):
this: Entity
class PartialRates(SophisticatedRates, total=False):
class PartialRates(SophisticatedRates, TypedDict, total=False):
pass
@@ -146,6 +159,8 @@ class State(
"this forces basemodel, i used typeddict"
# this feels fragmented. it is fragmented
# countdown: int # input carries here, should input have a bound like all(attr(input) in attr(state))? how tf do i even try to type that
# simple_counter: int # how to signal that ts adds up? we have that.
# counter: SophisticatedCounter
+16 -5
View File
@@ -6,7 +6,6 @@ Constraints:
"""
from itertools import islice
import json
from pprint import pprint
from typing import Any
@@ -64,19 +63,31 @@ def test():
d = gacha.execute(
build_input(context)(
20, # lets be optimistic
rolled_previously=240 - 135,
until_5=5,
until_6=73,
rolled_previously=240 - 135, # 15
until_5=5, # 5
until_6=73, # 7
good_stuff=False, # lets pretend
).model_dump()
)
assert d.status == RunStatus.COMPLETED, "oops"
state = State.model_validate(d.state)
pprint(state.storage)
pprint(
[
t
for t in d.trace
if t.node_id
in (
"counter_up",
"tick",
)
]
)
pprint(d.state)
assert any(
i["name"] in context["pool"]["n_240"]
for i in islice(state.storage, 120 - (240 - 135))
), "pity logic failed"
pprint(state.storage)
# pprint(gacha.compile().edges)
# pprint(gacha.compile().nodes)
+4 -1
View File
@@ -1,3 +1,4 @@
from tests.authoring.test_reducers import modulo_add
from tests.rewrite.actions import (
CounterUp,
RateChange,
@@ -12,6 +13,7 @@ from tests.rewrite.actions import (
from tests.rewrite.models import Input, State, Storage
from wf_authoring.builder import WorkflowBuilder
from wf_authoring.dsl.conditions import expr, state
from wf_authoring.reducers.catalog import ReducerCatalog
from wf_core.tokens import END
# alr so we start with workflowbuilder.
@@ -21,6 +23,7 @@ gacha = WorkflowBuilder(
input_schema=Input,
output_schema=Storage, # could be State, since the OG doesnt care, ill probably dump out the list.
state_schema=State,
reducers=ReducerCatalog.from_reducers(modulo_add),
)
"example workflow"
@@ -53,7 +56,7 @@ rate_route = gacha.route(
True: rate_up,
False: rate_same,
},
id = "rate_booster"
id="rate_booster",
)
gacha.use(pre_roll_router, id="router")
+5 -3
View File
@@ -111,15 +111,16 @@ def test_service_lists_all_capability_sources_with_owned_capability_names() -> N
std_source = sources_by_id["wf.std"]
assert "wf.std.runtime_error" in std_source["capabilities"]["node_specs"]
assert std_source["capabilities"]["reducers"] == [
assert set(std_source["capabilities"]["reducers"]) == {
"wf.std.append",
"wf.std.max",
"wf.std.merge_object",
"wf.std.replace",
"wf.std.set_union",
]
"wf.std.add",
}
assert std_source["capabilities"]["tools"] == []
assert std_source["reducer_count"] == 5
assert std_source["reducer_count"] == 6
mcp_source = sources_by_id["wf.mcp"]
assert mcp_source["capabilities"]["node_specs"] == ["wf.mcp.call_tool"]
@@ -164,6 +165,7 @@ def test_wf_std_source_contains_builtin_reducers() -> None:
"wf.std.max",
"wf.std.merge_object",
"wf.std.set_union",
"wf.std.add",
}