misc changes: returns None, root as ., add
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user