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.
- langgraph-0.0.8/LICENSE +51 -0
- langgraph-0.0.8/PKG-INFO +119 -0
- langgraph-0.0.8/README.md +102 -0
- langgraph-0.0.8/langgraph/__init__.py +0 -0
- langgraph-0.0.8/langgraph/channels/__init__.py +11 -0
- langgraph-0.0.8/langgraph/channels/base.py +131 -0
- langgraph-0.0.8/langgraph/channels/binop.py +70 -0
- langgraph-0.0.8/langgraph/channels/context.py +110 -0
- langgraph-0.0.8/langgraph/channels/last_value.py +61 -0
- langgraph-0.0.8/langgraph/channels/topic.py +79 -0
- langgraph-0.0.8/langgraph/checkpoint/__init__.py +8 -0
- langgraph-0.0.8/langgraph/checkpoint/base.py +65 -0
- langgraph-0.0.8/langgraph/checkpoint/memory.py +28 -0
- langgraph-0.0.8/langgraph/constants.py +2 -0
- langgraph-0.0.8/langgraph/graph/__init__.py +129 -0
- langgraph-0.0.8/langgraph/pregel/__init__.py +719 -0
- langgraph-0.0.8/langgraph/pregel/debug.py +34 -0
- langgraph-0.0.8/langgraph/pregel/io.py +37 -0
- langgraph-0.0.8/langgraph/pregel/log.py +3 -0
- langgraph-0.0.8/langgraph/pregel/read.py +201 -0
- langgraph-0.0.8/langgraph/pregel/reserved.py +8 -0
- langgraph-0.0.8/langgraph/pregel/validate.py +54 -0
- langgraph-0.0.8/langgraph/pregel/write.py +65 -0
- langgraph-0.0.8/langgraph/utils.py +8 -0
- langgraph-0.0.8/pyproject.toml +72 -0
langgraph-0.0.8/LICENSE
ADDED
|
@@ -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.
|
langgraph-0.0.8/PKG-INFO
ADDED
|
@@ -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()
|