langgraph-checkpointer-couchbase 1.0.9__tar.gz → 2.0.0__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_checkpointer_couchbase-2.0.0/.github/workflows/ci.yaml +95 -0
- {langgraph_checkpointer_couchbase-1.0.9 → langgraph_checkpointer_couchbase-2.0.0}/.github/workflows/release.yaml +3 -3
- langgraph_checkpointer_couchbase-2.0.0/AGENTS.md +30 -0
- {langgraph_checkpointer_couchbase-1.0.9 → langgraph_checkpointer_couchbase-2.0.0}/PKG-INFO +42 -14
- {langgraph_checkpointer_couchbase-1.0.9 → langgraph_checkpointer_couchbase-2.0.0}/README.md +37 -8
- langgraph_checkpointer_couchbase-2.0.0/langgraph_checkpointer_couchbase/__about__.py +1 -0
- langgraph_checkpointer_couchbase-2.0.0/langgraph_checkpointer_couchbase/__init__.py +16 -0
- {langgraph_checkpointer_couchbase-1.0.9 → langgraph_checkpointer_couchbase-2.0.0}/langgraph_checkpointer_couchbase/async_cb_saver.py +4 -3
- {langgraph_checkpointer_couchbase-1.0.9 → langgraph_checkpointer_couchbase-2.0.0}/langgraph_checkpointer_couchbase/couchbase_saver.py +4 -3
- {langgraph_checkpointer_couchbase-1.0.9 → langgraph_checkpointer_couchbase-2.0.0}/pyproject.toml +3 -4
- langgraph_checkpointer_couchbase-2.0.0/tests/test_checkpointer.py +147 -0
- langgraph_checkpointer_couchbase-2.0.0/tests/test_python_version_guard.py +18 -0
- langgraph_checkpointer_couchbase-1.0.9/langgraph_checkpointer_couchbase/__about__.py +0 -1
- langgraph_checkpointer_couchbase-1.0.9/langgraph_checkpointer_couchbase/__init__.py +0 -7
- {langgraph_checkpointer_couchbase-1.0.9 → langgraph_checkpointer_couchbase-2.0.0}/.env.example +0 -0
- {langgraph_checkpointer_couchbase-1.0.9 → langgraph_checkpointer_couchbase-2.0.0}/.gitignore +0 -0
- {langgraph_checkpointer_couchbase-1.0.9 → langgraph_checkpointer_couchbase-2.0.0}/LICENSE +0 -0
- {langgraph_checkpointer_couchbase-1.0.9 → langgraph_checkpointer_couchbase-2.0.0}/langgraph_checkpointer_couchbase/telemetry.py +0 -0
- {langgraph_checkpointer_couchbase-1.0.9 → langgraph_checkpointer_couchbase-2.0.0}/langgraph_checkpointer_couchbase/utils.py +0 -0
- {langgraph_checkpointer_couchbase-1.0.9 → langgraph_checkpointer_couchbase-2.0.0}/tests/__init__.py +0 -0
- {langgraph_checkpointer_couchbase-1.0.9 → langgraph_checkpointer_couchbase-2.0.0}/tests/agent_e2e_test.py +0 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
on:
|
|
3
|
+
pull_request:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
test:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
strategy:
|
|
16
|
+
fail-fast: false
|
|
17
|
+
matrix:
|
|
18
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
19
|
+
services:
|
|
20
|
+
couchbase:
|
|
21
|
+
image: couchbase:enterprise-7.6.4
|
|
22
|
+
ports:
|
|
23
|
+
- 8091-8096:8091-8096
|
|
24
|
+
- 11210:11210
|
|
25
|
+
env:
|
|
26
|
+
CB_CLUSTER: couchbase://localhost
|
|
27
|
+
CB_USERNAME: Administrator
|
|
28
|
+
CB_PASSWORD: password
|
|
29
|
+
CB_BUCKET: test
|
|
30
|
+
CB_SCOPE: langgraph
|
|
31
|
+
steps:
|
|
32
|
+
- name: Checkout Repository
|
|
33
|
+
uses: actions/checkout@v7
|
|
34
|
+
|
|
35
|
+
- name: Set up Python
|
|
36
|
+
uses: actions/setup-python@v7
|
|
37
|
+
with:
|
|
38
|
+
python-version: ${{ matrix.python-version }}
|
|
39
|
+
|
|
40
|
+
- name: Install Dependencies
|
|
41
|
+
run: pip install -e . pytest pytest-asyncio hatch
|
|
42
|
+
|
|
43
|
+
- name: Initialize Couchbase
|
|
44
|
+
env:
|
|
45
|
+
CB_CONTAINER: ${{ job.services.couchbase.id }}
|
|
46
|
+
run: |
|
|
47
|
+
for i in $(seq 1 60); do
|
|
48
|
+
curl -sf http://localhost:8091/ui/index.html > /dev/null && break
|
|
49
|
+
sleep 2
|
|
50
|
+
done
|
|
51
|
+
cli() { docker exec "$CB_CONTAINER" couchbase-cli "$@" -c localhost; }
|
|
52
|
+
cli cluster-init --cluster-username "$CB_USERNAME" --cluster-password "$CB_PASSWORD" \
|
|
53
|
+
--services data,index,query --cluster-ramsize 1024 --cluster-index-ramsize 256
|
|
54
|
+
cli bucket-create -u "$CB_USERNAME" -p "$CB_PASSWORD" --bucket "$CB_BUCKET" \
|
|
55
|
+
--bucket-type couchbase --bucket-ramsize 256 --wait
|
|
56
|
+
cli collection-manage -u "$CB_USERNAME" -p "$CB_PASSWORD" --bucket "$CB_BUCKET" --create-scope "$CB_SCOPE"
|
|
57
|
+
|
|
58
|
+
# A freshly initialized node can reject queries for a short while.
|
|
59
|
+
- name: Wait for Couchbase Query Service
|
|
60
|
+
shell: python
|
|
61
|
+
run: |
|
|
62
|
+
import os, time
|
|
63
|
+
from langgraph_checkpointer_couchbase import CouchbaseSaver
|
|
64
|
+
|
|
65
|
+
deadline = time.time() + 180
|
|
66
|
+
while True:
|
|
67
|
+
try:
|
|
68
|
+
with CouchbaseSaver.from_conn_info(
|
|
69
|
+
cb_conn_str=os.environ["CB_CLUSTER"],
|
|
70
|
+
cb_username=os.environ["CB_USERNAME"],
|
|
71
|
+
cb_password=os.environ["CB_PASSWORD"],
|
|
72
|
+
bucket_name=os.environ["CB_BUCKET"],
|
|
73
|
+
scope_name=os.environ["CB_SCOPE"],
|
|
74
|
+
) as saver:
|
|
75
|
+
saver.get_tuple({"configurable": {"thread_id": "ci-readiness"}})
|
|
76
|
+
print("Couchbase query service is ready")
|
|
77
|
+
break
|
|
78
|
+
except Exception as e:
|
|
79
|
+
if time.time() > deadline:
|
|
80
|
+
raise
|
|
81
|
+
print(f"Waiting for Couchbase: {type(e).__name__}")
|
|
82
|
+
time.sleep(3)
|
|
83
|
+
|
|
84
|
+
- name: Create Checkpoint Indexes
|
|
85
|
+
run: |
|
|
86
|
+
for c in checkpoints checkpoint_writes; do
|
|
87
|
+
curl -sf -u "$CB_USERNAME:$CB_PASSWORD" http://localhost:8093/query/service \
|
|
88
|
+
--data-urlencode "statement=CREATE INDEX idx_${c}_thread IF NOT EXISTS ON \`$CB_BUCKET\`.\`$CB_SCOPE\`.\`$c\`(thread_id, checkpoint_ns, checkpoint_id)"
|
|
89
|
+
done
|
|
90
|
+
|
|
91
|
+
- name: Run Tests
|
|
92
|
+
run: pytest tests/test_checkpointer.py tests/test_python_version_guard.py -v
|
|
93
|
+
|
|
94
|
+
- name: Hatch build
|
|
95
|
+
run: hatch build
|
|
@@ -13,10 +13,10 @@ jobs:
|
|
|
13
13
|
contents: write
|
|
14
14
|
steps:
|
|
15
15
|
- name: Checkout Repository
|
|
16
|
-
uses: actions/checkout@
|
|
16
|
+
uses: actions/checkout@v7
|
|
17
17
|
|
|
18
18
|
- name: Set up Python
|
|
19
|
-
uses: actions/setup-python@
|
|
19
|
+
uses: actions/setup-python@v7
|
|
20
20
|
with:
|
|
21
21
|
python-version: "3.11"
|
|
22
22
|
|
|
@@ -34,7 +34,7 @@ jobs:
|
|
|
34
34
|
|
|
35
35
|
- name: Create GitHub Release
|
|
36
36
|
id: create_release
|
|
37
|
-
uses: softprops/action-gh-release@
|
|
37
|
+
uses: softprops/action-gh-release@v3
|
|
38
38
|
with:
|
|
39
39
|
tag_name: ${{ github.ref_name }}
|
|
40
40
|
draft: false
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Agent Notes
|
|
2
|
+
|
|
3
|
+
- Packaging uses hatch/pip (`pyproject.toml`, `hatchling`). Do not add a lockfile or switch package managers.
|
|
4
|
+
- PR validation is `.github/workflows/ci.yaml`: it starts a Couchbase container, creates the bucket/scope, creates the recommended indexes, runs `pytest tests/test_checkpointer.py`, then `hatch build`.
|
|
5
|
+
- Local validation needs a Couchbase cluster with an existing bucket and scope (see "Running the Tests" in `README.md`). The saver creates its own collections but not indexes; without the README indexes, sequential-scan reads can miss just-written checkpoints and tests flake.
|
|
6
|
+
- `tests/agent_e2e_test.py` is the README agent flow and needs `OPENAI_API_KEY`; it is not run in CI.
|
|
7
|
+
|
|
8
|
+
## Release Process
|
|
9
|
+
|
|
10
|
+
Maintainers release this package in two steps:
|
|
11
|
+
|
|
12
|
+
1. A PR bumps `__version__` in `langgraph_checkpointer_couchbase/__about__.py`.
|
|
13
|
+
2. After that PR merges, a maintainer runs the **Publish Package to PyPI** workflow (`.github/workflows/release.yaml`) from the Actions tab on `main`. It builds with hatch, publishes to PyPI, and creates a GitHub release. Pushing a `v*` tag also triggers it.
|
|
14
|
+
|
|
15
|
+
Agents prepare step 1 only. Do not create tags or releases, and do not dispatch the release workflow.
|
|
16
|
+
|
|
17
|
+
### Version bumps in dependency upgrade PRs
|
|
18
|
+
|
|
19
|
+
Any PR that changes `dependencies` or `requires-python` in `pyproject.toml`, or changes code to follow a dependency upgrade, must bump `__version__` in the same PR. The maintainer can then approve, merge, and run the release workflow without a separate bump PR.
|
|
20
|
+
|
|
21
|
+
- Check the current release first (`https://pypi.org/pypi/langgraph-checkpointer-couchbase/json`) and bump from that version. Never reuse a published version.
|
|
22
|
+
- Pick the level with semver:
|
|
23
|
+
- **Major** (`X.0.0`): the release stops working for some current users, for example raising `requires-python`, moving a dependency floor across a major version, or changing public API in an incompatible way.
|
|
24
|
+
- **Minor** (`x.Y.0`): new backward-compatible features.
|
|
25
|
+
- **Patch** (`x.y.Z`): backward-compatible dependency floor raises and bug fixes.
|
|
26
|
+
- For a major bump, fence the incompatibility so users get a clear error instead of a confusing failure: declare it in `pyproject.toml` (`requires-python`, dependency floors, classifiers) and, where install metadata cannot enforce it, add a runtime check with a message that names the requirement.
|
|
27
|
+
- Put `Make new Release vX.Y.Z` at the start of the PR title so maintainers can tell release PRs apart (for example `Make new Release v1.0.10: bump langgraph floor`).
|
|
28
|
+
- In the PR body, state the old and new version, the bump level and the reason for it, and that a maintainer must run the release workflow after merging.
|
|
29
|
+
|
|
30
|
+
PRs that only touch CI, docs, or tests do not need a version bump.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: langgraph-checkpointer-couchbase
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Project-URL: Documentation, https://github.com/couchbase-ecosystem/langgraph-checkpointer-couchbase#readme
|
|
5
5
|
Project-URL: Issues, https://github.com/couchbase-ecosystem/langgraph-checkpointer-couchbase/issues
|
|
6
6
|
Project-URL: Source, https://github.com/couchbase-ecosystem/langgraph-checkpointer-couchbase
|
|
@@ -10,15 +10,14 @@ License-File: LICENSE
|
|
|
10
10
|
Keywords: checkpointer,couchbase,langchain,langgraph,persistence
|
|
11
11
|
Classifier: Development Status :: 4 - Beta
|
|
12
12
|
Classifier: Programming Language :: Python
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
15
13
|
Classifier: Programming Language :: Python :: 3.10
|
|
16
14
|
Classifier: Programming Language :: Python :: 3.11
|
|
17
15
|
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
17
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
19
18
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
20
|
-
Requires-Python: >=3.
|
|
21
|
-
Requires-Dist: couchbase>=4.
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: couchbase>=4.6.3
|
|
22
21
|
Requires-Dist: langchain-openai>=1.1.3
|
|
23
22
|
Requires-Dist: langchain>=1.1.3
|
|
24
23
|
Requires-Dist: langgraph>=1.0.5
|
|
@@ -50,10 +49,11 @@ pip install langgraph-checkpointer-couchbase
|
|
|
50
49
|
|
|
51
50
|
## Requirements
|
|
52
51
|
|
|
53
|
-
- Python 3.
|
|
52
|
+
- Python 3.10+
|
|
54
53
|
- Couchbase Server (7.0+ recommended)
|
|
55
|
-
-
|
|
56
|
-
-
|
|
54
|
+
- Couchbase Python SDK 4.6.3+
|
|
55
|
+
- LangGraph 1.0.5+
|
|
56
|
+
- LangChain 1.1.3+ and LangChain OpenAI 1.1.3+
|
|
57
57
|
|
|
58
58
|
## Prerequisites
|
|
59
59
|
|
|
@@ -67,6 +67,7 @@ First, set up your agent tools and model:
|
|
|
67
67
|
|
|
68
68
|
```python
|
|
69
69
|
from typing import Literal
|
|
70
|
+
from langchain_core.tools import tool
|
|
70
71
|
from langchain_openai import ChatOpenAI
|
|
71
72
|
|
|
72
73
|
@tool
|
|
@@ -81,7 +82,7 @@ def get_weather(city: Literal["nyc", "sf"]):
|
|
|
81
82
|
|
|
82
83
|
|
|
83
84
|
tools = [get_weather]
|
|
84
|
-
model = ChatOpenAI(
|
|
85
|
+
model = ChatOpenAI(model="gpt-5-mini", temperature=0)
|
|
85
86
|
```
|
|
86
87
|
|
|
87
88
|
### Synchronous Usage
|
|
@@ -89,7 +90,7 @@ model = ChatOpenAI(model_name="gpt-4o-mini", temperature=0)
|
|
|
89
90
|
```python
|
|
90
91
|
import os
|
|
91
92
|
from langgraph_checkpointer_couchbase import CouchbaseSaver
|
|
92
|
-
from
|
|
93
|
+
from langchain.agents import create_agent
|
|
93
94
|
|
|
94
95
|
with CouchbaseSaver.from_conn_info(
|
|
95
96
|
cb_conn_str=os.getenv("CB_CLUSTER") or "couchbase://localhost",
|
|
@@ -99,7 +100,7 @@ with CouchbaseSaver.from_conn_info(
|
|
|
99
100
|
scope_name=os.getenv("CB_SCOPE") or "langgraph",
|
|
100
101
|
) as checkpointer:
|
|
101
102
|
# Create the agent with checkpointing
|
|
102
|
-
graph =
|
|
103
|
+
graph = create_agent(model, tools=tools, checkpointer=checkpointer)
|
|
103
104
|
|
|
104
105
|
# Configure with a unique thread ID
|
|
105
106
|
config = {"configurable": {"thread_id": "1"}}
|
|
@@ -125,7 +126,7 @@ from acouchbase.cluster import Cluster as ACluster
|
|
|
125
126
|
from couchbase.auth import PasswordAuthenticator
|
|
126
127
|
from couchbase.options import ClusterOptions
|
|
127
128
|
from langgraph_checkpointer_couchbase import AsyncCouchbaseSaver
|
|
128
|
-
from
|
|
129
|
+
from langchain.agents import create_agent
|
|
129
130
|
|
|
130
131
|
auth = PasswordAuthenticator(
|
|
131
132
|
os.getenv("CB_USERNAME") or "Administrator",
|
|
@@ -143,7 +144,7 @@ async with AsyncCouchbaseSaver.from_cluster(
|
|
|
143
144
|
scope_name=scope_name,
|
|
144
145
|
) as checkpointer:
|
|
145
146
|
# Create the agent with checkpointing
|
|
146
|
-
graph =
|
|
147
|
+
graph = create_agent(model, tools=tools, checkpointer=checkpointer)
|
|
147
148
|
|
|
148
149
|
# Configure with a unique thread ID
|
|
149
150
|
config = {"configurable": {"thread_id": "2"}}
|
|
@@ -166,6 +167,17 @@ async with AsyncCouchbaseSaver.from_cluster(
|
|
|
166
167
|
await cluster.close()
|
|
167
168
|
```
|
|
168
169
|
|
|
170
|
+
## Recommended Indexes
|
|
171
|
+
|
|
172
|
+
The saver creates the `checkpoints` and `checkpoint_writes` collections on first use, but not indexes. Without them, queries fall back to sequential scans, which are slow on large collections and may not return a checkpoint written immediately before. Create these indexes once per scope (replace `test` and `langgraph` with your bucket and scope):
|
|
173
|
+
|
|
174
|
+
```sql
|
|
175
|
+
CREATE INDEX idx_checkpoints_thread IF NOT EXISTS
|
|
176
|
+
ON `test`.`langgraph`.`checkpoints`(thread_id, checkpoint_ns, checkpoint_id);
|
|
177
|
+
CREATE INDEX idx_checkpoint_writes_thread IF NOT EXISTS
|
|
178
|
+
ON `test`.`langgraph`.`checkpoint_writes`(thread_id, checkpoint_ns, checkpoint_id);
|
|
179
|
+
```
|
|
180
|
+
|
|
169
181
|
## Configuration Options
|
|
170
182
|
|
|
171
183
|
| Parameter | Description | Default |
|
|
@@ -176,6 +188,22 @@ await cluster.close()
|
|
|
176
188
|
| CB_BUCKET | Bucket to store checkpoints | test |
|
|
177
189
|
| CB_SCOPE | Scope within bucket | langgraph |
|
|
178
190
|
|
|
191
|
+
## Running the Tests
|
|
192
|
+
|
|
193
|
+
The checkpointer tests run against a live Couchbase cluster and do not need an LLM. Create the bucket, scope, and the [recommended indexes](#recommended-indexes) first, then:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
pip install -e . pytest pytest-asyncio
|
|
197
|
+
export CB_CLUSTER=couchbase://localhost CB_USERNAME=Administrator CB_PASSWORD=password CB_BUCKET=test CB_SCOPE=langgraph
|
|
198
|
+
pytest tests/test_checkpointer.py
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
The end-to-end agent example in `tests/agent_e2e_test.py` additionally requires `OPENAI_API_KEY`:
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
python tests/agent_e2e_test.py
|
|
205
|
+
```
|
|
206
|
+
|
|
179
207
|
## Usage Data
|
|
180
208
|
|
|
181
209
|
This product automatically collects usage and performance data (such as product name and version) and browser information (such as IP address) (collectively, "Usage Data"). Couchbase uses Usage Data, along with other data you may provide to Couchbase (such as your user name or email address), to develop and improve our products as well as inform our sales and marketing programs. We do not access or collect any data you store in Couchbase products. We use Usage Data to understand aggregate usage patterns and make our products more useful to you. For more information on how Couchbase collects, protects, and processes information, please refer to the Couchbase Privacy Policy viewable at https://www.couchbase.com/privacy-policy.
|
|
@@ -20,10 +20,11 @@ pip install langgraph-checkpointer-couchbase
|
|
|
20
20
|
|
|
21
21
|
## Requirements
|
|
22
22
|
|
|
23
|
-
- Python 3.
|
|
23
|
+
- Python 3.10+
|
|
24
24
|
- Couchbase Server (7.0+ recommended)
|
|
25
|
-
-
|
|
26
|
-
-
|
|
25
|
+
- Couchbase Python SDK 4.6.3+
|
|
26
|
+
- LangGraph 1.0.5+
|
|
27
|
+
- LangChain 1.1.3+ and LangChain OpenAI 1.1.3+
|
|
27
28
|
|
|
28
29
|
## Prerequisites
|
|
29
30
|
|
|
@@ -37,6 +38,7 @@ First, set up your agent tools and model:
|
|
|
37
38
|
|
|
38
39
|
```python
|
|
39
40
|
from typing import Literal
|
|
41
|
+
from langchain_core.tools import tool
|
|
40
42
|
from langchain_openai import ChatOpenAI
|
|
41
43
|
|
|
42
44
|
@tool
|
|
@@ -51,7 +53,7 @@ def get_weather(city: Literal["nyc", "sf"]):
|
|
|
51
53
|
|
|
52
54
|
|
|
53
55
|
tools = [get_weather]
|
|
54
|
-
model = ChatOpenAI(
|
|
56
|
+
model = ChatOpenAI(model="gpt-5-mini", temperature=0)
|
|
55
57
|
```
|
|
56
58
|
|
|
57
59
|
### Synchronous Usage
|
|
@@ -59,7 +61,7 @@ model = ChatOpenAI(model_name="gpt-4o-mini", temperature=0)
|
|
|
59
61
|
```python
|
|
60
62
|
import os
|
|
61
63
|
from langgraph_checkpointer_couchbase import CouchbaseSaver
|
|
62
|
-
from
|
|
64
|
+
from langchain.agents import create_agent
|
|
63
65
|
|
|
64
66
|
with CouchbaseSaver.from_conn_info(
|
|
65
67
|
cb_conn_str=os.getenv("CB_CLUSTER") or "couchbase://localhost",
|
|
@@ -69,7 +71,7 @@ with CouchbaseSaver.from_conn_info(
|
|
|
69
71
|
scope_name=os.getenv("CB_SCOPE") or "langgraph",
|
|
70
72
|
) as checkpointer:
|
|
71
73
|
# Create the agent with checkpointing
|
|
72
|
-
graph =
|
|
74
|
+
graph = create_agent(model, tools=tools, checkpointer=checkpointer)
|
|
73
75
|
|
|
74
76
|
# Configure with a unique thread ID
|
|
75
77
|
config = {"configurable": {"thread_id": "1"}}
|
|
@@ -95,7 +97,7 @@ from acouchbase.cluster import Cluster as ACluster
|
|
|
95
97
|
from couchbase.auth import PasswordAuthenticator
|
|
96
98
|
from couchbase.options import ClusterOptions
|
|
97
99
|
from langgraph_checkpointer_couchbase import AsyncCouchbaseSaver
|
|
98
|
-
from
|
|
100
|
+
from langchain.agents import create_agent
|
|
99
101
|
|
|
100
102
|
auth = PasswordAuthenticator(
|
|
101
103
|
os.getenv("CB_USERNAME") or "Administrator",
|
|
@@ -113,7 +115,7 @@ async with AsyncCouchbaseSaver.from_cluster(
|
|
|
113
115
|
scope_name=scope_name,
|
|
114
116
|
) as checkpointer:
|
|
115
117
|
# Create the agent with checkpointing
|
|
116
|
-
graph =
|
|
118
|
+
graph = create_agent(model, tools=tools, checkpointer=checkpointer)
|
|
117
119
|
|
|
118
120
|
# Configure with a unique thread ID
|
|
119
121
|
config = {"configurable": {"thread_id": "2"}}
|
|
@@ -136,6 +138,17 @@ async with AsyncCouchbaseSaver.from_cluster(
|
|
|
136
138
|
await cluster.close()
|
|
137
139
|
```
|
|
138
140
|
|
|
141
|
+
## Recommended Indexes
|
|
142
|
+
|
|
143
|
+
The saver creates the `checkpoints` and `checkpoint_writes` collections on first use, but not indexes. Without them, queries fall back to sequential scans, which are slow on large collections and may not return a checkpoint written immediately before. Create these indexes once per scope (replace `test` and `langgraph` with your bucket and scope):
|
|
144
|
+
|
|
145
|
+
```sql
|
|
146
|
+
CREATE INDEX idx_checkpoints_thread IF NOT EXISTS
|
|
147
|
+
ON `test`.`langgraph`.`checkpoints`(thread_id, checkpoint_ns, checkpoint_id);
|
|
148
|
+
CREATE INDEX idx_checkpoint_writes_thread IF NOT EXISTS
|
|
149
|
+
ON `test`.`langgraph`.`checkpoint_writes`(thread_id, checkpoint_ns, checkpoint_id);
|
|
150
|
+
```
|
|
151
|
+
|
|
139
152
|
## Configuration Options
|
|
140
153
|
|
|
141
154
|
| Parameter | Description | Default |
|
|
@@ -146,6 +159,22 @@ await cluster.close()
|
|
|
146
159
|
| CB_BUCKET | Bucket to store checkpoints | test |
|
|
147
160
|
| CB_SCOPE | Scope within bucket | langgraph |
|
|
148
161
|
|
|
162
|
+
## Running the Tests
|
|
163
|
+
|
|
164
|
+
The checkpointer tests run against a live Couchbase cluster and do not need an LLM. Create the bucket, scope, and the [recommended indexes](#recommended-indexes) first, then:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
pip install -e . pytest pytest-asyncio
|
|
168
|
+
export CB_CLUSTER=couchbase://localhost CB_USERNAME=Administrator CB_PASSWORD=password CB_BUCKET=test CB_SCOPE=langgraph
|
|
169
|
+
pytest tests/test_checkpointer.py
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The end-to-end agent example in `tests/agent_e2e_test.py` additionally requires `OPENAI_API_KEY`:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
python tests/agent_e2e_test.py
|
|
176
|
+
```
|
|
177
|
+
|
|
149
178
|
## Usage Data
|
|
150
179
|
|
|
151
180
|
This product automatically collects usage and performance data (such as product name and version) and browser information (such as IP address) (collectively, "Usage Data"). Couchbase uses Usage Data, along with other data you may provide to Couchbase (such as your user name or email address), to develop and improve our products as well as inform our sales and marketing programs. We do not access or collect any data you store in Couchbase products. We use Usage Data to understand aggregate usage patterns and make our products more useful to you. For more information on how Couchbase collects, protects, and processes information, please refer to the Couchbase Privacy Policy viewable at https://www.couchbase.com/privacy-policy.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.0.0"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
if sys.version_info < (3, 10):
|
|
4
|
+
raise ImportError(
|
|
5
|
+
"langgraph-checkpointer-couchbase 2.x requires Python 3.10 or newer "
|
|
6
|
+
f"(found {sys.version_info[0]}.{sys.version_info[1]}). "
|
|
7
|
+
"Upgrade Python, or pin langgraph-checkpointer-couchbase<2 to stay on an older release."
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
from .async_cb_saver import AsyncCouchbaseSaver
|
|
11
|
+
from .couchbase_saver import CouchbaseSaver
|
|
12
|
+
|
|
13
|
+
__all__ = ["CouchbaseSaver", "AsyncCouchbaseSaver"]
|
|
14
|
+
|
|
15
|
+
# --- Package-level telemetry (non-blocking, fire-and-forget) ---
|
|
16
|
+
import langgraph_checkpointer_couchbase.telemetry # noqa: F401, E402
|
|
@@ -9,6 +9,7 @@ from acouchbase.bucket import Bucket as ABucket
|
|
|
9
9
|
from couchbase.auth import PasswordAuthenticator
|
|
10
10
|
from couchbase.options import ClusterOptions, QueryOptions, UpsertOptions
|
|
11
11
|
from couchbase.exceptions import CollectionAlreadyExistsException
|
|
12
|
+
from couchbase.n1ql import QueryScanConsistency
|
|
12
13
|
|
|
13
14
|
from langgraph.checkpoint.base import (
|
|
14
15
|
BaseCheckpointSaver,
|
|
@@ -179,7 +180,7 @@ class AsyncCouchbaseSaver(BaseCheckpointSaver):
|
|
|
179
180
|
query = f'SELECT * FROM `{self.bucket_name}`.`{self.scope_name}`.`{self.checkpoints_collection_name}` WHERE thread_id = $1 AND checkpoint_ns = $2 ORDER BY checkpoint_id DESC LIMIT 1'
|
|
180
181
|
query_params = [thread_id, checkpoint_ns]
|
|
181
182
|
|
|
182
|
-
result = self.cluster.query(query, QueryOptions(positional_parameters=query_params))
|
|
183
|
+
result = self.cluster.query(query, QueryOptions(positional_parameters=query_params, scan_consistency=QueryScanConsistency.REQUEST_PLUS))
|
|
183
184
|
|
|
184
185
|
async for row in result:
|
|
185
186
|
doc = row[self.checkpoints_collection_name]
|
|
@@ -193,7 +194,7 @@ class AsyncCouchbaseSaver(BaseCheckpointSaver):
|
|
|
193
194
|
serialized_writes_query = f'SELECT * FROM `{self.bucket_name}`.`{self.scope_name}`.`{self.checkpoint_writes_collection_name}` WHERE thread_id = $1 AND checkpoint_ns = $2 AND checkpoint_id = $3'
|
|
194
195
|
serialized_writes_params = [thread_id, checkpoint_ns, doc["checkpoint_id"] or ""]
|
|
195
196
|
|
|
196
|
-
serialized_writes_result = self.cluster.query(serialized_writes_query, QueryOptions(positional_parameters=serialized_writes_params))
|
|
197
|
+
serialized_writes_result = self.cluster.query(serialized_writes_query, QueryOptions(positional_parameters=serialized_writes_params, scan_consistency=QueryScanConsistency.REQUEST_PLUS))
|
|
197
198
|
|
|
198
199
|
pending_writes = []
|
|
199
200
|
async for write_doc in serialized_writes_result:
|
|
@@ -273,7 +274,7 @@ class AsyncCouchbaseSaver(BaseCheckpointSaver):
|
|
|
273
274
|
if limit is not None:
|
|
274
275
|
query += f" LIMIT {limit}"
|
|
275
276
|
|
|
276
|
-
result = self.cluster.query(query, QueryOptions(positional_parameters=query_params))
|
|
277
|
+
result = self.cluster.query(query, QueryOptions(positional_parameters=query_params, scan_consistency=QueryScanConsistency.REQUEST_PLUS))
|
|
277
278
|
|
|
278
279
|
async for row in result:
|
|
279
280
|
doc = row[self.checkpoints_collection_name]
|
|
@@ -9,6 +9,7 @@ from couchbase.bucket import Bucket
|
|
|
9
9
|
from couchbase.auth import PasswordAuthenticator
|
|
10
10
|
from couchbase.options import ClusterOptions, QueryOptions, UpsertOptions
|
|
11
11
|
from couchbase.exceptions import CollectionAlreadyExistsException
|
|
12
|
+
from couchbase.n1ql import QueryScanConsistency
|
|
12
13
|
|
|
13
14
|
from langgraph.checkpoint.base import (
|
|
14
15
|
BaseCheckpointSaver,
|
|
@@ -163,7 +164,7 @@ class CouchbaseSaver(BaseCheckpointSaver):
|
|
|
163
164
|
query = f'SELECT * FROM `{self.bucket_name}`.`{self.scope_name}`.`{self.checkpoints_collection_name}` WHERE thread_id = $1 AND checkpoint_ns = $2 ORDER BY checkpoint_id DESC LIMIT 1'
|
|
164
165
|
query_params = [thread_id, checkpoint_ns]
|
|
165
166
|
|
|
166
|
-
result = self.cluster.query(query, QueryOptions(positional_parameters=query_params))
|
|
167
|
+
result = self.cluster.query(query, QueryOptions(positional_parameters=query_params, scan_consistency=QueryScanConsistency.REQUEST_PLUS))
|
|
167
168
|
|
|
168
169
|
for row in result:
|
|
169
170
|
doc = row[self.checkpoints_collection_name]
|
|
@@ -179,7 +180,7 @@ class CouchbaseSaver(BaseCheckpointSaver):
|
|
|
179
180
|
|
|
180
181
|
serialized_writes_query = f'SELECT * FROM `{self.bucket_name}`.`{self.scope_name}`.`{self.checkpoint_writes_collection_name}` WHERE thread_id = $1 AND checkpoint_ns = $2 AND checkpoint_id = $3'
|
|
181
182
|
serialized_writes_params = [thread_id, checkpoint_ns, doc["checkpoint_id"] or ""]
|
|
182
|
-
serialized_writes_result = self.cluster.query(serialized_writes_query, QueryOptions(positional_parameters=serialized_writes_params))
|
|
183
|
+
serialized_writes_result = self.cluster.query(serialized_writes_query, QueryOptions(positional_parameters=serialized_writes_params, scan_consistency=QueryScanConsistency.REQUEST_PLUS))
|
|
183
184
|
|
|
184
185
|
pending_writes = []
|
|
185
186
|
for write_doc in serialized_writes_result:
|
|
@@ -265,7 +266,7 @@ class CouchbaseSaver(BaseCheckpointSaver):
|
|
|
265
266
|
if limit is not None:
|
|
266
267
|
query += f" LIMIT {limit}"
|
|
267
268
|
|
|
268
|
-
result = self.cluster.query(query, QueryOptions(positional_parameters=query_params))
|
|
269
|
+
result = self.cluster.query(query, QueryOptions(positional_parameters=query_params, scan_consistency=QueryScanConsistency.REQUEST_PLUS))
|
|
269
270
|
|
|
270
271
|
for row in result:
|
|
271
272
|
doc = row[self.checkpoints_collection_name]
|
{langgraph_checkpointer_couchbase-1.0.9 → langgraph_checkpointer_couchbase-2.0.0}/pyproject.toml
RENAMED
|
@@ -7,7 +7,7 @@ name = "langgraph-checkpointer-couchbase"
|
|
|
7
7
|
dynamic = ["version"]
|
|
8
8
|
description = ''
|
|
9
9
|
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
11
|
license = "MIT"
|
|
12
12
|
keywords = ["langchain","langgraph", "couchbase", "checkpointer", "persistence"]
|
|
13
13
|
authors = [
|
|
@@ -16,16 +16,15 @@ authors = [
|
|
|
16
16
|
classifiers = [
|
|
17
17
|
"Development Status :: 4 - Beta",
|
|
18
18
|
"Programming Language :: Python",
|
|
19
|
-
"Programming Language :: Python :: 3.8",
|
|
20
|
-
"Programming Language :: Python :: 3.9",
|
|
21
19
|
"Programming Language :: Python :: 3.10",
|
|
22
20
|
"Programming Language :: Python :: 3.11",
|
|
23
21
|
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
24
23
|
"Programming Language :: Python :: Implementation :: CPython",
|
|
25
24
|
"Programming Language :: Python :: Implementation :: PyPy",
|
|
26
25
|
]
|
|
27
26
|
dependencies = [
|
|
28
|
-
"couchbase>=4.
|
|
27
|
+
"couchbase>=4.6.3",
|
|
29
28
|
"langgraph>=1.0.5",
|
|
30
29
|
"langchain-openai>=1.1.3",
|
|
31
30
|
"pydantic>=2.12.5",
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Checkpointer integration tests against a live Couchbase cluster.
|
|
2
|
+
|
|
3
|
+
These tests do not need an LLM. They exercise CouchbaseSaver and
|
|
4
|
+
AsyncCouchbaseSaver directly and through a minimal LangGraph StateGraph.
|
|
5
|
+
|
|
6
|
+
Required environment variables: CB_CLUSTER, CB_USERNAME, CB_PASSWORD,
|
|
7
|
+
CB_BUCKET, CB_SCOPE. The bucket and scope must already exist; the saver
|
|
8
|
+
creates its collections. Create the indexes listed in the README so reads
|
|
9
|
+
see checkpoints written immediately before.
|
|
10
|
+
"""
|
|
11
|
+
import operator
|
|
12
|
+
import os
|
|
13
|
+
import uuid
|
|
14
|
+
from typing import Annotated, TypedDict
|
|
15
|
+
|
|
16
|
+
import pytest
|
|
17
|
+
from langgraph.checkpoint.base import empty_checkpoint
|
|
18
|
+
from langgraph.graph import END, START, StateGraph
|
|
19
|
+
|
|
20
|
+
from langgraph_checkpointer_couchbase import AsyncCouchbaseSaver, CouchbaseSaver
|
|
21
|
+
|
|
22
|
+
REQUIRED_ENV = ["CB_CLUSTER", "CB_USERNAME", "CB_PASSWORD", "CB_BUCKET", "CB_SCOPE"]
|
|
23
|
+
|
|
24
|
+
pytestmark = pytest.mark.skipif(
|
|
25
|
+
any(not os.getenv(name) for name in REQUIRED_ENV),
|
|
26
|
+
reason=f"Couchbase connection not configured ({', '.join(REQUIRED_ENV)})",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def conn_info():
|
|
31
|
+
return dict(
|
|
32
|
+
cb_conn_str=os.environ["CB_CLUSTER"],
|
|
33
|
+
cb_username=os.environ["CB_USERNAME"],
|
|
34
|
+
cb_password=os.environ["CB_PASSWORD"],
|
|
35
|
+
bucket_name=os.environ["CB_BUCKET"],
|
|
36
|
+
scope_name=os.environ["CB_SCOPE"],
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def new_thread_config():
|
|
41
|
+
return {"configurable": {"thread_id": f"test-{uuid.uuid4()}", "checkpoint_ns": ""}}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def make_checkpoint(step):
|
|
45
|
+
checkpoint = empty_checkpoint()
|
|
46
|
+
checkpoint["channel_values"] = {"step": step}
|
|
47
|
+
checkpoint["channel_versions"] = {"step": step}
|
|
48
|
+
return checkpoint
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class State(TypedDict):
|
|
52
|
+
items: Annotated[list, operator.add]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def build_graph(checkpointer):
|
|
56
|
+
builder = StateGraph(State)
|
|
57
|
+
builder.add_node("first", lambda state: {"items": ["first"]})
|
|
58
|
+
builder.add_node("second", lambda state: {"items": ["second"]})
|
|
59
|
+
builder.add_edge(START, "first")
|
|
60
|
+
builder.add_edge("first", "second")
|
|
61
|
+
builder.add_edge("second", END)
|
|
62
|
+
return builder.compile(checkpointer=checkpointer)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_sync_put_get_list_and_writes():
|
|
66
|
+
with CouchbaseSaver.from_conn_info(**conn_info()) as saver:
|
|
67
|
+
config = new_thread_config()
|
|
68
|
+
assert saver.get_tuple(config) is None
|
|
69
|
+
|
|
70
|
+
first = make_checkpoint(1)
|
|
71
|
+
first_config = saver.put(config, first, {"source": "input", "step": 1}, {})
|
|
72
|
+
second = make_checkpoint(2)
|
|
73
|
+
second_config = saver.put(first_config, second, {"source": "loop", "step": 2}, {})
|
|
74
|
+
saver.put_writes(second_config, [("items", "a"), ("items", "b")], "task-1")
|
|
75
|
+
|
|
76
|
+
latest = saver.get_tuple(config)
|
|
77
|
+
assert latest.config["configurable"]["checkpoint_id"] == second["id"]
|
|
78
|
+
assert latest.checkpoint["channel_values"] == {"step": 2}
|
|
79
|
+
assert latest.metadata == {"source": "loop", "step": 2}
|
|
80
|
+
assert latest.parent_config["configurable"]["checkpoint_id"] == first["id"]
|
|
81
|
+
assert sorted(latest.pending_writes) == [("task-1", "items", "a"), ("task-1", "items", "b")]
|
|
82
|
+
|
|
83
|
+
by_id = saver.get_tuple(first_config)
|
|
84
|
+
assert by_id.checkpoint["id"] == first["id"]
|
|
85
|
+
assert by_id.parent_config is None
|
|
86
|
+
|
|
87
|
+
listed = list(saver.list(config))
|
|
88
|
+
assert [t.checkpoint["id"] for t in listed] == [second["id"], first["id"]]
|
|
89
|
+
assert [t.checkpoint["id"] for t in saver.list(config, limit=1)] == [second["id"]]
|
|
90
|
+
assert [t.checkpoint["id"] for t in saver.list(config, before=second_config)] == [first["id"]]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@pytest.mark.asyncio
|
|
94
|
+
async def test_async_put_get_list_and_writes():
|
|
95
|
+
async with AsyncCouchbaseSaver.from_conn_info(**conn_info()) as saver:
|
|
96
|
+
config = new_thread_config()
|
|
97
|
+
assert await saver.aget_tuple(config) is None
|
|
98
|
+
|
|
99
|
+
first = make_checkpoint(1)
|
|
100
|
+
first_config = await saver.aput(config, first, {"source": "input", "step": 1}, {})
|
|
101
|
+
second = make_checkpoint(2)
|
|
102
|
+
second_config = await saver.aput(first_config, second, {"source": "loop", "step": 2}, {})
|
|
103
|
+
await saver.aput_writes(second_config, [("items", "a")], "task-1")
|
|
104
|
+
|
|
105
|
+
latest = await saver.aget_tuple(config)
|
|
106
|
+
assert latest.config["configurable"]["checkpoint_id"] == second["id"]
|
|
107
|
+
assert latest.checkpoint["channel_values"] == {"step": 2}
|
|
108
|
+
assert latest.metadata == {"source": "loop", "step": 2}
|
|
109
|
+
assert latest.parent_config["configurable"]["checkpoint_id"] == first["id"]
|
|
110
|
+
assert latest.pending_writes == [("task-1", "items", "a")]
|
|
111
|
+
|
|
112
|
+
listed = [t async for t in saver.alist(config)]
|
|
113
|
+
assert [t.checkpoint["id"] for t in listed] == [second["id"], first["id"]]
|
|
114
|
+
limited = [t async for t in saver.alist(config, limit=1)]
|
|
115
|
+
assert [t.checkpoint["id"] for t in limited] == [second["id"]]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_sync_graph_persists_state():
|
|
119
|
+
with CouchbaseSaver.from_conn_info(**conn_info()) as saver:
|
|
120
|
+
graph = build_graph(saver)
|
|
121
|
+
config = new_thread_config()
|
|
122
|
+
|
|
123
|
+
result = graph.invoke({"items": ["start"]}, config)
|
|
124
|
+
assert result == {"items": ["start", "first", "second"]}
|
|
125
|
+
|
|
126
|
+
state = graph.get_state(config)
|
|
127
|
+
assert state.values == {"items": ["start", "first", "second"]}
|
|
128
|
+
assert len(list(graph.get_state_history(config))) >= 3
|
|
129
|
+
|
|
130
|
+
# A second run on the same thread continues from the persisted state.
|
|
131
|
+
result = graph.invoke({"items": ["again"]}, config)
|
|
132
|
+
assert result["items"] == ["start", "first", "second", "again", "first", "second"]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@pytest.mark.asyncio
|
|
136
|
+
async def test_async_graph_persists_state():
|
|
137
|
+
async with AsyncCouchbaseSaver.from_conn_info(**conn_info()) as saver:
|
|
138
|
+
graph = build_graph(saver)
|
|
139
|
+
config = new_thread_config()
|
|
140
|
+
|
|
141
|
+
result = await graph.ainvoke({"items": ["start"]}, config)
|
|
142
|
+
assert result == {"items": ["start", "first", "second"]}
|
|
143
|
+
|
|
144
|
+
state = await graph.aget_state(config)
|
|
145
|
+
assert state.values == {"items": ["start", "first", "second"]}
|
|
146
|
+
history = [s async for s in graph.aget_state_history(config)]
|
|
147
|
+
assert len(history) >= 3
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""The package refuses to import on Python versions older than 3.10."""
|
|
2
|
+
import importlib
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
import langgraph_checkpointer_couchbase
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_import_fails_with_clear_error_on_old_python(monkeypatch):
|
|
11
|
+
monkeypatch.setattr(sys, "version_info", (3, 9, 18, "final", 0))
|
|
12
|
+
with pytest.raises(ImportError, match="requires Python 3.10 or newer"):
|
|
13
|
+
importlib.reload(langgraph_checkpointer_couchbase)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_import_succeeds_on_supported_python():
|
|
17
|
+
importlib.reload(langgraph_checkpointer_couchbase)
|
|
18
|
+
assert langgraph_checkpointer_couchbase.CouchbaseSaver
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.0.9"
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
from .async_cb_saver import AsyncCouchbaseSaver
|
|
2
|
-
from .couchbase_saver import CouchbaseSaver
|
|
3
|
-
|
|
4
|
-
__all__ = ["CouchbaseSaver", "AsyncCouchbaseSaver"]
|
|
5
|
-
|
|
6
|
-
# --- Package-level telemetry (non-blocking, fire-and-forget) ---
|
|
7
|
-
import langgraph_checkpointer_couchbase.telemetry # noqa: F401, E402
|
{langgraph_checkpointer_couchbase-1.0.9 → langgraph_checkpointer_couchbase-2.0.0}/.env.example
RENAMED
|
File without changes
|
{langgraph_checkpointer_couchbase-1.0.9 → langgraph_checkpointer_couchbase-2.0.0}/.gitignore
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{langgraph_checkpointer_couchbase-1.0.9 → langgraph_checkpointer_couchbase-2.0.0}/tests/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|