langgraph 0.0.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,51 @@
1
+ # LangGraph License
2
+
3
+ By using the software, you agree to all of the terms and conditions below.
4
+
5
+ ## Copyright License
6
+
7
+ The licensor grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject to the limitations and conditions below.
8
+
9
+ ## Limitations
10
+
11
+ You may not provide the software to third parties as a hosted or managed service, where the service provides users with access to any substantial set of the features or functionality of the software.
12
+
13
+ You may not move, change, disable, or circumvent the license key functionality in the software, and you may not remove or obscure any functionality in the software that is protected by the license key.
14
+
15
+ You may not alter, remove, or obscure any licensing, copyright, or other notices of the licensor in the software. Any use of the licensor’s trademarks is subject to applicable law.
16
+
17
+ ## Patents
18
+
19
+ The licensor grants you a license, under any patent claims the licensor can license, or becomes able to license, to make, have made, use, sell, offer for sale, import and have imported the software, in each case subject to the limitations and conditions in this license. This license does not cover any patent claims that you cause to be infringed by modifications or additions to the software. If you or your company make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
20
+
21
+ ## Notices
22
+
23
+ You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms.
24
+
25
+ If you modify the software, you must include in any modified copies of the software prominent notices stating that you have modified the software.
26
+
27
+ ## No Other Rights
28
+
29
+ These terms do not imply any licenses other than those expressly granted in these terms.
30
+
31
+ ## Termination
32
+
33
+ If you use the software in violation of these terms, such use is not licensed, and your licenses will automatically terminate. If the licensor provides you with a notice of your violation, and you cease all violation of this license no later than 30 days after you receive that notice, your licenses will be reinstated retroactively. However, if you violate these terms after such reinstatement, any additional violation of these terms will cause your licenses to terminate automatically and permanently.
34
+
35
+ ## No Liability
36
+
37
+ As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.
38
+
39
+ ## Definitions
40
+
41
+ The licensor is the entity offering these terms, and the software is the software the licensor makes available under these terms, including any portion of it.
42
+
43
+ you refers to the individual or entity agreeing to these terms.
44
+
45
+ your company is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. control means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
46
+
47
+ your licenses are all the licenses granted to you for the software under these terms.
48
+
49
+ use means anything you do with the software requiring one of your licenses.
50
+
51
+ trademark means trademarks, service marks, and similar rights.
@@ -0,0 +1,119 @@
1
+ Metadata-Version: 2.1
2
+ Name: langgraph
3
+ Version: 0.0.8
4
+ Summary: langgraph
5
+ Home-page: https://www.github.com/langchain-ai/langgraph
6
+ License: LangGraph License
7
+ Requires-Python: >=3.8.1,<4.0
8
+ Classifier: License :: Other/Proprietary License
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.9
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Requires-Dist: langchain-core (>=0.1.8,<0.2.0)
14
+ Project-URL: Repository, https://www.github.com/langchain-ai/langgraph
15
+ Description-Content-Type: text/markdown
16
+
17
+ # `langgraph`
18
+
19
+ ## Get started
20
+
21
+ `pip install langgraph`
22
+
23
+ ## Overview
24
+
25
+ LangGraph is an alpha-stage library for building stateful, multi-actor applications with LLMs. It extends the [LangChain Expression Language](https://python.langchain.com/docs/expression_language/) with the ability to coordinate multiple chains (or actors) across multiple steps of computation. It is inspired by [Pregel](https://research.google/pubs/pub37252/) and [Apache Beam](https://beam.apache.org/).
26
+
27
+ Some of the use cases are:
28
+
29
+ - Recursive/iterative LLM chains
30
+ - LLM chains with persistent state/memory
31
+ - LLM agents
32
+ - Multi-agent simulations
33
+ - ...and more!
34
+
35
+ ## How it works
36
+
37
+ ### Channels
38
+
39
+ Channels are used to communicate between chains. Each channel has a value type, an update type, and an update function – which takes a sequence of updates and modifies the stored value. Channels can be used to send data from one chain to another, or to send data from a chain to itself in a future step. LangGraph provides a number of built-in channels:
40
+
41
+ #### Basic channels: LastValue and Topic
42
+
43
+ - `LastValue`: The default channel, stores the last value sent to the channel, useful for input and output values, or for sending data from one step to the next
44
+ - `Topic`: A configurable PubSub Topic, useful for sending multiple values between chains, or for accumulating output. Can be configured to deduplicate values, and/or to accummulate values over the course of multiple steps.
45
+
46
+ #### Advanced channels: Context and BinaryOperatorAggregate
47
+
48
+ - `Context`: exposes the value of a context manager, managing its lifecycle. Useful for accessing external resources that require setup and/or teardown. eg. `client = Context(httpx.Client)`
49
+ - `BinaryOperatorAggregate`: stores a persistent value, updated by applying a binary operator to the current value and each update sent to the channel, useful for computing aggregates over multiple steps. eg. `total = BinaryOperatorAggregate(int, operator.add)`
50
+
51
+ ### Chains
52
+
53
+ Chains are LCEL Runnables which subscribe to one or more channels, and write to one or more channels. Any valid LCEL expression can be used as a chain. Chains can be combined into a Pregel application, which coordinates the execution of the chains across multiple steps.
54
+
55
+ ### Pregel
56
+
57
+ Pregel combines multiple chains (or actors) into a single application. It coordinates the execution of the chains across multiple steps, following the Pregel/Bulk Synchronous Parallel model. Each step consists of three phases:
58
+
59
+ - **Plan**: Determine which chains to execute in this step, ie. the chains that subscribe to channels updated in the previous step (or, in the first step, chains that subscribe to input channels)
60
+ - **Execution**: Execute those chains in parallel, until all complete, or one fails, or a timeout is reached. Any channel updates are invisible to other chains until the next step.
61
+ - **Update**: Update the channels with the values written by the chains in this step.
62
+
63
+ Repeat until no chains are planned for execution, or a maximum number of steps is reached.
64
+
65
+ ## Example
66
+
67
+ ```python
68
+ from langgraph import Channel, Pregel
69
+
70
+ grow_value = (
71
+ Channel.subscribe_to("value")
72
+ | (lambda x: x + x)
73
+ | Channel.write_to(value=lambda x: x if len(x) < 10 else None)
74
+ )
75
+
76
+ app = Pregel(
77
+ chains={"grow_value": grow_value},
78
+ input="value",
79
+ output="value",
80
+ )
81
+
82
+ assert app.invoke("a") == "aaaaaaaa"
83
+ ```
84
+
85
+ Check `examples` for more examples.
86
+
87
+ ## Near-term Roadmap
88
+
89
+ - [x] Iterate on API
90
+ - [x] do we want api to receive output from multiple channels in invoke()
91
+ - [x] do we want api to send input to multiple channels in invoke()
92
+ - [x] Finish updating tests to new API
93
+ - [x] Implement input_schema and output_schema in Pregel
94
+ - [ ] More tests
95
+ - [x] Test different input and output types (str, str sequence)
96
+ - [x] Add tests for Stream, UniqueInbox
97
+ - [ ] Add tests for subscribe_to_each().join()
98
+ - [x] Add optional debug logging
99
+ - [ ] Add an optional Diff value for Channels that implements `__add__`, returned by update(), yielded by Pregel for output channels. Add replacing_keys set to AddableDict. use an addabledict for yielding values. channels that dont implement it get marked with replacing_keys
100
+ - [x] Implement checkpointing
101
+ - [x] Save checkpoints at end of each step/run
102
+ - [x] Load checkpoint at start of invocation
103
+ - [x] API to specify storage backend and save key
104
+ - [x] Tests
105
+ - [ ] Add more examples
106
+ - [ ] multi agent simulation
107
+ - [ ] human in the loop
108
+ - [ ] combine documents
109
+ - [ ] agent executor (add current v total iterations info to read/write steps to enable doing a final update at the end)
110
+ - [ ] run over dataset
111
+ - [ ] Fault tolerance
112
+ - [ ] Expose a unique id to each step, hash of (app, chain, checkpoint) (include input updates for first step)
113
+ - [ ] Retry individual processes in a step
114
+ - [ ] Retry entire step?
115
+ - [ ] Pregel.stream_log to contain additional keys specific to Pregel
116
+ - [ ] tasks: inputs of each chain in each step, keyed by {name}:{step}
117
+ - [ ] task_results: same as above but outputs
118
+ - [ ] channels: channel values at end of each step, keyed by {name}:{step}
119
+
@@ -0,0 +1,102 @@
1
+ # `langgraph`
2
+
3
+ ## Get started
4
+
5
+ `pip install langgraph`
6
+
7
+ ## Overview
8
+
9
+ LangGraph is an alpha-stage library for building stateful, multi-actor applications with LLMs. It extends the [LangChain Expression Language](https://python.langchain.com/docs/expression_language/) with the ability to coordinate multiple chains (or actors) across multiple steps of computation. It is inspired by [Pregel](https://research.google/pubs/pub37252/) and [Apache Beam](https://beam.apache.org/).
10
+
11
+ Some of the use cases are:
12
+
13
+ - Recursive/iterative LLM chains
14
+ - LLM chains with persistent state/memory
15
+ - LLM agents
16
+ - Multi-agent simulations
17
+ - ...and more!
18
+
19
+ ## How it works
20
+
21
+ ### Channels
22
+
23
+ Channels are used to communicate between chains. Each channel has a value type, an update type, and an update function – which takes a sequence of updates and modifies the stored value. Channels can be used to send data from one chain to another, or to send data from a chain to itself in a future step. LangGraph provides a number of built-in channels:
24
+
25
+ #### Basic channels: LastValue and Topic
26
+
27
+ - `LastValue`: The default channel, stores the last value sent to the channel, useful for input and output values, or for sending data from one step to the next
28
+ - `Topic`: A configurable PubSub Topic, useful for sending multiple values between chains, or for accumulating output. Can be configured to deduplicate values, and/or to accummulate values over the course of multiple steps.
29
+
30
+ #### Advanced channels: Context and BinaryOperatorAggregate
31
+
32
+ - `Context`: exposes the value of a context manager, managing its lifecycle. Useful for accessing external resources that require setup and/or teardown. eg. `client = Context(httpx.Client)`
33
+ - `BinaryOperatorAggregate`: stores a persistent value, updated by applying a binary operator to the current value and each update sent to the channel, useful for computing aggregates over multiple steps. eg. `total = BinaryOperatorAggregate(int, operator.add)`
34
+
35
+ ### Chains
36
+
37
+ Chains are LCEL Runnables which subscribe to one or more channels, and write to one or more channels. Any valid LCEL expression can be used as a chain. Chains can be combined into a Pregel application, which coordinates the execution of the chains across multiple steps.
38
+
39
+ ### Pregel
40
+
41
+ Pregel combines multiple chains (or actors) into a single application. It coordinates the execution of the chains across multiple steps, following the Pregel/Bulk Synchronous Parallel model. Each step consists of three phases:
42
+
43
+ - **Plan**: Determine which chains to execute in this step, ie. the chains that subscribe to channels updated in the previous step (or, in the first step, chains that subscribe to input channels)
44
+ - **Execution**: Execute those chains in parallel, until all complete, or one fails, or a timeout is reached. Any channel updates are invisible to other chains until the next step.
45
+ - **Update**: Update the channels with the values written by the chains in this step.
46
+
47
+ Repeat until no chains are planned for execution, or a maximum number of steps is reached.
48
+
49
+ ## Example
50
+
51
+ ```python
52
+ from langgraph import Channel, Pregel
53
+
54
+ grow_value = (
55
+ Channel.subscribe_to("value")
56
+ | (lambda x: x + x)
57
+ | Channel.write_to(value=lambda x: x if len(x) < 10 else None)
58
+ )
59
+
60
+ app = Pregel(
61
+ chains={"grow_value": grow_value},
62
+ input="value",
63
+ output="value",
64
+ )
65
+
66
+ assert app.invoke("a") == "aaaaaaaa"
67
+ ```
68
+
69
+ Check `examples` for more examples.
70
+
71
+ ## Near-term Roadmap
72
+
73
+ - [x] Iterate on API
74
+ - [x] do we want api to receive output from multiple channels in invoke()
75
+ - [x] do we want api to send input to multiple channels in invoke()
76
+ - [x] Finish updating tests to new API
77
+ - [x] Implement input_schema and output_schema in Pregel
78
+ - [ ] More tests
79
+ - [x] Test different input and output types (str, str sequence)
80
+ - [x] Add tests for Stream, UniqueInbox
81
+ - [ ] Add tests for subscribe_to_each().join()
82
+ - [x] Add optional debug logging
83
+ - [ ] Add an optional Diff value for Channels that implements `__add__`, returned by update(), yielded by Pregel for output channels. Add replacing_keys set to AddableDict. use an addabledict for yielding values. channels that dont implement it get marked with replacing_keys
84
+ - [x] Implement checkpointing
85
+ - [x] Save checkpoints at end of each step/run
86
+ - [x] Load checkpoint at start of invocation
87
+ - [x] API to specify storage backend and save key
88
+ - [x] Tests
89
+ - [ ] Add more examples
90
+ - [ ] multi agent simulation
91
+ - [ ] human in the loop
92
+ - [ ] combine documents
93
+ - [ ] agent executor (add current v total iterations info to read/write steps to enable doing a final update at the end)
94
+ - [ ] run over dataset
95
+ - [ ] Fault tolerance
96
+ - [ ] Expose a unique id to each step, hash of (app, chain, checkpoint) (include input updates for first step)
97
+ - [ ] Retry individual processes in a step
98
+ - [ ] Retry entire step?
99
+ - [ ] Pregel.stream_log to contain additional keys specific to Pregel
100
+ - [ ] tasks: inputs of each chain in each step, keyed by {name}:{step}
101
+ - [ ] task_results: same as above but outputs
102
+ - [ ] channels: channel values at end of each step, keyed by {name}:{step}
File without changes
@@ -0,0 +1,11 @@
1
+ from langgraph.channels.binop import BinaryOperatorAggregate
2
+ from langgraph.channels.context import Context
3
+ from langgraph.channels.last_value import LastValue
4
+ from langgraph.channels.topic import Topic
5
+
6
+ __all__ = [
7
+ "LastValue",
8
+ "Topic",
9
+ "Context",
10
+ "BinaryOperatorAggregate",
11
+ ]
@@ -0,0 +1,131 @@
1
+ from abc import ABC, abstractmethod
2
+ from contextlib import asynccontextmanager, contextmanager
3
+ from datetime import datetime, timezone
4
+ from typing import (
5
+ Any,
6
+ AsyncGenerator,
7
+ Generator,
8
+ Generic,
9
+ Mapping,
10
+ Optional,
11
+ Sequence,
12
+ TypeVar,
13
+ )
14
+
15
+ from typing_extensions import Self
16
+
17
+ from langgraph.checkpoint.base import Checkpoint
18
+
19
+ Value = TypeVar("Value")
20
+ Update = TypeVar("Update")
21
+ C = TypeVar("C")
22
+
23
+
24
+ class EmptyChannelError(Exception):
25
+ """Raised when attempting to get the value of a channel that hasn't been updated
26
+ for the first time yet."""
27
+
28
+ pass
29
+
30
+
31
+ class InvalidUpdateError(Exception):
32
+ """Raised when attempting to update a channel with an invalid sequence of updates."""
33
+
34
+ pass
35
+
36
+
37
+ class BaseChannel(Generic[Value, Update, C], ABC):
38
+ @property
39
+ @abstractmethod
40
+ def ValueType(self) -> Any:
41
+ """The type of the value stored in the channel."""
42
+
43
+ @property
44
+ @abstractmethod
45
+ def UpdateType(self) -> Any:
46
+ """The type of the update received by the channel."""
47
+
48
+ @contextmanager
49
+ @abstractmethod
50
+ def empty(self, checkpoint: Optional[C] = None) -> Generator[Self, None, None]:
51
+ """Return a new identical channel, optionally initialized from a checkpoint."""
52
+
53
+ @asynccontextmanager
54
+ async def aempty(
55
+ self, checkpoint: Optional[C] = None
56
+ ) -> AsyncGenerator[Self, None]:
57
+ """Return a new identical channel, optionally initialized from a checkpoint."""
58
+ with self.empty(checkpoint) as value:
59
+ yield value
60
+
61
+ @abstractmethod
62
+ def update(self, values: Sequence[Update]) -> None:
63
+ """Update the channel's value with the given sequence of updates.
64
+ The order of the updates in the sequence is arbitrary.
65
+
66
+ Raises InvalidUpdateError if the sequence of updates is invalid."""
67
+
68
+ @abstractmethod
69
+ def get(self) -> Value:
70
+ """Return the current value of the channel.
71
+
72
+ Raises EmptyChannelError if the channel is empty (never updated yet)."""
73
+
74
+ @abstractmethod
75
+ def checkpoint(self) -> C | None:
76
+ """Return a string representation of the channel's current state.
77
+
78
+ Raises EmptyChannelError if the channel is empty (never updated yet),
79
+ or doesn't supportcheckpoints."""
80
+
81
+
82
+ @contextmanager
83
+ def ChannelsManager(
84
+ channels: Mapping[str, BaseChannel],
85
+ checkpoint: Checkpoint,
86
+ ) -> Generator[Mapping[str, BaseChannel], None, None]:
87
+ """Manage channels for the lifetime of a Pregel invocation (multiple steps)."""
88
+ # TODO use https://docs.python.org/3/library/contextlib.html#contextlib.ExitStack
89
+ empty = {
90
+ k: v.empty(checkpoint["channel_values"].get(k)) for k, v in channels.items()
91
+ }
92
+ try:
93
+ yield {k: v.__enter__() for k, v in empty.items()}
94
+ finally:
95
+ for v in empty.values():
96
+ v.__exit__(None, None, None)
97
+
98
+
99
+ @asynccontextmanager
100
+ async def AsyncChannelsManager(
101
+ channels: Mapping[str, BaseChannel],
102
+ checkpoint: Checkpoint,
103
+ ) -> AsyncGenerator[Mapping[str, BaseChannel], None]:
104
+ """Manage channels for the lifetime of a Pregel invocation (multiple steps)."""
105
+ empty = {
106
+ k: v.aempty(checkpoint["channel_values"].get(k)) for k, v in channels.items()
107
+ }
108
+ try:
109
+ yield {k: await v.__aenter__() for k, v in empty.items()}
110
+ finally:
111
+ for v in empty.values():
112
+ await v.__aexit__(None, None, None)
113
+
114
+
115
+ def create_checkpoint(
116
+ checkpoint: Checkpoint, channels: Mapping[str, BaseChannel]
117
+ ) -> Checkpoint:
118
+ """Create a checkpoint for the given channels."""
119
+ checkpoint = Checkpoint(
120
+ v=1,
121
+ ts=datetime.now(timezone.utc).isoformat(),
122
+ channel_values=checkpoint["channel_values"],
123
+ channel_versions=checkpoint["channel_versions"],
124
+ versions_seen=checkpoint["versions_seen"],
125
+ )
126
+ for k, v in channels.items():
127
+ try:
128
+ checkpoint["channel_values"][k] = v.checkpoint()
129
+ except EmptyChannelError:
130
+ pass
131
+ return checkpoint
@@ -0,0 +1,70 @@
1
+ from contextlib import contextmanager
2
+ from typing import Callable, Generator, Generic, Optional, Sequence, Type
3
+
4
+ from typing_extensions import Self
5
+
6
+ from langgraph.channels.base import BaseChannel, EmptyChannelError, Value
7
+
8
+
9
+ class BinaryOperatorAggregate(Generic[Value], BaseChannel[Value, Value, Value]):
10
+ """Stores the result of applying a binary operator to the current value and each new value.
11
+
12
+ ```python
13
+ import operator
14
+
15
+ total = Channels.BinaryOperatorAggregate(int, operator.add)
16
+ ```
17
+ """
18
+
19
+ def __init__(self, typ: Type[Value], operator: Callable[[Value, Value], Value]):
20
+ self.typ = typ
21
+ self.operator = operator
22
+ try:
23
+ self.value = typ()
24
+ except Exception:
25
+ pass
26
+
27
+ @property
28
+ def ValueType(self) -> Type[Value]:
29
+ """The type of the value stored in the channel."""
30
+ return self.typ
31
+
32
+ @property
33
+ def UpdateType(self) -> Type[Value]:
34
+ """The type of the update received by the channel."""
35
+ return self.typ
36
+
37
+ @contextmanager
38
+ def empty(self, checkpoint: Optional[Value] = None) -> Generator[Self, None, None]:
39
+ empty = self.__class__(self.typ, self.operator)
40
+ if checkpoint is not None:
41
+ empty.value = checkpoint
42
+ try:
43
+ yield empty
44
+ finally:
45
+ try:
46
+ del empty.value
47
+ except AttributeError:
48
+ pass
49
+
50
+ def update(self, values: Sequence[Value]) -> None:
51
+ if not values:
52
+ return
53
+ if not hasattr(self, "value"):
54
+ self.value = values[0]
55
+ values = values[1:]
56
+
57
+ for value in values:
58
+ self.value = self.operator(self.value, value)
59
+
60
+ def get(self) -> Value:
61
+ try:
62
+ return self.value
63
+ except AttributeError:
64
+ raise EmptyChannelError()
65
+
66
+ def checkpoint(self) -> Value:
67
+ try:
68
+ return self.value
69
+ except AttributeError:
70
+ raise EmptyChannelError()
@@ -0,0 +1,110 @@
1
+ from contextlib import asynccontextmanager, contextmanager
2
+ from typing import (
3
+ Any,
4
+ AsyncContextManager,
5
+ AsyncGenerator,
6
+ Callable,
7
+ ContextManager,
8
+ Generator,
9
+ Generic,
10
+ Optional,
11
+ Sequence,
12
+ Type,
13
+ )
14
+
15
+ from typing_extensions import Self
16
+
17
+ from langgraph.channels.base import (
18
+ BaseChannel,
19
+ EmptyChannelError,
20
+ InvalidUpdateError,
21
+ Value,
22
+ )
23
+
24
+
25
+ class Context(Generic[Value], BaseChannel[Value, None, None]):
26
+ """Exposes the value of a context manager, for the duration of an invocation.
27
+ Context manager is entered before the first step, and exited after the last step.
28
+ Optionally, provide an equivalent async context manager, which will be used
29
+ instead for async invocations.
30
+
31
+ ```python
32
+ import httpx
33
+
34
+ client = Channels.Context(httpx.Client, httpx.AsyncClient)
35
+ ```
36
+ """
37
+
38
+ value: Value
39
+
40
+ def __init__(
41
+ self,
42
+ ctx: Optional[Callable[[], ContextManager[Value]]] = None,
43
+ actx: Optional[Callable[[], AsyncContextManager[Value]]] = None,
44
+ typ: Optional[Type[Value]] = None,
45
+ ) -> None:
46
+ if ctx is None and actx is None:
47
+ raise ValueError("Must provide either sync or async context manager.")
48
+
49
+ self.typ = typ
50
+ self.ctx = ctx
51
+ self.actx = actx
52
+
53
+ @property
54
+ def ValueType(self) -> Any:
55
+ """The type of the value stored in the channel."""
56
+ return (
57
+ self.typ
58
+ or (self.ctx if hasattr(self.ctx, "__enter__") else None)
59
+ or (self.actx if hasattr(self.actx, "__aenter__") else None)
60
+ or None
61
+ )
62
+
63
+ @property
64
+ def UpdateType(self) -> Type[None]:
65
+ """The type of the update received by the channel."""
66
+ raise InvalidUpdateError()
67
+
68
+ @contextmanager
69
+ def empty(self, checkpoint: None = None) -> Generator[Self, None, None]:
70
+ if self.ctx is None:
71
+ raise ValueError("Cannot enter sync context manager.")
72
+
73
+ empty = self.__class__(ctx=self.ctx, actx=self.actx, typ=self.typ)
74
+ # ContextManager doesn't have a checkpoint
75
+ ctx = self.ctx()
76
+ empty.value = ctx.__enter__()
77
+ try:
78
+ yield empty
79
+ finally:
80
+ ctx.__exit__(None, None, None)
81
+
82
+ @asynccontextmanager
83
+ async def aempty(
84
+ self, checkpoint: Optional[str] = None
85
+ ) -> AsyncGenerator[Self, None]:
86
+ if self.actx is not None:
87
+ empty = self.__class__(ctx=self.ctx, actx=self.actx, typ=self.typ)
88
+ # ContextManager doesn't have a checkpoint
89
+ actx = self.actx()
90
+ empty.value = await actx.__aenter__()
91
+ try:
92
+ yield empty
93
+ finally:
94
+ await actx.__aexit__(None, None, None)
95
+ else:
96
+ with self.empty() as empty:
97
+ yield empty
98
+
99
+ def update(self, values: Sequence[None]) -> None:
100
+ if values:
101
+ raise InvalidUpdateError()
102
+
103
+ def get(self) -> Value:
104
+ try:
105
+ return self.value
106
+ except AttributeError:
107
+ raise EmptyChannelError()
108
+
109
+ def checkpoint(self) -> None:
110
+ raise EmptyChannelError()
@@ -0,0 +1,61 @@
1
+ from contextlib import contextmanager
2
+ from typing import Generator, Generic, Optional, Sequence, Type
3
+
4
+ from typing_extensions import Self
5
+
6
+ from langgraph.channels.base import (
7
+ BaseChannel,
8
+ EmptyChannelError,
9
+ InvalidUpdateError,
10
+ Value,
11
+ )
12
+
13
+
14
+ class LastValue(Generic[Value], BaseChannel[Value, Value, Value]):
15
+ """Stores the last value received, can receive at most one value per step."""
16
+
17
+ def __init__(self, typ: Type[Value]) -> None:
18
+ self.typ = typ
19
+
20
+ @property
21
+ def ValueType(self) -> Type[Value]:
22
+ """The type of the value stored in the channel."""
23
+ return self.typ
24
+
25
+ @property
26
+ def UpdateType(self) -> Type[Value]:
27
+ """The type of the update received by the channel."""
28
+ return self.typ
29
+
30
+ @contextmanager
31
+ def empty(self, checkpoint: Optional[Value] = None) -> Generator[Self, None, None]:
32
+ empty = self.__class__(self.typ)
33
+ if checkpoint is not None:
34
+ empty.value = checkpoint
35
+ try:
36
+ yield empty
37
+ finally:
38
+ try:
39
+ del empty.value
40
+ except AttributeError:
41
+ pass
42
+
43
+ def update(self, values: Sequence[Value]) -> None:
44
+ if len(values) == 0:
45
+ return
46
+ if len(values) != 1:
47
+ raise InvalidUpdateError()
48
+
49
+ self.value = values[-1]
50
+
51
+ def get(self) -> Value:
52
+ try:
53
+ return self.value
54
+ except AttributeError:
55
+ raise EmptyChannelError()
56
+
57
+ def checkpoint(self) -> Value:
58
+ try:
59
+ return self.value
60
+ except AttributeError:
61
+ raise EmptyChannelError()