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.
Files changed (14) hide show
  1. {langgraph_checkpointer_couchbase-1.0.6 → langgraph_checkpointer_couchbase-1.0.8}/.github/workflows/release.yaml +3 -7
  2. {langgraph_checkpointer_couchbase-1.0.6 → langgraph_checkpointer_couchbase-1.0.8}/PKG-INFO +64 -48
  3. {langgraph_checkpointer_couchbase-1.0.6 → langgraph_checkpointer_couchbase-1.0.8}/README.md +56 -42
  4. {langgraph_checkpointer_couchbase-1.0.6 → langgraph_checkpointer_couchbase-1.0.8}/langgraph_checkpointer_couchbase/async_cb_saver.py +75 -19
  5. {langgraph_checkpointer_couchbase-1.0.6 → langgraph_checkpointer_couchbase-1.0.8}/langgraph_checkpointer_couchbase/couchbase_saver.py +23 -17
  6. {langgraph_checkpointer_couchbase-1.0.6 → langgraph_checkpointer_couchbase-1.0.8}/pyproject.toml +8 -6
  7. langgraph_checkpointer_couchbase-1.0.8/tests/agent_e2e_test.py +88 -0
  8. langgraph_checkpointer_couchbase-1.0.6/tests/agent_e2e_test.py +0 -70
  9. {langgraph_checkpointer_couchbase-1.0.6 → langgraph_checkpointer_couchbase-1.0.8}/.env.example +0 -0
  10. {langgraph_checkpointer_couchbase-1.0.6 → langgraph_checkpointer_couchbase-1.0.8}/.gitignore +0 -0
  11. {langgraph_checkpointer_couchbase-1.0.6 → langgraph_checkpointer_couchbase-1.0.8}/LICENSE +0 -0
  12. {langgraph_checkpointer_couchbase-1.0.6 → langgraph_checkpointer_couchbase-1.0.8}/langgraph_checkpointer_couchbase/__init__.py +0 -0
  13. {langgraph_checkpointer_couchbase-1.0.6 → langgraph_checkpointer_couchbase-1.0.8}/langgraph_checkpointer_couchbase/utils.py +0 -0
  14. {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
- branches:
5
- - main
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: v${{ steps.hatch_version_step.outputs.VERSION }}
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.6
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.3.5
22
- Requires-Dist: langchain-openai>=0.3.11
23
- Requires-Dist: langgraph>=0.3.22
24
- Requires-Dist: pydantic>=2.11.1
25
- Requires-Dist: typing-extensions>=4.13.0
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
- # Create the agent with checkpointing
99
- graph = create_react_agent(model, tools=tools, checkpointer=checkpointer)
100
-
101
- # Configure with a unique thread ID
102
- config = {"configurable": {"thread_id": "1"}}
103
-
104
- # Run the agent
105
- res = graph.invoke({"messages": [("human", "what's the weather in sf")]}, config)
106
-
107
- # Retrieve checkpoints
108
- latest_checkpoint = checkpointer.get(config)
109
- latest_checkpoint_tuple = checkpointer.get_tuple(config)
110
- checkpoint_tuples = list(checkpointer.list(config))
111
-
112
- print(latest_checkpoint)
113
- print(latest_checkpoint_tuple)
114
- print(checkpoint_tuples)
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
- async with AsyncCouchbaseSaver.from_conn_info(
125
- cb_conn_str=os.getenv("CB_CLUSTER") or "couchbase://localhost",
126
- cb_username=os.getenv("CB_USERNAME") or "Administrator",
127
- cb_password=os.getenv("CB_PASSWORD") or "password",
128
- bucket_name=os.getenv("CB_BUCKET") or "test",
129
- scope_name=os.getenv("CB_SCOPE") or "langgraph",
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
- # Create the agent with checkpointing
132
- graph = create_react_agent(model, tools=tools, checkpointer=checkpointer)
133
-
134
- # Configure with a unique thread ID
135
- config = {"configurable": {"thread_id": "2"}}
136
-
137
- # Run the agent asynchronously
138
- res = await graph.ainvoke(
139
- {"messages": [("human", "what's the weather in nyc")]}, config
140
- )
141
-
142
- # Retrieve checkpoints asynchronously
143
- latest_checkpoint = await checkpointer.aget(config)
144
- latest_checkpoint_tuple = await checkpointer.aget_tuple(config)
145
- checkpoint_tuples = [c async for c in checkpointer.alist(config)]
146
-
147
- print(latest_checkpoint)
148
- print(latest_checkpoint_tuple)
149
- print(checkpoint_tuples)
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
- # 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)
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
- async with AsyncCouchbaseSaver.from_conn_info(
98
- cb_conn_str=os.getenv("CB_CLUSTER") or "couchbase://localhost",
99
- cb_username=os.getenv("CB_USERNAME") or "Administrator",
100
- cb_password=os.getenv("CB_PASSWORD") or "password",
101
- bucket_name=os.getenv("CB_BUCKET") or "test",
102
- scope_name=os.getenv("CB_SCOPE") or "langgraph",
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
- # Create the agent with checkpointing
105
- graph = create_react_agent(model, tools=tools, checkpointer=checkpointer)
106
-
107
- # Configure with a unique thread ID
108
- config = {"configurable": {"thread_id": "2"}}
109
-
110
- # Run the agent asynchronously
111
- res = await graph.ainvoke(
112
- {"messages": [("human", "what's the weather in nyc")]}, config
113
- )
114
-
115
- # Retrieve checkpoints asynchronously
116
- latest_checkpoint = await checkpointer.aget(config)
117
- latest_checkpoint_tuple = await checkpointer.aget_tuple(config)
118
- checkpoint_tuples = [c async for c in checkpointer.alist(config)]
119
-
120
- print(latest_checkpoint)
121
- print(latest_checkpoint_tuple)
122
- print(checkpoint_tuples)
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
- saver = AsyncCouchbaseSaver(cluster, bucket_name, scope_name, checkpoints_collection_name, checkpoint_writes_collection_name)
73
- cls.bucket = cluster.bucket(bucket_name)
74
- await cls.bucket.on_connect()
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
- saver = AsyncCouchbaseSaver(cluster, bucket_name, scope_name, checkpoints_collection_name, checkpoint_writes_collection_name)
102
- cls.bucket = cluster.bucket(bucket_name)
103
- await cls.bucket.on_connect()
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
- print("Error: 'task_id' is not present in checkpoint_writes")
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.loads(_decode_binary(doc["metadata"])),
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.loads(_decode_binary(doc["metadata"])),
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
- metadata = self.serde.dumps(metadata)
282
- if metadata:
283
- metadata = _encode_binary(metadata)
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": 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=timedelta(seconds=5)))
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=timedelta(seconds=5)))
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(timedelta(seconds=5))
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
- print(f"Error creating collections: {e}")
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
- print(f"Error creating collections: {e}")
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
- print("Error: 'task_id' is not present in checkpoint_writes")
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
- metadata = _decode_binary(doc["metadata"])
198
- metadata = self.serde.loads(metadata)
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.loads(_decode_binary(doc["metadata"])),
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.dumps(metadata)
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": 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=timedelta(seconds=5)))
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, (doc), UpsertOptions(timeout=timedelta(seconds=5)))
393
+ collection.upsert(upsert_key, doc, UpsertOptions(timeout=DEFAULT_TIMEOUT))
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "langgraph-checkpointer-couchbase"
7
- version = "1.0.6"
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.3.5",
29
- "langgraph>=0.3.22",
30
- "langchain-openai>=0.3.11",
31
- "pydantic>=2.11.1",
32
- "typing_extensions>=4.13.0"
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())