mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-20 22:52:29 +02:00
149 lines
5.2 KiB
Python
149 lines
5.2 KiB
Python
import pytest
|
|
from typing_extensions import TypedDict
|
|
|
|
from saf_python_sdk.advanced_graph import (
|
|
AdvancedStateGraph,
|
|
Context,
|
|
any_of,
|
|
channel_condition,
|
|
)
|
|
from saf_python_sdk.types import Command, Send
|
|
|
|
pytestmark = pytest.mark.anyio
|
|
|
|
|
|
class PrimitiveState(TypedDict):
|
|
counter: int
|
|
logs: list[str]
|
|
done: str | None
|
|
|
|
|
|
async def test_input_and_state_primitives_are_compatible() -> None:
|
|
graph = AdvancedStateGraph(PrimitiveState)
|
|
|
|
async def start_node(state: PrimitiveState) -> Command:
|
|
state["logs"].append(f"start:counter={state['counter']}")
|
|
return Command(goto=Send("middle_node", "from_start"))
|
|
|
|
async def middle_node(ctx: Context, tool_input: str, state: PrimitiveState) -> Command:
|
|
state["logs"].append(f"middle:input={tool_input}")
|
|
return Command(update=state, goto=Send("finish_node", "from_middle"))
|
|
|
|
async def finish_node(payload: str, state: PrimitiveState) -> dict[str, object]:
|
|
state["logs"].append(f"finish:input={payload}")
|
|
return {
|
|
"logs": state["logs"],
|
|
"counter": state["counter"],
|
|
"done": payload,
|
|
}
|
|
|
|
graph.add_entry_node(start_node)
|
|
graph.add_node(middle_node)
|
|
graph.add_finish_node(finish_node)
|
|
|
|
result = await graph.compile().ainvoke({"counter": 7, "logs": [], "done": None})
|
|
assert result["counter"] == 7
|
|
assert result["done"] == "from_middle"
|
|
assert result["logs"] == [
|
|
"start:counter=7",
|
|
"middle:input=from_start",
|
|
"finish:input=from_middle",
|
|
]
|
|
|
|
|
|
async def test_run_ends_without_finish_node() -> None:
|
|
graph = AdvancedStateGraph(PrimitiveState)
|
|
|
|
async def start_node(state: PrimitiveState) -> Command:
|
|
state["logs"].append("start")
|
|
return Command(update=state, goto=Send("middle_node", "from_start"))
|
|
|
|
async def middle_node(input: str, state: PrimitiveState) -> dict[str, object]:
|
|
state["logs"].append(f"middle:{input}")
|
|
return {"counter": state["counter"] + 1, "logs": state["logs"], "done": "stopped"}
|
|
|
|
graph.add_entry_node(start_node)
|
|
graph.add_node(middle_node)
|
|
|
|
result = await graph.compile().ainvoke({"counter": 7, "logs": [], "done": None})
|
|
assert result["counter"] == 8
|
|
assert result["done"] == "stopped"
|
|
assert result["logs"] == ["start", "middle:from_start"]
|
|
|
|
|
|
async def test_channel_wait_respects_max_m() -> None:
|
|
graph = AdvancedStateGraph(PrimitiveState)
|
|
graph.add_async_channel("events", list[str])
|
|
|
|
async def start_node(ctx: Context, state: PrimitiveState) -> Command:
|
|
ctx.publish_to_channel("events", "a")
|
|
ctx.publish_to_channel("events", "b")
|
|
ctx.publish_to_channel("events", "c")
|
|
return Command(update=state, goto=Send("wait_node", None))
|
|
|
|
async def wait_node(ctx: Context, _input: None, state: PrimitiveState) -> dict[str, object]:
|
|
result = await ctx.wait_for(channel_condition("events", min=2, max=4))
|
|
values = result.conditions[0].values or []
|
|
assert isinstance(values, list)
|
|
return {"counter": len(values), "logs": values, "done": "ok"}
|
|
|
|
graph.add_entry_node(start_node)
|
|
graph.add_finish_node(wait_node)
|
|
|
|
result = await graph.compile().ainvoke({"counter": 0, "logs": [], "done": None})
|
|
assert result["counter"] == 3
|
|
assert result["logs"] == ["a", "b", "c"]
|
|
assert result["done"] == "ok"
|
|
|
|
|
|
async def test_any_of_consumes_all_ready_channels() -> None:
|
|
graph = AdvancedStateGraph(PrimitiveState)
|
|
graph.add_async_channel("alpha", str)
|
|
graph.add_async_channel("beta", str)
|
|
|
|
async def start_node(ctx: Context, state: PrimitiveState) -> Command:
|
|
ctx.publish_to_channel("alpha", "a1")
|
|
ctx.publish_to_channel("beta", "b1")
|
|
return Command(update=state, goto=Send("wait_node", None))
|
|
|
|
async def wait_node(ctx: Context, _input: None, state: PrimitiveState) -> Command:
|
|
first = await ctx.wait_for(
|
|
any_of(channel_condition("alpha"), channel_condition("beta"))
|
|
)
|
|
assert len(first.conditions) == 2
|
|
assert first.conditions[0].met is True
|
|
assert first.conditions[0].channel_name == "alpha"
|
|
assert first.conditions[0].values == ["a1"]
|
|
assert first.conditions[1].met is True
|
|
assert first.conditions[1].channel_name == "beta"
|
|
assert first.conditions[1].values == ["b1"]
|
|
ctx.publish_to_channel("beta", "b2")
|
|
return Command(
|
|
update={"counter": 1, "logs": ["matched=2"], "done": None},
|
|
goto=Send("verify_node", None),
|
|
)
|
|
|
|
async def verify_node(
|
|
ctx: Context, _input: None, state: PrimitiveState
|
|
) -> dict[str, object]:
|
|
second = await ctx.wait_for(channel_condition("beta"))
|
|
values = second.conditions[0].values or []
|
|
return {
|
|
"counter": 2,
|
|
"logs": [*state["logs"], f"beta={values[0]}"],
|
|
"done": "ok",
|
|
}
|
|
|
|
graph.add_entry_node(start_node)
|
|
graph.add_node(wait_node)
|
|
graph.add_finish_node(verify_node)
|
|
|
|
result = await graph.compile().ainvoke({"counter": 0, "logs": [], "done": None})
|
|
assert result["counter"] == 2
|
|
assert result["logs"] == [
|
|
"matched=2",
|
|
"beta=b2",
|
|
]
|
|
assert result["done"] == "ok"
|
|
|