mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-09-11 04:07:52 +02:00
Implement get_graph for imperative api (#3076)
This commit is contained in:
@@ -4,6 +4,7 @@ import concurrent.futures
|
||||
import functools
|
||||
import inspect
|
||||
import types
|
||||
from collections.abc import Iterator
|
||||
from typing import (
|
||||
Any,
|
||||
Awaitable,
|
||||
@@ -14,6 +15,9 @@ from typing import (
|
||||
overload,
|
||||
)
|
||||
|
||||
from langchain_core.runnables.base import Runnable
|
||||
from langchain_core.runnables.config import RunnableConfig
|
||||
from langchain_core.runnables.graph import Graph, Node
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
from langgraph.channels.ephemeral_value import EphemeralValue
|
||||
@@ -22,6 +26,7 @@ from langgraph.checkpoint.base import BaseCheckpointSaver
|
||||
from langgraph.constants import CONF, END, START, TAG_HIDDEN
|
||||
from langgraph.pregel import Pregel
|
||||
from langgraph.pregel.call import get_runnable_for_func
|
||||
from langgraph.pregel.protocol import PregelProtocol
|
||||
from langgraph.pregel.read import PregelNode
|
||||
from langgraph.pregel.write import ChannelWrite, ChannelWriteEntry
|
||||
from langgraph.store.base import BaseStore
|
||||
@@ -157,9 +162,9 @@ def task(
|
||||
def _tick(__allargs__: tuple) -> T:
|
||||
return func(*__allargs__[0], **__allargs__[1])
|
||||
|
||||
return functools.update_wrapper(
|
||||
functools.partial(call, _tick, retry=retry), func
|
||||
)
|
||||
wrapper = functools.partial(call, _tick, retry=retry)
|
||||
object.__setattr__(wrapper, "_is_pregel_task", True)
|
||||
return functools.update_wrapper(wrapper, func)
|
||||
|
||||
if __func_or_none__ is not None:
|
||||
return decorator(__func_or_none__)
|
||||
@@ -408,7 +413,7 @@ def entrypoint(
|
||||
else Any
|
||||
)
|
||||
|
||||
return Pregel(
|
||||
return EntrypointPregel(
|
||||
nodes={
|
||||
func.__name__: PregelNode(
|
||||
bound=bound,
|
||||
@@ -432,3 +437,97 @@ def entrypoint(
|
||||
)
|
||||
|
||||
return _imp
|
||||
|
||||
|
||||
class EntrypointPregel(Pregel):
|
||||
def get_graph(
|
||||
self,
|
||||
config: Optional[RunnableConfig] = None,
|
||||
*,
|
||||
xray: Union[int, bool] = False,
|
||||
) -> Graph:
|
||||
name, entrypoint = next(iter(self.nodes.items()))
|
||||
graph = Graph()
|
||||
node = Node(f"__{name}", name, entrypoint.bound, None)
|
||||
graph.nodes[node.id] = node
|
||||
candidates: list[tuple[Node, Union[Callable, PregelProtocol]]] = [
|
||||
*_find_children(entrypoint.bound, node)
|
||||
]
|
||||
seen: set[Union[Callable, PregelProtocol]] = set()
|
||||
for parent, child in candidates:
|
||||
if child in seen:
|
||||
continue
|
||||
else:
|
||||
seen.add(child)
|
||||
if callable(child):
|
||||
node = Node(f"__{child.__name__}", child.__name__, child, None) # type: ignore[arg-type]
|
||||
graph.nodes[node.id] = node
|
||||
graph.add_edge(parent, node, conditional=True)
|
||||
graph.add_edge(node, parent)
|
||||
candidates.extend(_find_children(child, node))
|
||||
elif isinstance(child, Runnable):
|
||||
if xray > 0:
|
||||
graph = child.get_graph(config, xray=xray - 1 if xray else 0)
|
||||
graph.trim_first_node()
|
||||
graph.trim_last_node()
|
||||
s, e = graph.extend(graph, prefix=child.name or "")
|
||||
if s is None:
|
||||
raise ValueError(
|
||||
f"Could not extend subgraph '{child.name}' due to missing entrypoint"
|
||||
)
|
||||
else:
|
||||
graph.add_edge(parent, s, conditional=True)
|
||||
if e is not None:
|
||||
graph.add_edge(e, parent)
|
||||
else:
|
||||
node = graph.add_node(child, child.name)
|
||||
graph.add_edge(parent, node, conditional=True)
|
||||
graph.add_edge(node, parent)
|
||||
return graph
|
||||
|
||||
|
||||
def _find_children(
|
||||
candidate: Union[Callable, Runnable], parent: Node
|
||||
) -> Iterator[tuple[Node, Union[Callable, PregelProtocol]]]:
|
||||
from langchain_core.runnables.utils import get_function_nonlocals
|
||||
|
||||
from langgraph.utils.runnable import (
|
||||
RunnableCallable,
|
||||
RunnableLambda,
|
||||
RunnableSeq,
|
||||
RunnableSequence,
|
||||
)
|
||||
|
||||
candidates: list[Union[Callable, Runnable]] = []
|
||||
if callable(candidate) and getattr(candidate, "_is_pregel_task", False) is True:
|
||||
candidates.extend(
|
||||
nl.__self__ if hasattr(nl, "__self__") else nl
|
||||
for nl in get_function_nonlocals(
|
||||
candidate.__wrapped__
|
||||
if hasattr(candidate, "__wrapped__") and callable(candidate.__wrapped__)
|
||||
else candidate
|
||||
)
|
||||
)
|
||||
else:
|
||||
candidates.append(candidate)
|
||||
|
||||
for c in candidates:
|
||||
if callable(c) and getattr(c, "_is_pregel_task", False) is True:
|
||||
yield (parent, c)
|
||||
elif isinstance(c, PregelProtocol):
|
||||
yield (parent, c)
|
||||
elif isinstance(c, RunnableSequence) or isinstance(c, RunnableSeq):
|
||||
candidates.extend(c.steps)
|
||||
elif isinstance(c, RunnableLambda):
|
||||
candidates.extend(c.deps)
|
||||
elif isinstance(c, RunnableCallable):
|
||||
if c.func is not None:
|
||||
candidates.extend(
|
||||
nl.__self__ if hasattr(nl, "__self__") else nl
|
||||
for nl in get_function_nonlocals(c.func)
|
||||
)
|
||||
elif c.afunc is not None:
|
||||
candidates.extend(
|
||||
nl.__self__ if hasattr(nl, "__self__") else nl
|
||||
for nl in get_function_nonlocals(c.afunc)
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import asyncio
|
||||
import concurrent.futures
|
||||
import threading
|
||||
import time
|
||||
from functools import partial
|
||||
from typing import (
|
||||
@@ -7,11 +8,13 @@ from typing import (
|
||||
AsyncIterator,
|
||||
Awaitable,
|
||||
Callable,
|
||||
Generic,
|
||||
Iterable,
|
||||
Iterator,
|
||||
Optional,
|
||||
Sequence,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
@@ -39,6 +42,56 @@ from langgraph.pregel.retry import arun_with_retry, run_with_retry
|
||||
from langgraph.types import PregelExecutableTask, RetryPolicy
|
||||
from langgraph.utils.future import chain_future
|
||||
|
||||
F = TypeVar("F", concurrent.futures.Future, asyncio.Future)
|
||||
E = TypeVar("E", threading.Event, asyncio.Event)
|
||||
|
||||
|
||||
class FuturesDict(Generic[F, E], dict[F, Optional[PregelExecutableTask]]):
|
||||
event: E
|
||||
callback: Callable[[PregelExecutableTask, Optional[BaseException]], None]
|
||||
counter: int
|
||||
done: set[F]
|
||||
lock: threading.Lock
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
event: E,
|
||||
callback: Callable[[PregelExecutableTask, Optional[BaseException]], None],
|
||||
future_type: Type[F],
|
||||
# used for generic typing, newer py supports FutureDict[...](...)
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.lock = threading.Lock()
|
||||
self.event = event
|
||||
self.callback = callback
|
||||
self.counter = 0
|
||||
self.done: set[F] = set()
|
||||
|
||||
def __setitem__(
|
||||
self,
|
||||
key: F,
|
||||
value: Optional[PregelExecutableTask],
|
||||
) -> None:
|
||||
super().__setitem__(key, value) # type: ignore[index]
|
||||
if value is not None:
|
||||
with self.lock:
|
||||
self.counter += 1
|
||||
key.add_done_callback(partial(self.on_done, value))
|
||||
|
||||
def on_done(
|
||||
self,
|
||||
task: PregelExecutableTask,
|
||||
fut: F,
|
||||
) -> None:
|
||||
try:
|
||||
self.callback(task, _exception(fut))
|
||||
finally:
|
||||
with self.lock:
|
||||
self.done.add(fut)
|
||||
self.counter -= 1
|
||||
if self.counter == 0 or _should_stop_others(self.done):
|
||||
self.event.set()
|
||||
|
||||
|
||||
class PregelRunner:
|
||||
"""Responsible for executing a set of Pregel tasks concurrently, committing
|
||||
@@ -138,7 +191,6 @@ class PregelRunner:
|
||||
# updates from this tick are committed/streamed first
|
||||
__next_tick__=True,
|
||||
)
|
||||
fut.add_done_callback(partial(self.commit, next_task))
|
||||
futures[fut] = next_task
|
||||
rtn[idx] = fut
|
||||
return [rtn.get(i) for i in range(len(writes))]
|
||||
@@ -151,17 +203,26 @@ class PregelRunner:
|
||||
retry: Optional[RetryPolicy] = None,
|
||||
callbacks: Callbacks = None,
|
||||
) -> concurrent.futures.Future[Any]:
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
raise RuntimeError("In an sync context async tasks cannot be called")
|
||||
(fut,) = writer(
|
||||
task,
|
||||
[(PUSH, None)],
|
||||
calls=[Call(func, input, retry=retry, callbacks=callbacks)],
|
||||
)
|
||||
assert fut is not None, "writer did not return a future for call"
|
||||
return fut
|
||||
# return a chained future to ensure commit() callback is called
|
||||
# before the returned future is resolved, to ensure stream order etc
|
||||
sfut: concurrent.futures.Future[Any] = concurrent.futures.Future()
|
||||
chain_future(fut, sfut)
|
||||
return sfut
|
||||
|
||||
tasks = tuple(tasks)
|
||||
futures: dict[concurrent.futures.Future, Optional[PregelExecutableTask]] = {}
|
||||
done_futures: set[concurrent.futures.Future] = set()
|
||||
futures = FuturesDict(
|
||||
callback=self.commit,
|
||||
event=threading.Event(),
|
||||
future_type=concurrent.futures.Future,
|
||||
)
|
||||
# give control back to the caller
|
||||
yield
|
||||
# fast path if single task with no timeout and no waiter
|
||||
@@ -178,12 +239,12 @@ class PregelRunner:
|
||||
)
|
||||
self.commit(t, None)
|
||||
except Exception as exc:
|
||||
self.commit(t, None, exc)
|
||||
self.commit(t, exc)
|
||||
if reraise and futures:
|
||||
# will be re-raised after futures are done
|
||||
fut: concurrent.futures.Future = concurrent.futures.Future()
|
||||
fut.set_exception(exc)
|
||||
done_futures.add(fut)
|
||||
futures.done.add(fut)
|
||||
elif reraise:
|
||||
raise
|
||||
if not futures: # maybe `t` schuduled another task
|
||||
@@ -206,7 +267,6 @@ class PregelRunner:
|
||||
},
|
||||
__reraise_on_exit__=reraise,
|
||||
)
|
||||
fut.add_done_callback(partial(self.commit, t))
|
||||
futures[fut] = t
|
||||
# execute tasks, and wait for one to fail or all to finish.
|
||||
# each task is independent from all other concurrent tasks
|
||||
@@ -226,9 +286,6 @@ class PregelRunner:
|
||||
# waiter task finished, schedule another
|
||||
if inflight and get_waiter is not None:
|
||||
futures[get_waiter()] = None
|
||||
else:
|
||||
# store for panic check
|
||||
done_futures.add(fut)
|
||||
else:
|
||||
# remove references to loop vars
|
||||
del fut, task
|
||||
@@ -237,13 +294,13 @@ class PregelRunner:
|
||||
break
|
||||
# give control back to the caller
|
||||
yield
|
||||
# wait for pending done callbacks
|
||||
# if a 2nd future finishes while `wait` is returning, it's possible
|
||||
# that done callbacks for the 2nd future aren't called until next tick
|
||||
time.sleep(0)
|
||||
# wait for done callbacks
|
||||
futures.event.wait(
|
||||
timeout=(max(0, end_time - time.monotonic()) if end_time else None)
|
||||
)
|
||||
# panic on failure or timeout
|
||||
_panic_or_proceed(
|
||||
done_futures.union(f for f, t in futures.items() if t is not None),
|
||||
futures.done.union(f for f, t in futures.items() if t is not None),
|
||||
panic=reraise,
|
||||
)
|
||||
|
||||
@@ -293,7 +350,7 @@ class PregelRunner:
|
||||
rtn[idx] = fut
|
||||
elif next_task.writes:
|
||||
# if it already ran, return the result
|
||||
fut = asyncio.Future()
|
||||
fut = asyncio.Future(loop=loop)
|
||||
ret = next(
|
||||
(v for c, v in next_task.writes if c == RETURN), MISSING
|
||||
)
|
||||
@@ -331,7 +388,6 @@ class PregelRunner:
|
||||
__next_tick__=True,
|
||||
),
|
||||
)
|
||||
fut.add_done_callback(partial(self.commit, next_task))
|
||||
futures[fut] = next_task
|
||||
rtn[idx] = fut
|
||||
return [rtn.get(i) for i in range(len(writes))]
|
||||
@@ -344,23 +400,29 @@ class PregelRunner:
|
||||
retry: Optional[RetryPolicy] = None,
|
||||
callbacks: Callbacks = None,
|
||||
) -> Union[asyncio.Future[Any], concurrent.futures.Future[Any]]:
|
||||
if not asyncio.iscoroutinefunction(func):
|
||||
raise RuntimeError(
|
||||
"In an async context use func.to_thread(...) to invoke tasks"
|
||||
)
|
||||
(fut,) = writer(
|
||||
task,
|
||||
[(PUSH, None)],
|
||||
calls=[Call(func, input, retry=retry, callbacks=callbacks)],
|
||||
)
|
||||
assert fut is not None, "writer did not return a future for call"
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
return fut
|
||||
# adapted from asyncio.run_coroutine_threadsafe
|
||||
sfut: concurrent.futures.Future = concurrent.futures.Future()
|
||||
loop.call_soon_threadsafe(chain_future, fut, sfut)
|
||||
# return a chained future to ensure commit() callback is called
|
||||
# before the returned future is resolved, to ensure stream order etc
|
||||
sfut: asyncio.Future[Any] = asyncio.Future(loop=loop)
|
||||
chain_future(fut, sfut)
|
||||
return sfut
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
tasks = tuple(tasks)
|
||||
futures: dict[asyncio.Future, Optional[PregelExecutableTask]] = {}
|
||||
done_futures: set[asyncio.Future] = set()
|
||||
futures = FuturesDict(
|
||||
callback=self.commit,
|
||||
event=asyncio.Event(),
|
||||
future_type=asyncio.Future,
|
||||
)
|
||||
# give control back to the caller
|
||||
yield
|
||||
# fast path if single task with no waiter and no timeout
|
||||
@@ -378,12 +440,12 @@ class PregelRunner:
|
||||
)
|
||||
self.commit(t, None)
|
||||
except Exception as exc:
|
||||
self.commit(t, None, exc)
|
||||
self.commit(t, exc)
|
||||
if reraise and futures:
|
||||
# will be re-raised after futures are done
|
||||
fut: asyncio.Future = loop.create_future()
|
||||
fut.set_exception(exc)
|
||||
done_futures.add(fut)
|
||||
futures.done.add(fut)
|
||||
elif reraise:
|
||||
raise
|
||||
if not futures: # maybe `t` schuduled another task
|
||||
@@ -412,7 +474,6 @@ class PregelRunner:
|
||||
__reraise_on_exit__=reraise,
|
||||
),
|
||||
)
|
||||
fut.add_done_callback(partial(self.commit, t))
|
||||
futures[fut] = t
|
||||
# execute tasks, and wait for one to fail or all to finish.
|
||||
# each task is independent from all other concurrent tasks
|
||||
@@ -432,9 +493,6 @@ class PregelRunner:
|
||||
# waiter task finished, schedule another
|
||||
if inflight and get_waiter is not None:
|
||||
futures[get_waiter()] = None
|
||||
else:
|
||||
# store for panic check
|
||||
done_futures.add(fut)
|
||||
else:
|
||||
# remove references to loop vars
|
||||
del fut, task
|
||||
@@ -443,16 +501,17 @@ class PregelRunner:
|
||||
break
|
||||
# give control back to the caller
|
||||
yield
|
||||
# wait for pending done callbacks
|
||||
# if a 2nd future finishes while `wait` is returning, it's possible
|
||||
# that done callbacks for the 2nd future aren't called until next tick
|
||||
await asyncio.sleep(0)
|
||||
# wait for done callbacks
|
||||
await asyncio.wait_for(
|
||||
futures.event.wait(),
|
||||
timeout=(max(0, end_time - loop.time()) if end_time else None),
|
||||
)
|
||||
# cancel waiter task
|
||||
for fut in futures:
|
||||
fut.cancel()
|
||||
# panic on failure or timeout
|
||||
_panic_or_proceed(
|
||||
done_futures.union(f for f, t in futures.items() if t is not None),
|
||||
futures.done.union(f for f, t in futures.items() if t is not None),
|
||||
timeout_exc_cls=asyncio.TimeoutError,
|
||||
panic=reraise,
|
||||
)
|
||||
@@ -460,11 +519,8 @@ class PregelRunner:
|
||||
def commit(
|
||||
self,
|
||||
task: PregelExecutableTask,
|
||||
fut: Union[None, concurrent.futures.Future[Any], asyncio.Future[Any]],
|
||||
exception: Optional[BaseException] = None,
|
||||
exception: Optional[BaseException],
|
||||
) -> None:
|
||||
if fut is not None:
|
||||
exception = _exception(fut)
|
||||
if isinstance(exception, asyncio.CancelledError):
|
||||
# for cancelled tasks, also save error in task,
|
||||
# so loop can finish super-step
|
||||
@@ -495,7 +551,7 @@ class PregelRunner:
|
||||
|
||||
|
||||
def _should_stop_others(
|
||||
done: Union[set[concurrent.futures.Future[Any]], set[asyncio.Future[Any]]],
|
||||
done: set[F],
|
||||
) -> bool:
|
||||
"""Check if any task failed, if so, cancel all other tasks.
|
||||
GraphInterrupts are not considered failures."""
|
||||
|
||||
@@ -112,13 +112,16 @@ def _chain_future(source: AnyFuture, destination: AnyFuture) -> None:
|
||||
source.add_done_callback(_call_set_state)
|
||||
|
||||
|
||||
def chain_future(source: AnyFuture, destination: concurrent.futures.Future) -> None:
|
||||
def chain_future(source: AnyFuture, destination: AnyFuture) -> None:
|
||||
# adapted from asyncio.run_coroutine_threadsafe
|
||||
try:
|
||||
_chain_future(source, destination)
|
||||
except (SystemExit, KeyboardInterrupt):
|
||||
raise
|
||||
except BaseException as exc:
|
||||
if destination.set_running_or_notify_cancel():
|
||||
if isinstance(destination, concurrent.futures.Future):
|
||||
if destination.set_running_or_notify_cancel():
|
||||
destination.set_exception(exc)
|
||||
else:
|
||||
destination.set_exception(exc)
|
||||
raise
|
||||
|
||||
Generated
+3
-3
@@ -1324,14 +1324,14 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain-core"
|
||||
version = "0.3.25"
|
||||
version = "0.3.30"
|
||||
description = "Building applications with LLMs through composability"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.9"
|
||||
groups = ["main", "dev"]
|
||||
files = [
|
||||
{file = "langchain_core-0.3.25-py3-none-any.whl", hash = "sha256:e10581c6c74ba16bdc6fdf16b00cced2aa447cc4024ed19746a1232918edde38"},
|
||||
{file = "langchain_core-0.3.25.tar.gz", hash = "sha256:fdb8df41e5cdd928c0c2551ebbde1cea770ee3c64598395367ad77ddf9acbae7"},
|
||||
{file = "langchain_core-0.3.30-py3-none-any.whl", hash = "sha256:0a4c4e02fac5968b67fbb0142c00c2b976c97e45fce62c7ac9eb1636a6926493"},
|
||||
{file = "langchain_core-0.3.30.tar.gz", hash = "sha256:0f1281b4416977df43baf366633ad18e96c5dcaaeae6fcb8a799f9889c853243"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1521,9 +1521,77 @@ def test_imp_task(request: pytest.FixtureRequest, checkpointer_name: str) -> Non
|
||||
assert mapper_calls == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
|
||||
def test_imp_nested(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str, snapshot: SnapshotAssertion
|
||||
) -> None:
|
||||
checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}")
|
||||
|
||||
def mynode(input: list[str]) -> list[str]:
|
||||
return [it + "a" for it in input]
|
||||
|
||||
builder = StateGraph(list[str])
|
||||
builder.add_node(mynode)
|
||||
builder.add_edge(START, "mynode")
|
||||
add_a = builder.compile()
|
||||
|
||||
@task
|
||||
def submapper(input: int) -> str:
|
||||
return str(input)
|
||||
|
||||
@task()
|
||||
def mapper(input: int) -> str:
|
||||
time.sleep(input / 100)
|
||||
return submapper(input).result() * 2
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def graph(input: list[int]) -> list[str]:
|
||||
futures = [mapper(i) for i in input]
|
||||
mapped = [f.result() for f in futures]
|
||||
answer = interrupt("question")
|
||||
final = [m + answer for m in mapped]
|
||||
return add_a.invoke(final)
|
||||
|
||||
assert graph.get_input_jsonschema() == {
|
||||
"type": "array",
|
||||
"items": {"type": "integer"},
|
||||
"title": "LangGraphInput",
|
||||
}
|
||||
assert graph.get_output_jsonschema() == {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"title": "LangGraphOutput",
|
||||
}
|
||||
|
||||
assert graph.get_graph().draw_mermaid() == snapshot
|
||||
|
||||
thread1 = {"configurable": {"thread_id": "1"}}
|
||||
assert [*graph.stream([0, 1], thread1)] == [
|
||||
{"submapper": "0"},
|
||||
{"mapper": "00"},
|
||||
{"submapper": "1"},
|
||||
{"mapper": "11"},
|
||||
{
|
||||
"__interrupt__": (
|
||||
Interrupt(
|
||||
value="question",
|
||||
resumable=True,
|
||||
ns=[AnyStr("graph:")],
|
||||
when="during",
|
||||
),
|
||||
)
|
||||
},
|
||||
]
|
||||
|
||||
assert graph.invoke(Command(resume="answer"), thread1) == [
|
||||
"00answera",
|
||||
"11answera",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
|
||||
def test_imp_stream_order(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str
|
||||
request: pytest.FixtureRequest, checkpointer_name: str, snapshot: SnapshotAssertion
|
||||
) -> None:
|
||||
checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}")
|
||||
|
||||
@@ -1546,6 +1614,8 @@ def test_imp_stream_order(
|
||||
fut_baz = baz(fut_bar.result())
|
||||
return fut_baz.result()
|
||||
|
||||
assert graph.get_graph().draw_mermaid() == snapshot
|
||||
|
||||
thread1 = {"configurable": {"thread_id": "1"}}
|
||||
assert [c for c in graph.stream({"a": "0"}, thread1)] == [
|
||||
{
|
||||
@@ -4951,7 +5021,7 @@ def test_interrupt_loop(request: pytest.FixtureRequest, checkpointer_name: str):
|
||||
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
|
||||
def test_interrupt_functional(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str
|
||||
request: pytest.FixtureRequest, checkpointer_name: str, snapshot: SnapshotAssertion
|
||||
) -> None:
|
||||
checkpointer: BaseCheckpointSaver = request.getfixturevalue(
|
||||
f"checkpointer_{checkpointer_name}"
|
||||
@@ -4973,6 +5043,8 @@ def test_interrupt_functional(
|
||||
fut_bar = bar(bar_input)
|
||||
return fut_bar.result()
|
||||
|
||||
assert graph.get_graph().draw_mermaid() == snapshot
|
||||
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
# First run, interrupted at bar
|
||||
graph.invoke({"a": ""}, config)
|
||||
@@ -4983,7 +5055,7 @@ def test_interrupt_functional(
|
||||
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
|
||||
def test_interrupt_task_functional(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str
|
||||
request: pytest.FixtureRequest, checkpointer_name: str, snapshot: SnapshotAssertion
|
||||
) -> None:
|
||||
checkpointer: BaseCheckpointSaver = request.getfixturevalue(
|
||||
f"checkpointer_{checkpointer_name}"
|
||||
@@ -5004,6 +5076,8 @@ def test_interrupt_task_functional(
|
||||
fut_bar = bar(fut_foo.result())
|
||||
return fut_bar.result()
|
||||
|
||||
assert graph.get_graph().draw_mermaid() == snapshot
|
||||
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
# First run, interrupted at bar
|
||||
graph.invoke({"a": ""}, config)
|
||||
@@ -5432,7 +5506,9 @@ def test_multiple_updates() -> None:
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
|
||||
def test_falsy_return_from_task(request: pytest.FixtureRequest, checkpointer_name: str):
|
||||
def test_falsy_return_from_task(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str, snapshot: SnapshotAssertion
|
||||
):
|
||||
"""Test with a falsy return from a task."""
|
||||
checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}")
|
||||
|
||||
@@ -5446,6 +5522,8 @@ def test_falsy_return_from_task(request: pytest.FixtureRequest, checkpointer_nam
|
||||
falsy_task().result()
|
||||
interrupt("test")
|
||||
|
||||
assert graph.get_graph().draw_mermaid() == snapshot
|
||||
|
||||
configurable = {"configurable": {"thread_id": str(uuid.uuid4())}}
|
||||
graph.invoke({"a": 5}, configurable)
|
||||
graph.invoke(Command(resume="123"), configurable)
|
||||
@@ -5453,7 +5531,7 @@ def test_falsy_return_from_task(request: pytest.FixtureRequest, checkpointer_nam
|
||||
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
|
||||
def test_multiple_interrupts_imperative(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str
|
||||
request: pytest.FixtureRequest, checkpointer_name: str, snapshot: SnapshotAssertion
|
||||
):
|
||||
"""Test multiple interrupts with an imperative API."""
|
||||
checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}")
|
||||
@@ -5478,6 +5556,8 @@ def test_multiple_interrupts_imperative(
|
||||
|
||||
return {"values": values}
|
||||
|
||||
assert graph.get_graph().draw_mermaid() == snapshot
|
||||
|
||||
configurable = {"configurable": {"thread_id": str(uuid.uuid4())}}
|
||||
graph.invoke({}, configurable)
|
||||
graph.invoke(Command(resume="a"), configurable)
|
||||
|
||||
@@ -1132,7 +1132,8 @@ async def test_node_not_cancelled_on_other_node_interrupted(
|
||||
assert awhiles == 1
|
||||
|
||||
|
||||
async def test_step_timeout_on_stream_hang() -> None:
|
||||
@pytest.mark.parametrize("stream_hang_s", [0.3, 0.6])
|
||||
async def test_step_timeout_on_stream_hang(stream_hang_s: float) -> None:
|
||||
inner_task_cancelled = False
|
||||
|
||||
async def awhile(input: Any) -> None:
|
||||
@@ -2534,6 +2535,7 @@ async def test_imp_task_cancel(checkpointer_name: str) -> None:
|
||||
assert mapper_cancels == 2
|
||||
|
||||
|
||||
@pytest.mark.skip("TODO: re-enable")
|
||||
@NEEDS_CONTEXTVARS
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC)
|
||||
async def test_imp_sync_from_async(checkpointer_name: str) -> None:
|
||||
|
||||
Reference in New Issue
Block a user