langgraph-checkpointer-couchbase 1.0.6__tar.gz → 1.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_checkpointer_couchbase-1.0.6 → langgraph_checkpointer_couchbase-1.0.8}/.github/workflows/release.yaml +3 -7
- {langgraph_checkpointer_couchbase-1.0.6 → langgraph_checkpointer_couchbase-1.0.8}/PKG-INFO +64 -48
- {langgraph_checkpointer_couchbase-1.0.6 → langgraph_checkpointer_couchbase-1.0.8}/README.md +56 -42
- {langgraph_checkpointer_couchbase-1.0.6 → langgraph_checkpointer_couchbase-1.0.8}/langgraph_checkpointer_couchbase/async_cb_saver.py +75 -19
- {langgraph_checkpointer_couchbase-1.0.6 → langgraph_checkpointer_couchbase-1.0.8}/langgraph_checkpointer_couchbase/couchbase_saver.py +23 -17
- {langgraph_checkpointer_couchbase-1.0.6 → langgraph_checkpointer_couchbase-1.0.8}/pyproject.toml +8 -6
- langgraph_checkpointer_couchbase-1.0.8/tests/agent_e2e_test.py +88 -0
- langgraph_checkpointer_couchbase-1.0.6/tests/agent_e2e_test.py +0 -70
- {langgraph_checkpointer_couchbase-1.0.6 → langgraph_checkpointer_couchbase-1.0.8}/.env.example +0 -0
- {langgraph_checkpointer_couchbase-1.0.6 → langgraph_checkpointer_couchbase-1.0.8}/.gitignore +0 -0
- {langgraph_checkpointer_couchbase-1.0.6 → langgraph_checkpointer_couchbase-1.0.8}/LICENSE +0 -0
- {langgraph_checkpointer_couchbase-1.0.6 → langgraph_checkpointer_couchbase-1.0.8}/langgraph_checkpointer_couchbase/__init__.py +0 -0
- {langgraph_checkpointer_couchbase-1.0.6 → langgraph_checkpointer_couchbase-1.0.8}/langgraph_checkpointer_couchbase/utils.py +0 -0
- {langgraph_checkpointer_couchbase-1.0.6 → langgraph_checkpointer_couchbase-1.0.8}/tests/__init__.py +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
name: Publish Package to PyPI
|
|
2
2
|
on:
|
|
3
3
|
push:
|
|
4
|
-
|
|
5
|
-
-
|
|
4
|
+
tags:
|
|
5
|
+
- "v*"
|
|
6
6
|
workflow_dispatch:
|
|
7
7
|
|
|
8
8
|
jobs:
|
|
@@ -29,10 +29,6 @@ jobs:
|
|
|
29
29
|
- name: Hatch build
|
|
30
30
|
run: hatch build
|
|
31
31
|
|
|
32
|
-
- name: Get hatch version
|
|
33
|
-
id: hatch_version_step
|
|
34
|
-
run: echo "VERSION=$(hatch version)" >> $GITHUB_OUTPUT
|
|
35
|
-
|
|
36
32
|
- name: Publish release distributions to PyPI
|
|
37
33
|
uses: pypa/gh-action-pypi-publish@release/v1
|
|
38
34
|
|
|
@@ -40,6 +36,6 @@ jobs:
|
|
|
40
36
|
id: create_release
|
|
41
37
|
uses: softprops/action-gh-release@v2
|
|
42
38
|
with:
|
|
43
|
-
tag_name:
|
|
39
|
+
tag_name: ${{ github.ref_name }}
|
|
44
40
|
draft: false
|
|
45
41
|
prerelease: false
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: langgraph-checkpointer-couchbase
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.8
|
|
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
|
|
@@ -18,11 +18,13 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
18
18
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
19
19
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
20
20
|
Requires-Python: >=3.8
|
|
21
|
-
Requires-Dist: couchbase>=4.
|
|
22
|
-
Requires-Dist: langchain-openai>=
|
|
23
|
-
Requires-Dist:
|
|
24
|
-
Requires-Dist:
|
|
25
|
-
Requires-Dist:
|
|
21
|
+
Requires-Dist: couchbase>=4.5.0
|
|
22
|
+
Requires-Dist: langchain-openai>=1.1.3
|
|
23
|
+
Requires-Dist: langchain>=1.1.3
|
|
24
|
+
Requires-Dist: langgraph>=1.0.5
|
|
25
|
+
Requires-Dist: pydantic>=2.12.5
|
|
26
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
27
|
+
Requires-Dist: typing-extensions>=4.15.0
|
|
26
28
|
Description-Content-Type: text/markdown
|
|
27
29
|
|
|
28
30
|
# LangGraph Checkpoint Couchbase
|
|
@@ -95,58 +97,72 @@ with CouchbaseSaver.from_conn_info(
|
|
|
95
97
|
bucket_name=os.getenv("CB_BUCKET") or "test",
|
|
96
98
|
scope_name=os.getenv("CB_SCOPE") or "langgraph",
|
|
97
99
|
) as checkpointer:
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
100
|
+
# Create the agent with checkpointing
|
|
101
|
+
graph = create_react_agent(model, tools=tools, checkpointer=checkpointer)
|
|
102
|
+
|
|
103
|
+
# Configure with a unique thread ID
|
|
104
|
+
config = {"configurable": {"thread_id": "1"}}
|
|
105
|
+
|
|
106
|
+
# Run the agent
|
|
107
|
+
res = graph.invoke({"messages": [("human", "what's the weather in sf")]}, config)
|
|
108
|
+
|
|
109
|
+
# Retrieve checkpoints
|
|
110
|
+
latest_checkpoint = checkpointer.get(config)
|
|
111
|
+
latest_checkpoint_tuple = checkpointer.get_tuple(config)
|
|
112
|
+
checkpoint_tuples = list(checkpointer.list(config))
|
|
113
|
+
|
|
114
|
+
print(latest_checkpoint)
|
|
115
|
+
print(latest_checkpoint_tuple)
|
|
116
|
+
print(checkpoint_tuples)
|
|
115
117
|
```
|
|
116
118
|
|
|
117
119
|
### Asynchronous Usage
|
|
118
120
|
|
|
119
121
|
```python
|
|
120
122
|
import os
|
|
123
|
+
from acouchbase.cluster import Cluster as ACluster
|
|
124
|
+
from couchbase.auth import PasswordAuthenticator
|
|
125
|
+
from couchbase.options import ClusterOptions
|
|
121
126
|
from langgraph_checkpointer_couchbase import AsyncCouchbaseSaver
|
|
122
127
|
from langgraph.graph import create_react_agent
|
|
123
128
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
129
|
+
auth = PasswordAuthenticator(
|
|
130
|
+
os.getenv("CB_USERNAME") or "Administrator",
|
|
131
|
+
os.getenv("CB_PASSWORD") or "password",
|
|
132
|
+
)
|
|
133
|
+
options = ClusterOptions(auth)
|
|
134
|
+
cluster = await ACluster.connect(os.getenv("CB_CLUSTER") or "couchbase://localhost", options)
|
|
135
|
+
|
|
136
|
+
bucket_name = os.getenv("CB_BUCKET") or "test"
|
|
137
|
+
scope_name = os.getenv("CB_SCOPE") or "langgraph"
|
|
138
|
+
|
|
139
|
+
async with AsyncCouchbaseSaver.from_cluster(
|
|
140
|
+
cluster=cluster,
|
|
141
|
+
bucket_name=bucket_name,
|
|
142
|
+
scope_name=scope_name,
|
|
130
143
|
) as checkpointer:
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
144
|
+
# Create the agent with checkpointing
|
|
145
|
+
graph = create_react_agent(model, tools=tools, checkpointer=checkpointer)
|
|
146
|
+
|
|
147
|
+
# Configure with a unique thread ID
|
|
148
|
+
config = {"configurable": {"thread_id": "2"}}
|
|
149
|
+
|
|
150
|
+
# Run the agent asynchronously
|
|
151
|
+
res = await graph.ainvoke(
|
|
152
|
+
{"messages": [("human", "what's the weather in nyc")]}, config
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Retrieve checkpoints asynchronously
|
|
156
|
+
latest_checkpoint = await checkpointer.aget(config)
|
|
157
|
+
latest_checkpoint_tuple = await checkpointer.aget_tuple(config)
|
|
158
|
+
checkpoint_tuples = [c async for c in checkpointer.alist(config)]
|
|
159
|
+
|
|
160
|
+
print(latest_checkpoint)
|
|
161
|
+
print(latest_checkpoint_tuple)
|
|
162
|
+
print(checkpoint_tuples)
|
|
163
|
+
|
|
164
|
+
# Close the cluster when done
|
|
165
|
+
await cluster.close()
|
|
150
166
|
```
|
|
151
167
|
|
|
152
168
|
## Configuration Options
|
|
@@ -68,58 +68,72 @@ with CouchbaseSaver.from_conn_info(
|
|
|
68
68
|
bucket_name=os.getenv("CB_BUCKET") or "test",
|
|
69
69
|
scope_name=os.getenv("CB_SCOPE") or "langgraph",
|
|
70
70
|
) as checkpointer:
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
71
|
+
# Create the agent with checkpointing
|
|
72
|
+
graph = create_react_agent(model, tools=tools, checkpointer=checkpointer)
|
|
73
|
+
|
|
74
|
+
# Configure with a unique thread ID
|
|
75
|
+
config = {"configurable": {"thread_id": "1"}}
|
|
76
|
+
|
|
77
|
+
# Run the agent
|
|
78
|
+
res = graph.invoke({"messages": [("human", "what's the weather in sf")]}, config)
|
|
79
|
+
|
|
80
|
+
# Retrieve checkpoints
|
|
81
|
+
latest_checkpoint = checkpointer.get(config)
|
|
82
|
+
latest_checkpoint_tuple = checkpointer.get_tuple(config)
|
|
83
|
+
checkpoint_tuples = list(checkpointer.list(config))
|
|
84
|
+
|
|
85
|
+
print(latest_checkpoint)
|
|
86
|
+
print(latest_checkpoint_tuple)
|
|
87
|
+
print(checkpoint_tuples)
|
|
88
88
|
```
|
|
89
89
|
|
|
90
90
|
### Asynchronous Usage
|
|
91
91
|
|
|
92
92
|
```python
|
|
93
93
|
import os
|
|
94
|
+
from acouchbase.cluster import Cluster as ACluster
|
|
95
|
+
from couchbase.auth import PasswordAuthenticator
|
|
96
|
+
from couchbase.options import ClusterOptions
|
|
94
97
|
from langgraph_checkpointer_couchbase import AsyncCouchbaseSaver
|
|
95
98
|
from langgraph.graph import create_react_agent
|
|
96
99
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
auth = PasswordAuthenticator(
|
|
101
|
+
os.getenv("CB_USERNAME") or "Administrator",
|
|
102
|
+
os.getenv("CB_PASSWORD") or "password",
|
|
103
|
+
)
|
|
104
|
+
options = ClusterOptions(auth)
|
|
105
|
+
cluster = await ACluster.connect(os.getenv("CB_CLUSTER") or "couchbase://localhost", options)
|
|
106
|
+
|
|
107
|
+
bucket_name = os.getenv("CB_BUCKET") or "test"
|
|
108
|
+
scope_name = os.getenv("CB_SCOPE") or "langgraph"
|
|
109
|
+
|
|
110
|
+
async with AsyncCouchbaseSaver.from_cluster(
|
|
111
|
+
cluster=cluster,
|
|
112
|
+
bucket_name=bucket_name,
|
|
113
|
+
scope_name=scope_name,
|
|
103
114
|
) as checkpointer:
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
115
|
+
# Create the agent with checkpointing
|
|
116
|
+
graph = create_react_agent(model, tools=tools, checkpointer=checkpointer)
|
|
117
|
+
|
|
118
|
+
# Configure with a unique thread ID
|
|
119
|
+
config = {"configurable": {"thread_id": "2"}}
|
|
120
|
+
|
|
121
|
+
# Run the agent asynchronously
|
|
122
|
+
res = await graph.ainvoke(
|
|
123
|
+
{"messages": [("human", "what's the weather in nyc")]}, config
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Retrieve checkpoints asynchronously
|
|
127
|
+
latest_checkpoint = await checkpointer.aget(config)
|
|
128
|
+
latest_checkpoint_tuple = await checkpointer.aget_tuple(config)
|
|
129
|
+
checkpoint_tuples = [c async for c in checkpointer.alist(config)]
|
|
130
|
+
|
|
131
|
+
print(latest_checkpoint)
|
|
132
|
+
print(latest_checkpoint_tuple)
|
|
133
|
+
print(checkpoint_tuples)
|
|
134
|
+
|
|
135
|
+
# Close the cluster when done
|
|
136
|
+
await cluster.close()
|
|
123
137
|
```
|
|
124
138
|
|
|
125
139
|
## Configuration Options
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
from contextlib import asynccontextmanager
|
|
2
2
|
from datetime import timedelta
|
|
3
3
|
from typing import Any, AsyncIterator, Dict, Optional, Sequence, Tuple
|
|
4
|
+
import logging
|
|
4
5
|
|
|
5
6
|
from langchain_core.runnables import RunnableConfig
|
|
6
7
|
from acouchbase.cluster import Cluster as ACluster
|
|
7
8
|
from acouchbase.bucket import Bucket as ABucket
|
|
8
9
|
from couchbase.auth import PasswordAuthenticator
|
|
9
10
|
from couchbase.options import ClusterOptions, QueryOptions, UpsertOptions
|
|
11
|
+
from couchbase.exceptions import CollectionAlreadyExistsException
|
|
10
12
|
|
|
11
13
|
from langgraph.checkpoint.base import (
|
|
12
14
|
BaseCheckpointSaver,
|
|
@@ -18,6 +20,13 @@ from langgraph.checkpoint.base import (
|
|
|
18
20
|
)
|
|
19
21
|
from .utils import _encode_binary, _decode_binary
|
|
20
22
|
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
# Default timeout for database operations
|
|
26
|
+
DEFAULT_TIMEOUT = timedelta(seconds=5)
|
|
27
|
+
# Default serialization type for metadata (for backward compatibility)
|
|
28
|
+
DEFAULT_METADATA_TYPE = "json"
|
|
29
|
+
|
|
21
30
|
class AsyncCouchbaseSaver(BaseCheckpointSaver):
|
|
22
31
|
"""A checkpoint saver that stores checkpoints in a Couchbase database."""
|
|
23
32
|
|
|
@@ -35,9 +44,35 @@ class AsyncCouchbaseSaver(BaseCheckpointSaver):
|
|
|
35
44
|
self.cluster = cluster
|
|
36
45
|
self.bucket_name = bucket_name
|
|
37
46
|
self.scope_name = scope_name
|
|
47
|
+
self.bucket = self.cluster.bucket(bucket_name)
|
|
48
|
+
self.scope = self.bucket.scope(scope_name)
|
|
38
49
|
self.checkpoints_collection_name = checkpoints_collection_name
|
|
39
50
|
self.checkpoint_writes_collection_name = checkpoint_writes_collection_name
|
|
40
51
|
|
|
52
|
+
async def create_collections(self):
|
|
53
|
+
"""Create collections in the Couchbase bucket if they do not exist."""
|
|
54
|
+
|
|
55
|
+
collection_manager = self.bucket.collections()
|
|
56
|
+
try:
|
|
57
|
+
await collection_manager.create_collection(self.scope_name, self.checkpoints_collection_name)
|
|
58
|
+
except CollectionAlreadyExistsException as _:
|
|
59
|
+
pass
|
|
60
|
+
except Exception as e:
|
|
61
|
+
logger.exception("Error creating collections")
|
|
62
|
+
raise e
|
|
63
|
+
finally:
|
|
64
|
+
self.checkpoints_collection = self.bucket.scope(self.scope_name).collection(self.checkpoints_collection_name)
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
await collection_manager.create_collection(self.scope_name, self.checkpoint_writes_collection_name)
|
|
68
|
+
except CollectionAlreadyExistsException as _:
|
|
69
|
+
pass
|
|
70
|
+
except Exception as e:
|
|
71
|
+
logger.exception("Error creating collections")
|
|
72
|
+
raise e
|
|
73
|
+
finally:
|
|
74
|
+
self.checkpoint_writes_collection = self.bucket.scope(self.scope_name).collection(self.checkpoint_writes_collection_name)
|
|
75
|
+
|
|
41
76
|
@classmethod
|
|
42
77
|
@asynccontextmanager
|
|
43
78
|
async def from_conn_info(
|
|
@@ -64,20 +99,26 @@ class AsyncCouchbaseSaver(BaseCheckpointSaver):
|
|
|
64
99
|
auth = PasswordAuthenticator(cb_username, cb_password)
|
|
65
100
|
options = ClusterOptions(auth)
|
|
66
101
|
cluster = await ACluster.connect(cb_conn_str, options)
|
|
67
|
-
|
|
68
|
-
cls.cluster = cluster
|
|
69
|
-
cls.bucket_name = bucket_name
|
|
70
|
-
cls.scope_name = scope_name
|
|
71
102
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
103
|
+
bucket = cluster.bucket(bucket_name)
|
|
104
|
+
await bucket.on_connect()
|
|
105
|
+
|
|
106
|
+
saver = AsyncCouchbaseSaver(
|
|
107
|
+
cluster,
|
|
108
|
+
bucket_name,
|
|
109
|
+
scope_name,
|
|
110
|
+
checkpoints_collection_name,
|
|
111
|
+
checkpoint_writes_collection_name,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
await saver.create_collections()
|
|
75
115
|
|
|
76
116
|
yield saver
|
|
77
117
|
finally:
|
|
78
118
|
if cluster:
|
|
79
119
|
await cluster.close()
|
|
80
120
|
|
|
121
|
+
|
|
81
122
|
@classmethod
|
|
82
123
|
@asynccontextmanager
|
|
83
124
|
async def from_cluster(
|
|
@@ -98,9 +139,18 @@ class AsyncCouchbaseSaver(BaseCheckpointSaver):
|
|
|
98
139
|
AsyncCouchbaseSaver: An instance of the AsyncCouchbaseSaver
|
|
99
140
|
"""
|
|
100
141
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
142
|
+
bucket = cluster.bucket(bucket_name)
|
|
143
|
+
await bucket.on_connect()
|
|
144
|
+
|
|
145
|
+
saver = AsyncCouchbaseSaver(
|
|
146
|
+
cluster,
|
|
147
|
+
bucket_name,
|
|
148
|
+
scope_name,
|
|
149
|
+
checkpoints_collection_name,
|
|
150
|
+
checkpoint_writes_collection_name,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
await saver.create_collections()
|
|
104
154
|
|
|
105
155
|
yield saver
|
|
106
156
|
|
|
@@ -149,7 +199,7 @@ class AsyncCouchbaseSaver(BaseCheckpointSaver):
|
|
|
149
199
|
async for write_doc in serialized_writes_result:
|
|
150
200
|
checkpoint_writes = write_doc.get(self.checkpoint_writes_collection_name, {})
|
|
151
201
|
if "task_id" not in checkpoint_writes:
|
|
152
|
-
|
|
202
|
+
logger.warning("'task_id' is not present in checkpoint_writes")
|
|
153
203
|
else:
|
|
154
204
|
pending_writes.append(
|
|
155
205
|
(
|
|
@@ -162,7 +212,7 @@ class AsyncCouchbaseSaver(BaseCheckpointSaver):
|
|
|
162
212
|
return CheckpointTuple(
|
|
163
213
|
{"configurable": config_values},
|
|
164
214
|
checkpoint,
|
|
165
|
-
self.serde.
|
|
215
|
+
self.serde.loads_typed((doc.get("metadata_type", DEFAULT_METADATA_TYPE), _decode_binary(doc["metadata"]))) if doc.get("metadata") else None,
|
|
166
216
|
(
|
|
167
217
|
{
|
|
168
218
|
"configurable": {
|
|
@@ -177,6 +227,8 @@ class AsyncCouchbaseSaver(BaseCheckpointSaver):
|
|
|
177
227
|
pending_writes,
|
|
178
228
|
)
|
|
179
229
|
|
|
230
|
+
return None
|
|
231
|
+
|
|
180
232
|
async def alist(
|
|
181
233
|
self,
|
|
182
234
|
config: Optional[RunnableConfig],
|
|
@@ -236,7 +288,7 @@ class AsyncCouchbaseSaver(BaseCheckpointSaver):
|
|
|
236
288
|
}
|
|
237
289
|
},
|
|
238
290
|
checkpoint,
|
|
239
|
-
self.serde.
|
|
291
|
+
self.serde.loads_typed((doc.get("metadata_type", DEFAULT_METADATA_TYPE), _decode_binary(doc["metadata"]))) if doc.get("metadata") else None,
|
|
240
292
|
(
|
|
241
293
|
{
|
|
242
294
|
"configurable": {
|
|
@@ -278,15 +330,16 @@ class AsyncCouchbaseSaver(BaseCheckpointSaver):
|
|
|
278
330
|
if serialized_checkpoint:
|
|
279
331
|
serialized_checkpoint = _encode_binary(serialized_checkpoint)
|
|
280
332
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
333
|
+
# Serialize and encode metadata
|
|
334
|
+
metadata_type, metadata_bytes = self.serde.dumps_typed(metadata)
|
|
335
|
+
serialized_metadata = _encode_binary(metadata_bytes) if metadata_bytes else None
|
|
284
336
|
|
|
285
337
|
doc = {
|
|
286
338
|
"parent_checkpoint_id": config["configurable"].get("checkpoint_id"),
|
|
287
339
|
"type": type_,
|
|
288
340
|
"checkpoint": serialized_checkpoint,
|
|
289
|
-
"metadata":
|
|
341
|
+
"metadata": serialized_metadata,
|
|
342
|
+
"metadata_type": metadata_type,
|
|
290
343
|
"thread_id" : thread_id,
|
|
291
344
|
"checkpoint_ns": checkpoint_ns,
|
|
292
345
|
"checkpoint_id": checkpoint_id,
|
|
@@ -294,8 +347,10 @@ class AsyncCouchbaseSaver(BaseCheckpointSaver):
|
|
|
294
347
|
|
|
295
348
|
upsert_key = f"{thread_id}::{checkpoint_ns}::{checkpoint_id}"
|
|
296
349
|
|
|
350
|
+
# ensure bucket connected (idempotent)
|
|
351
|
+
await self.bucket.on_connect()
|
|
297
352
|
collection = self.bucket.scope(self.scope_name).collection(self.checkpoints_collection_name)
|
|
298
|
-
await collection.upsert(upsert_key, (doc), UpsertOptions(timeout=
|
|
353
|
+
await collection.upsert(upsert_key, (doc), UpsertOptions(timeout=DEFAULT_TIMEOUT))
|
|
299
354
|
|
|
300
355
|
return {
|
|
301
356
|
"configurable": {
|
|
@@ -324,6 +379,7 @@ class AsyncCouchbaseSaver(BaseCheckpointSaver):
|
|
|
324
379
|
checkpoint_ns = config["configurable"]["checkpoint_ns"]
|
|
325
380
|
checkpoint_id = config["configurable"]["checkpoint_id"]
|
|
326
381
|
|
|
382
|
+
await self.bucket.on_connect()
|
|
327
383
|
collection = self.bucket.scope(self.scope_name).collection(self.checkpoint_writes_collection_name)
|
|
328
384
|
|
|
329
385
|
for idx, (channel, value) in enumerate(writes):
|
|
@@ -342,4 +398,4 @@ class AsyncCouchbaseSaver(BaseCheckpointSaver):
|
|
|
342
398
|
"type": type_,
|
|
343
399
|
"value": serialized_value,
|
|
344
400
|
}
|
|
345
|
-
await collection.upsert(upsert_key, (doc), UpsertOptions(timeout=
|
|
401
|
+
await collection.upsert(upsert_key, (doc), UpsertOptions(timeout=DEFAULT_TIMEOUT))
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from contextlib import contextmanager
|
|
2
2
|
from datetime import timedelta
|
|
3
3
|
from typing import Any, Dict, Iterator, Optional, Sequence, Tuple
|
|
4
|
+
import logging
|
|
4
5
|
|
|
5
6
|
from langchain_core.runnables import RunnableConfig
|
|
6
7
|
from couchbase.cluster import Cluster
|
|
@@ -19,6 +20,13 @@ from langgraph.checkpoint.base import (
|
|
|
19
20
|
)
|
|
20
21
|
|
|
21
22
|
from .utils import _encode_binary, _decode_binary
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
# Default timeout for database operations
|
|
27
|
+
DEFAULT_TIMEOUT = timedelta(seconds=5)
|
|
28
|
+
# Default serialization type for metadata (for backward compatibility)
|
|
29
|
+
DEFAULT_METADATA_TYPE = "json"
|
|
22
30
|
class CouchbaseSaver(BaseCheckpointSaver):
|
|
23
31
|
"""A checkpoint saver that stores checkpoints in a Couchbase database.
|
|
24
32
|
|
|
@@ -74,11 +82,7 @@ class CouchbaseSaver(BaseCheckpointSaver):
|
|
|
74
82
|
auth = PasswordAuthenticator(cb_username, cb_password)
|
|
75
83
|
options = ClusterOptions(auth)
|
|
76
84
|
cluster = Cluster(cb_conn_str, options)
|
|
77
|
-
cluster.wait_until_ready(
|
|
78
|
-
|
|
79
|
-
cls.cluster = cluster
|
|
80
|
-
cls.bucket_name = bucket_name
|
|
81
|
-
cls.scope_name = scope_name
|
|
85
|
+
cluster.wait_until_ready(DEFAULT_TIMEOUT)
|
|
82
86
|
|
|
83
87
|
saver = CouchbaseSaver(cluster, bucket_name, scope_name, checkpoints_collection_name, checkpoint_writes_collection_name)
|
|
84
88
|
|
|
@@ -118,7 +122,7 @@ class CouchbaseSaver(BaseCheckpointSaver):
|
|
|
118
122
|
except CollectionAlreadyExistsException as _:
|
|
119
123
|
pass
|
|
120
124
|
except Exception as e:
|
|
121
|
-
|
|
125
|
+
logger.exception("Error creating collections")
|
|
122
126
|
raise e
|
|
123
127
|
finally:
|
|
124
128
|
self.checkpoints_collection = self.bucket.scope(self.scope_name).collection(self.checkpoints_collection_name)
|
|
@@ -128,7 +132,7 @@ class CouchbaseSaver(BaseCheckpointSaver):
|
|
|
128
132
|
except CollectionAlreadyExistsException as _:
|
|
129
133
|
pass
|
|
130
134
|
except Exception as e:
|
|
131
|
-
|
|
135
|
+
logger.exception("Error creating collections")
|
|
132
136
|
raise e
|
|
133
137
|
finally:
|
|
134
138
|
self.checkpoint_writes_collection = self.bucket.scope(self.scope_name).collection(self.checkpoint_writes_collection_name)
|
|
@@ -181,7 +185,7 @@ class CouchbaseSaver(BaseCheckpointSaver):
|
|
|
181
185
|
for write_doc in serialized_writes_result:
|
|
182
186
|
checkpoint_writes = write_doc.get(self.checkpoint_writes_collection_name, {})
|
|
183
187
|
if "task_id" not in checkpoint_writes:
|
|
184
|
-
|
|
188
|
+
logger.warning("'task_id' is not present in checkpoint_writes")
|
|
185
189
|
else:
|
|
186
190
|
# Decode and deserialize value data
|
|
187
191
|
value_data = _decode_binary(checkpoint_writes["value"])
|
|
@@ -194,8 +198,8 @@ class CouchbaseSaver(BaseCheckpointSaver):
|
|
|
194
198
|
)
|
|
195
199
|
|
|
196
200
|
# Decode and deserialize metadata
|
|
197
|
-
|
|
198
|
-
metadata = self.serde.
|
|
201
|
+
metadata_data = _decode_binary(doc["metadata"])
|
|
202
|
+
metadata = self.serde.loads_typed((doc.get("metadata_type", DEFAULT_METADATA_TYPE), metadata_data))
|
|
199
203
|
|
|
200
204
|
return CheckpointTuple(
|
|
201
205
|
{"configurable": config_values},
|
|
@@ -215,6 +219,8 @@ class CouchbaseSaver(BaseCheckpointSaver):
|
|
|
215
219
|
pending_writes,
|
|
216
220
|
)
|
|
217
221
|
|
|
222
|
+
return None
|
|
223
|
+
|
|
218
224
|
def list(
|
|
219
225
|
self,
|
|
220
226
|
config: Optional[RunnableConfig],
|
|
@@ -274,7 +280,7 @@ class CouchbaseSaver(BaseCheckpointSaver):
|
|
|
274
280
|
}
|
|
275
281
|
},
|
|
276
282
|
checkpoint,
|
|
277
|
-
self.serde.
|
|
283
|
+
self.serde.loads_typed((doc.get("metadata_type", DEFAULT_METADATA_TYPE), _decode_binary(doc["metadata"]))),
|
|
278
284
|
(
|
|
279
285
|
{
|
|
280
286
|
"configurable": {
|
|
@@ -319,15 +325,15 @@ class CouchbaseSaver(BaseCheckpointSaver):
|
|
|
319
325
|
serialized_checkpoint = _encode_binary(serialized_checkpoint)
|
|
320
326
|
|
|
321
327
|
# Serialize and encode metadata
|
|
322
|
-
metadata_bytes = self.serde.
|
|
323
|
-
if metadata_bytes
|
|
324
|
-
metadata = _encode_binary(metadata_bytes)
|
|
328
|
+
metadata_type, metadata_bytes = self.serde.dumps_typed(metadata)
|
|
329
|
+
serialized_metadata = _encode_binary(metadata_bytes) if metadata_bytes else None
|
|
325
330
|
|
|
326
331
|
doc = {
|
|
327
332
|
"parent_checkpoint_id": config["configurable"].get("checkpoint_id"),
|
|
328
333
|
"type": type_,
|
|
329
334
|
"checkpoint": serialized_checkpoint,
|
|
330
|
-
"metadata":
|
|
335
|
+
"metadata": serialized_metadata,
|
|
336
|
+
"metadata_type": metadata_type,
|
|
331
337
|
"thread_id" : thread_id,
|
|
332
338
|
"checkpoint_ns": checkpoint_ns,
|
|
333
339
|
"checkpoint_id": checkpoint_id,
|
|
@@ -335,7 +341,7 @@ class CouchbaseSaver(BaseCheckpointSaver):
|
|
|
335
341
|
upsert_key = f"{thread_id}::{checkpoint_ns}::{checkpoint_id}"
|
|
336
342
|
|
|
337
343
|
collection = self.checkpoints_collection
|
|
338
|
-
collection.upsert(upsert_key, (doc), UpsertOptions(timeout=
|
|
344
|
+
collection.upsert(upsert_key, (doc), UpsertOptions(timeout=DEFAULT_TIMEOUT))
|
|
339
345
|
|
|
340
346
|
return {
|
|
341
347
|
"configurable": {
|
|
@@ -384,4 +390,4 @@ class CouchbaseSaver(BaseCheckpointSaver):
|
|
|
384
390
|
"type": type_,
|
|
385
391
|
"value": serialized_value,
|
|
386
392
|
}
|
|
387
|
-
collection.upsert(upsert_key,
|
|
393
|
+
collection.upsert(upsert_key, doc, UpsertOptions(timeout=DEFAULT_TIMEOUT))
|
{langgraph_checkpointer_couchbase-1.0.6 → langgraph_checkpointer_couchbase-1.0.8}/pyproject.toml
RENAMED
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "langgraph-checkpointer-couchbase"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.8"
|
|
8
8
|
description = ''
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
@@ -25,11 +25,13 @@ classifiers = [
|
|
|
25
25
|
"Programming Language :: Python :: Implementation :: PyPy",
|
|
26
26
|
]
|
|
27
27
|
dependencies = [
|
|
28
|
-
"couchbase>=4.
|
|
29
|
-
"langgraph>=0.
|
|
30
|
-
"langchain-openai>=
|
|
31
|
-
"pydantic>=2.
|
|
32
|
-
"typing_extensions>=4.
|
|
28
|
+
"couchbase>=4.5.0",
|
|
29
|
+
"langgraph>=1.0.5",
|
|
30
|
+
"langchain-openai>=1.1.3",
|
|
31
|
+
"pydantic>=2.12.5",
|
|
32
|
+
"typing_extensions>=4.15.0",
|
|
33
|
+
"langchain>=1.1.3",
|
|
34
|
+
"python-dotenv>=1.0.0",
|
|
33
35
|
]
|
|
34
36
|
|
|
35
37
|
[project.urls]
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import Literal
|
|
3
|
+
from langchain_core.tools import tool
|
|
4
|
+
from langchain_openai import ChatOpenAI
|
|
5
|
+
from langchain.agents import create_agent
|
|
6
|
+
from langgraph_checkpointer_couchbase import CouchbaseSaver, AsyncCouchbaseSaver
|
|
7
|
+
from dotenv import load_dotenv
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
load_dotenv()
|
|
11
|
+
|
|
12
|
+
@tool
|
|
13
|
+
def get_weather(city: Literal["nyc", "sf"]):
|
|
14
|
+
"""Use this to get weather information."""
|
|
15
|
+
if city == "nyc":
|
|
16
|
+
return "It might be cloudy in nyc"
|
|
17
|
+
elif city == "sf":
|
|
18
|
+
return "It's always sunny in sf"
|
|
19
|
+
else:
|
|
20
|
+
raise AssertionError("Unknown city")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
tools = [get_weather]
|
|
24
|
+
model = ChatOpenAI(model="gpt-5-mini", temperature=0)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def syncTest():
|
|
28
|
+
with CouchbaseSaver.from_conn_info(
|
|
29
|
+
cb_conn_str=os.getenv("CB_CLUSTER"),
|
|
30
|
+
cb_username=os.getenv("CB_USERNAME"),
|
|
31
|
+
cb_password=os.getenv("CB_PASSWORD"),
|
|
32
|
+
bucket_name=os.getenv("CB_BUCKET"),
|
|
33
|
+
scope_name=os.getenv("CB_SCOPE"),
|
|
34
|
+
) as checkpointer:
|
|
35
|
+
graph = create_agent(
|
|
36
|
+
model,
|
|
37
|
+
tools=tools,
|
|
38
|
+
checkpointer=checkpointer,
|
|
39
|
+
)
|
|
40
|
+
config = {"configurable": {"thread_id": "1"}}
|
|
41
|
+
res = graph.invoke({"messages": [("human", "what's the weather in sf")]}, config)
|
|
42
|
+
|
|
43
|
+
latest_checkpoint = checkpointer.get(config)
|
|
44
|
+
latest_checkpoint_tuple = checkpointer.get_tuple(config)
|
|
45
|
+
checkpoint_tuples = list(checkpointer.list(config))
|
|
46
|
+
|
|
47
|
+
print("=== Sync Test Results ===")
|
|
48
|
+
print(f"Response: {res}")
|
|
49
|
+
print(f"Latest checkpoint: {latest_checkpoint}")
|
|
50
|
+
print(f"Latest checkpoint tuple: {latest_checkpoint_tuple}")
|
|
51
|
+
print(f"All checkpoint tuples: {checkpoint_tuples}")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
async def asyncTest():
|
|
55
|
+
async with AsyncCouchbaseSaver.from_conn_info(
|
|
56
|
+
cb_conn_str=os.getenv("CB_CLUSTER"),
|
|
57
|
+
cb_username=os.getenv("CB_USERNAME"),
|
|
58
|
+
cb_password=os.getenv("CB_PASSWORD"),
|
|
59
|
+
bucket_name=os.getenv("CB_BUCKET"),
|
|
60
|
+
scope_name=os.getenv("CB_SCOPE"),
|
|
61
|
+
) as checkpointer:
|
|
62
|
+
graph = create_agent(
|
|
63
|
+
model,
|
|
64
|
+
tools=tools,
|
|
65
|
+
checkpointer=checkpointer,
|
|
66
|
+
)
|
|
67
|
+
config = {"configurable": {"thread_id": "2"}}
|
|
68
|
+
res = await graph.ainvoke(
|
|
69
|
+
{"messages": [("human", "what's the weather in nyc")]}, config
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
latest_checkpoint = await checkpointer.aget(config)
|
|
73
|
+
latest_checkpoint_tuple = await checkpointer.aget_tuple(config)
|
|
74
|
+
checkpoint_tuples = [c async for c in checkpointer.alist(config)]
|
|
75
|
+
|
|
76
|
+
print("=== Async Test Results ===")
|
|
77
|
+
print(f"Response: {res}")
|
|
78
|
+
print(f"Latest checkpoint: {latest_checkpoint}")
|
|
79
|
+
print(f"Latest checkpoint tuple: {latest_checkpoint_tuple}")
|
|
80
|
+
print(f"All checkpoint tuples: {checkpoint_tuples}")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
if __name__ == "__main__":
|
|
84
|
+
print("Running sync test...")
|
|
85
|
+
syncTest()
|
|
86
|
+
print("\nRunning async test...")
|
|
87
|
+
asyncio.run(asyncTest())
|
|
88
|
+
print("\nAll tests completed!")
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
from typing import Literal
|
|
3
|
-
from langchain_core.runnables import ConfigurableField
|
|
4
|
-
from langchain_core.tools import tool
|
|
5
|
-
from langchain_openai import ChatOpenAI
|
|
6
|
-
from langgraph.prebuilt import create_react_agent
|
|
7
|
-
from langgraph_checkpoint_couchbase import CouchbaseSaver, AsyncCouchbaseSaver
|
|
8
|
-
from dotenv import load_dotenv
|
|
9
|
-
import os
|
|
10
|
-
load_dotenv()
|
|
11
|
-
|
|
12
|
-
@tool
|
|
13
|
-
def get_weather(city: Literal["nyc", "sf"]):
|
|
14
|
-
"""Use this to get weather information."""
|
|
15
|
-
if city == "nyc":
|
|
16
|
-
return "It might be cloudy in nyc"
|
|
17
|
-
elif city == "sf":
|
|
18
|
-
return "It's always sunny in sf"
|
|
19
|
-
else:
|
|
20
|
-
raise AssertionError("Unknown city")
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
tools = [get_weather]
|
|
24
|
-
model = ChatOpenAI(model_name="gpt-4o-mini", temperature=0)
|
|
25
|
-
|
|
26
|
-
def syncTest():
|
|
27
|
-
with CouchbaseSaver.from_conn_info(
|
|
28
|
-
cb_conn_str=os.getenv("CB_CLUSTER") or "couchbase://localhost",
|
|
29
|
-
cb_username=os.getenv("CB_USERNAME") or "Administrator",
|
|
30
|
-
cb_password=os.getenv("CB_PASSWORD") or "password",
|
|
31
|
-
bucket_name=os.getenv("CB_BUCKET") or "test",
|
|
32
|
-
scope_name=os.getenv("CB_SCOPE") or "langgraph",
|
|
33
|
-
) as checkpointer:
|
|
34
|
-
graph = create_react_agent(model, tools=tools, checkpointer=checkpointer)
|
|
35
|
-
config = {"configurable": {"thread_id": "1"}}
|
|
36
|
-
res = graph.invoke({"messages": [("human", "what's the weather in sf")]}, config)
|
|
37
|
-
|
|
38
|
-
latest_checkpoint = checkpointer.get(config)
|
|
39
|
-
latest_checkpoint_tuple = checkpointer.get_tuple(config)
|
|
40
|
-
checkpoint_tuples = list(checkpointer.list(config))
|
|
41
|
-
|
|
42
|
-
print(latest_checkpoint)
|
|
43
|
-
print(latest_checkpoint_tuple)
|
|
44
|
-
print(checkpoint_tuples)
|
|
45
|
-
|
|
46
|
-
async def asyncTest():
|
|
47
|
-
async with AsyncCouchbaseSaver.from_conn_info(
|
|
48
|
-
cb_conn_str=os.getenv("CB_CLUSTER") or "couchbase://localhost",
|
|
49
|
-
cb_username=os.getenv("CB_USERNAME") or "Administrator",
|
|
50
|
-
cb_password=os.getenv("CB_PASSWORD") or "password",
|
|
51
|
-
bucket_name=os.getenv("CB_BUCKET") or "test",
|
|
52
|
-
scope_name=os.getenv("CB_SCOPE") or "langgraph",
|
|
53
|
-
) as checkpointer:
|
|
54
|
-
graph = create_react_agent(model, tools=tools, checkpointer=checkpointer)
|
|
55
|
-
config = {"configurable": {"thread_id": "2"}}
|
|
56
|
-
res = await graph.ainvoke(
|
|
57
|
-
{"messages": [("human", "what's the weather in nyc")]}, config
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
latest_checkpoint = await checkpointer.aget(config)
|
|
61
|
-
latest_checkpoint_tuple = await checkpointer.aget_tuple(config)
|
|
62
|
-
checkpoint_tuples = [c async for c in checkpointer.alist(config)]
|
|
63
|
-
|
|
64
|
-
print(latest_checkpoint)
|
|
65
|
-
print(latest_checkpoint_tuple)
|
|
66
|
-
print(checkpoint_tuples)
|
|
67
|
-
|
|
68
|
-
if __name__ == "__main__":
|
|
69
|
-
syncTest()
|
|
70
|
-
asyncio.run(asyncTest())
|
{langgraph_checkpointer_couchbase-1.0.6 → langgraph_checkpointer_couchbase-1.0.8}/.env.example
RENAMED
|
File without changes
|
{langgraph_checkpointer_couchbase-1.0.6 → langgraph_checkpointer_couchbase-1.0.8}/.gitignore
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{langgraph_checkpointer_couchbase-1.0.6 → langgraph_checkpointer_couchbase-1.0.8}/tests/__init__.py
RENAMED
|
File without changes
|