couchbase-agent-memory 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,131 @@
1
+ """Couchbase Agent Memory Client SDK.
2
+
3
+ A Python SDK for interacting with the Couchbase Agent Memory server API.
4
+ Provides a hierarchical, resource-based interface for managing users, sessions, and memory blocks.
5
+ """
6
+
7
+ __version__ = "1.0.0"
8
+
9
+ # Main clients
10
+ from agentmemory.client import AgentMemoryClient, AsyncAgentMemoryClient
11
+
12
+ # Resources
13
+ from agentmemory.resources import (
14
+ UserResource,
15
+ AsyncUserResource,
16
+ SessionResource,
17
+ AsyncSessionResource,
18
+ )
19
+
20
+ # Models
21
+ from agentmemory.models import (
22
+ # Enums
23
+ HealthStatus,
24
+ MemoryBlockStatus,
25
+ # User models
26
+ User,
27
+ UserList,
28
+ # Session models
29
+ Session,
30
+ SessionList,
31
+ # Memory models
32
+ ChatMessage,
33
+ MemoryBlock,
34
+ MemoryList,
35
+ AddMemoryResponse,
36
+ DeleteMemoryResponse,
37
+ UpdateMemoryResponse,
38
+ ListMemoriesResponse,
39
+ FilterOptions,
40
+ # Health models
41
+ AsyncBatchProcessorHealthResponse,
42
+ AsyncBatchQueueStats,
43
+ AsyncBatchRateBudget,
44
+ AsyncBatchStatistics,
45
+ HealthResponse,
46
+ ModelsHealth,
47
+ HealthPingResponse,
48
+ )
49
+
50
+ # Exceptions
51
+ from agentmemory.exceptions import (
52
+ AgentMemoryError,
53
+ AuthenticationError,
54
+ NotFoundError,
55
+ ValidationError,
56
+ ConflictError,
57
+ RateLimitError,
58
+ ServiceUnavailableError,
59
+ UpstreamError,
60
+ ServerError,
61
+ ConnectionError,
62
+ TimeoutError,
63
+ ServerDownError,
64
+ ReadTimeoutError,
65
+ )
66
+
67
+ # Logging
68
+ from agentmemory.logger import (
69
+ enable_logging,
70
+ disable_logging,
71
+ set_level,
72
+ is_logging_enabled,
73
+ )
74
+
75
+ __all__ = [
76
+ # Version
77
+ "__version__",
78
+ # Clients
79
+ "AgentMemoryClient",
80
+ "AsyncAgentMemoryClient",
81
+ # Resources
82
+ "UserResource",
83
+ "AsyncUserResource",
84
+ "SessionResource",
85
+ "AsyncSessionResource",
86
+ # Enums
87
+ "HealthStatus",
88
+ "MemoryBlockStatus",
89
+ # User models
90
+ "User",
91
+ "UserList",
92
+ # Session models
93
+ "Session",
94
+ "SessionList",
95
+ # Memory models
96
+ "ChatMessage",
97
+ "MemoryBlock",
98
+ "MemoryList",
99
+ "AddMemoryResponse",
100
+ "DeleteMemoryResponse",
101
+ "UpdateMemoryResponse",
102
+ "ListMemoriesResponse",
103
+ "FilterOptions",
104
+ # Health models
105
+ "AsyncBatchProcessorHealthResponse",
106
+ "AsyncBatchQueueStats",
107
+ "AsyncBatchRateBudget",
108
+ "AsyncBatchStatistics",
109
+ "HealthResponse",
110
+ "ModelsHealth",
111
+ "HealthPingResponse",
112
+ # Exceptions
113
+ "AgentMemoryError",
114
+ "AuthenticationError",
115
+ "NotFoundError",
116
+ "ValidationError",
117
+ "ConflictError",
118
+ "RateLimitError",
119
+ "ServiceUnavailableError",
120
+ "UpstreamError",
121
+ "ServerError",
122
+ "ConnectionError",
123
+ "TimeoutError",
124
+ "ServerDownError",
125
+ "ReadTimeoutError",
126
+ # Logging
127
+ "enable_logging",
128
+ "disable_logging",
129
+ "set_level",
130
+ "is_logging_enabled",
131
+ ]
agentmemory/client.py ADDED
@@ -0,0 +1,491 @@
1
+ """
2
+ Main client class for interacting with the Couchbase Agent Memory server APIs
3
+ Provides AgentMemoryClient and AsyncAgentMemoryClient classes
4
+ """
5
+
6
+ from typing import Optional, Dict, Any, List, Union, Callable
7
+ from urllib.parse import urlsplit, urlunsplit
8
+
9
+ from agentmemory.defaults import (
10
+ DEFAULT_MAX_RETRIES,
11
+ DEFAULT_TIMEOUT,
12
+ )
13
+ from agentmemory.exceptions import ValidationError
14
+ from agentmemory.http import HTTPClient, AsyncHTTPClient
15
+ from agentmemory.logger import logger
16
+ from agentmemory.models import (
17
+ User,
18
+ UserList,
19
+ HealthPingResponse,
20
+ )
21
+ from agentmemory.resources.health import health_ping, async_health_ping, HealthEntity
22
+ from agentmemory.resources.user import UserResource, AsyncUserResource
23
+
24
+
25
+ def _require_non_blank(value: Optional[str], field: str) -> str:
26
+ if value is None or not value.strip():
27
+ raise ValidationError(f"{field} must not be blank")
28
+ if field.endswith("_id") and any(ch in value for ch in "/?#"):
29
+ raise ValidationError(f"{field} must not contain '/', '?', or '#'")
30
+ return value
31
+
32
+
33
+ def _redact_url(url: str) -> str:
34
+ parts = urlsplit(url)
35
+ netloc = parts.netloc.rsplit("@", 1)[-1]
36
+ return urlunsplit((parts.scheme, netloc, parts.path, "", ""))
37
+
38
+
39
+ class AgentMemoryClient:
40
+ """Synchronous client for the Couchbase Agent Memory API.
41
+
42
+ Provides a hierarchical, resource-based interface for managing users,
43
+ sessions, and memory blocks.
44
+ """
45
+
46
+ def __init__(
47
+ self,
48
+ base_url: str,
49
+ token: Optional[Union[str, Callable[[], str]]] = None,
50
+ timeout: float = DEFAULT_TIMEOUT,
51
+ max_retries: int = DEFAULT_MAX_RETRIES,
52
+ client_id: Optional[str] = None,
53
+ verify: Union[bool, str] = True,
54
+ ):
55
+ """Initialize the Couchbase Agent Memory client.
56
+
57
+ Args:
58
+ base_url: Base URL of the Couchbase Agent Memory server
59
+ token: JWT bearer token for authentication. Can be:
60
+ - str: Static token string
61
+ - Callable[[], str]: Function that returns current token (for dynamic refresh)
62
+ - None: If auth is disabled in Couchbase Agent Memory server
63
+ timeout: Request timeout in seconds
64
+ max_retries: Maximum number of retries for failed requests
65
+ client_id: Optional client identifier for request tracking
66
+ verify: SSL certificate verification. Can be:
67
+ - True: Verify SSL certificates (default)
68
+ - False: Disable SSL verification (for self-signed certs in development)
69
+ - str: Path to a CA certificate file to trust
70
+ """
71
+ self.base_url = base_url
72
+ self._http = HTTPClient(
73
+ base_url=base_url,
74
+ token=token,
75
+ timeout=timeout,
76
+ max_retries=max_retries,
77
+ client_id=client_id,
78
+ verify=verify,
79
+ )
80
+ logger.info(
81
+ f"Couchbase Agent Memory client initialized: {_redact_url(base_url)}"
82
+ )
83
+
84
+ def close(self) -> None:
85
+ """Close the client and release resources."""
86
+ self._http.close()
87
+ logger.info("Couchbase Agent Memory client closed")
88
+
89
+ def __enter__(self) -> "AgentMemoryClient":
90
+ return self
91
+
92
+ def __exit__(self, *args) -> None:
93
+ self.close()
94
+
95
+ def __repr__(self) -> str:
96
+ return f"AgentMemoryClient(base_url={self.base_url!r})"
97
+
98
+ # Health
99
+ def health_ping(
100
+ self,
101
+ entity: Optional[HealthEntity] = None,
102
+ ) -> HealthPingResponse:
103
+ """Check health of Couchbase Agent Memory server components.
104
+
105
+ By default, checks all components and returns a combined health status.
106
+ Optionally specify an entity to check only that component.
107
+
108
+ Args:
109
+ entity: Optional specific entity to check. One of:
110
+ - "server": Overall server health
111
+ - "couchbase": Database health
112
+ - "models": Embedding and LLM model services
113
+ - "async-batch-processor": Async batch processor readiness
114
+ - "async-batch-processor-stats": Async batch processor statistics
115
+ (underscore aliases are also accepted)
116
+ - "memory": Memory service health
117
+ If None, checks all entities.
118
+
119
+ Returns:
120
+ HealthPingResponse with health status for checked components
121
+ """
122
+ return health_ping(self._http, entity)
123
+
124
+ # User ops
125
+ def create_user(
126
+ self,
127
+ user_id: str,
128
+ name: str,
129
+ metadata: Optional[Dict[str, Any]] = None,
130
+ ) -> UserResource:
131
+ """Create a new user.
132
+
133
+ Args:
134
+ user_id: Unique identifier for the user
135
+ name: Display name for the user
136
+ metadata: Optional metadata dictionary
137
+
138
+ Returns:
139
+ UserResource for performing user operations
140
+
141
+ Raises:
142
+ ConflictError: If user already exists
143
+ ValidationError: If request is invalid
144
+ """
145
+ _require_non_blank(user_id, "user_id")
146
+ _require_non_blank(name, "name")
147
+
148
+ payload = {
149
+ "user_id": user_id,
150
+ "name": name,
151
+ }
152
+ if metadata is not None:
153
+ payload["metadata"] = metadata
154
+
155
+ user = self._http.post("/users", response_model=User, json=payload)
156
+ logger.info(f"Created user: {user_id} ({name})")
157
+ return UserResource(
158
+ self._http,
159
+ user_id,
160
+ user.name,
161
+ sessions=user.sessions,
162
+ metadata=user.metadata,
163
+ )
164
+
165
+ def get_user(self, user_id: str) -> UserResource:
166
+ """Get a user resource hydrated from the server.
167
+
168
+ Fetches the user record so the returned resource exposes
169
+ name, sessions, and metadata in addition to user_id.
170
+
171
+ Args:
172
+ user_id: User ID
173
+
174
+ Returns:
175
+ UserResource for performing user operations
176
+
177
+ Raises:
178
+ NotFoundError: If user doesn't exist
179
+ """
180
+ _require_non_blank(user_id, "user_id")
181
+
182
+ result = self._http.post("/users/search", json={"user_id": user_id})
183
+ user = User.model_validate(result)
184
+ logger.info(f"Retrieved user: {user_id}")
185
+ return UserResource(
186
+ self._http,
187
+ user_id,
188
+ user.name,
189
+ sessions=user.sessions,
190
+ metadata=user.metadata,
191
+ )
192
+
193
+ def list_users(self) -> UserList:
194
+ """List all users.
195
+
196
+ Returns:
197
+ UserList containing all users and count
198
+ """
199
+ result = self._http.get("/users", response_model=UserList)
200
+ logger.info(f"Listed {result.count} user(s)")
201
+ return result
202
+
203
+ def search_users(
204
+ self,
205
+ user_id: Optional[str] = None,
206
+ name: Optional[str] = None,
207
+ metadata: Optional[Dict[str, Any]] = None,
208
+ ) -> Union[User, List[User]]:
209
+ """Search for users by criteria.
210
+
211
+ At least one search criterion must be provided.
212
+
213
+ Args:
214
+ user_id: Filter by user ID
215
+ name: Filter by name
216
+ metadata: Filter by metadata fields
217
+
218
+ Returns:
219
+ Single User or list of Users matching criteria
220
+
221
+ Raises:
222
+ ValidationError: If no criteria provided
223
+ NotFoundError: If no users found
224
+ """
225
+ if user_id is not None and not user_id.strip():
226
+ raise ValidationError("user_id must not be blank if provided")
227
+ if name is not None and not name.strip():
228
+ raise ValidationError("name must not be blank if provided")
229
+
230
+ payload = {}
231
+ if user_id is not None:
232
+ payload["user_id"] = user_id
233
+ if name is not None:
234
+ payload["name"] = name
235
+ if metadata:
236
+ payload["metadata"] = metadata
237
+ if not payload:
238
+ raise ValidationError(
239
+ "at least one of user_id, name, or metadata must be provided"
240
+ )
241
+
242
+ result = self._http.post("/users/search", json=payload)
243
+
244
+ # Handle single user vs list
245
+ if isinstance(result, list):
246
+ users = [User.model_validate(u) for u in result]
247
+ logger.info(f"Found {len(users)} user(s) matching criteria")
248
+ return users
249
+ user = User.model_validate(result)
250
+ logger.info(f"Found user: {user.id}")
251
+ return user
252
+
253
+ def delete_user(self, user_id: str) -> None:
254
+ """Delete a user and all associated sessions and memories.
255
+
256
+ Args:
257
+ user_id: ID of user to delete
258
+
259
+ Raises:
260
+ NotFoundError: If user doesn't exist
261
+ """
262
+ _require_non_blank(user_id, "user_id")
263
+
264
+ self._http.delete(f"/users/{user_id}")
265
+ logger.info(f"Deleted user: {user_id} and all associated data")
266
+
267
+
268
+ class AsyncAgentMemoryClient:
269
+ """Asynchronous client for the Couchbase Agent Memory API.
270
+
271
+ Provides a hierarchical, resource-based interface for managing users,
272
+ sessions, and memory blocks asynchronously.
273
+ """
274
+
275
+ def __init__(
276
+ self,
277
+ base_url: str,
278
+ token: Optional[Union[str, Callable[[], str]]] = None,
279
+ timeout: float = DEFAULT_TIMEOUT,
280
+ max_retries: int = DEFAULT_MAX_RETRIES,
281
+ client_id: Optional[str] = None,
282
+ verify: Union[bool, str] = True,
283
+ ):
284
+ """Initialize the async Couchbase Agent Memory client.
285
+
286
+ Args:
287
+ base_url: Base URL of the Couchbase Agent Memory server
288
+ token: JWT bearer token for authentication. Can be:
289
+ - str: Static token string
290
+ - Callable[[], str]: Function that returns current token (for dynamic refresh)
291
+ - None: If auth is disabled in Couchbase Agent Memory server
292
+ timeout: Request timeout in seconds
293
+ max_retries: Maximum number of retries for failed requests
294
+ client_id: Optional client identifier for request tracking
295
+ verify: SSL certificate verification. Can be:
296
+ - True: Verify SSL certificates (default)
297
+ - False: Disable SSL verification (for self-signed certs in development)
298
+ - str: Path to a CA certificate file to trust
299
+ """
300
+ self.base_url = base_url
301
+ self._http = AsyncHTTPClient(
302
+ base_url=base_url,
303
+ token=token,
304
+ timeout=timeout,
305
+ max_retries=max_retries,
306
+ client_id=client_id,
307
+ verify=verify,
308
+ )
309
+ logger.info(
310
+ f"Async Couchbase Agent Memory client initialized: {_redact_url(base_url)}"
311
+ )
312
+
313
+ async def close(self) -> None:
314
+ """Close the client and release resources."""
315
+ await self._http.close()
316
+ logger.info("Async Couchbase Agent Memory client closed")
317
+
318
+ async def __aenter__(self) -> "AsyncAgentMemoryClient":
319
+ return self
320
+
321
+ async def __aexit__(self, *args) -> None:
322
+ await self.close()
323
+
324
+ def __repr__(self) -> str:
325
+ return f"AsyncAgentMemoryClient(base_url={self.base_url!r})"
326
+
327
+ # Health
328
+ async def health_ping(
329
+ self,
330
+ entity: Optional[HealthEntity] = None,
331
+ ) -> HealthPingResponse:
332
+ """Check health of Couchbase Agent Memory server components.
333
+
334
+ By default, checks all components and returns a combined health status.
335
+ Optionally specify an entity to check only that component.
336
+
337
+ Args:
338
+ entity: Optional specific entity to check. One of:
339
+ - "server": Overall server health
340
+ - "couchbase": Database health
341
+ - "models": Embedding and LLM model services
342
+ - "async-batch-processor": Async batch processor readiness
343
+ - "async-batch-processor-stats": Async batch processor statistics
344
+ (underscore aliases are also accepted)
345
+ - "memory": Memory service health
346
+ If None, checks all entities.
347
+
348
+ Returns:
349
+ HealthPingResponse with health status for checked components
350
+ """
351
+ return await async_health_ping(self._http, entity)
352
+
353
+ # User ops
354
+ async def create_user(
355
+ self,
356
+ user_id: str,
357
+ name: str,
358
+ metadata: Optional[Dict[str, Any]] = None,
359
+ ) -> AsyncUserResource:
360
+ """Create a new user.
361
+
362
+ Args:
363
+ user_id: Unique identifier for the user
364
+ name: Display name for the user
365
+ metadata: Optional metadata dictionary
366
+
367
+ Returns:
368
+ AsyncUserResource for performing user operations
369
+
370
+ Raises:
371
+ ConflictError: If user already exists
372
+ ValidationError: If request is invalid
373
+ """
374
+ _require_non_blank(user_id, "user_id")
375
+ _require_non_blank(name, "name")
376
+
377
+ payload = {"user_id": user_id, "name": name}
378
+ if metadata is not None:
379
+ payload["metadata"] = metadata
380
+
381
+ user = await self._http.post("/users", response_model=User, json=payload)
382
+ logger.info(f"Created user: {user_id} ({name})")
383
+ return AsyncUserResource(
384
+ self._http,
385
+ user_id,
386
+ user.name,
387
+ sessions=user.sessions,
388
+ metadata=user.metadata,
389
+ )
390
+
391
+ async def get_user(self, user_id: str) -> AsyncUserResource:
392
+ """Get a user resource hydrated from the server.
393
+
394
+ Fetches the user record so the returned resource exposes
395
+ name, sessions, and metadata in addition to user_id.
396
+
397
+ Args:
398
+ user_id: User ID
399
+
400
+ Returns:
401
+ AsyncUserResource for performing user operations
402
+
403
+ Raises:
404
+ NotFoundError: If user doesn't exist
405
+ """
406
+ _require_non_blank(user_id, "user_id")
407
+
408
+ result = await self._http.post("/users/search", json={"user_id": user_id})
409
+ user = User.model_validate(result)
410
+ logger.info(f"Retrieved user: {user_id}")
411
+ return AsyncUserResource(
412
+ self._http,
413
+ user_id,
414
+ user.name,
415
+ sessions=user.sessions,
416
+ metadata=user.metadata,
417
+ )
418
+
419
+ async def list_users(self) -> UserList:
420
+ """List all users.
421
+
422
+ Returns:
423
+ UserList containing all users and count
424
+ """
425
+ result = await self._http.get("/users", response_model=UserList)
426
+ logger.info(f"Listed {result.count} user(s)")
427
+ return result
428
+
429
+ async def search_users(
430
+ self,
431
+ user_id: Optional[str] = None,
432
+ name: Optional[str] = None,
433
+ metadata: Optional[Dict[str, Any]] = None,
434
+ ) -> Union[User, List[User]]:
435
+ """Search for users by criteria.
436
+
437
+ At least one search criterion must be provided.
438
+
439
+ Args:
440
+ user_id: Filter by user ID
441
+ name: Filter by name
442
+ metadata: Filter by metadata fields
443
+
444
+ Returns:
445
+ Single User or list of Users matching criteria
446
+
447
+ Raises:
448
+ ValidationError: If no criteria provided
449
+ NotFoundError: If no users found
450
+ """
451
+ if user_id is not None and not user_id.strip():
452
+ raise ValidationError("user_id must not be blank if provided")
453
+ if name is not None and not name.strip():
454
+ raise ValidationError("name must not be blank if provided")
455
+
456
+ payload = {}
457
+ if user_id is not None:
458
+ payload["user_id"] = user_id
459
+ if name is not None:
460
+ payload["name"] = name
461
+ if metadata:
462
+ payload["metadata"] = metadata
463
+ if not payload:
464
+ raise ValidationError(
465
+ "at least one of user_id, name, or metadata must be provided"
466
+ )
467
+
468
+ result = await self._http.post("/users/search", json=payload)
469
+
470
+ # Handle single user vs list
471
+ if isinstance(result, list):
472
+ users = [User.model_validate(u) for u in result]
473
+ logger.info(f"Found {len(users)} user(s) matching criteria")
474
+ return users
475
+ user = User.model_validate(result)
476
+ logger.info(f"Found user: {user.id}")
477
+ return user
478
+
479
+ async def delete_user(self, user_id: str) -> None:
480
+ """Delete a user and all associated sessions and memories.
481
+
482
+ Args:
483
+ user_id: ID of user to delete
484
+
485
+ Raises:
486
+ NotFoundError: If user doesn't exist
487
+ """
488
+ _require_non_blank(user_id, "user_id")
489
+
490
+ await self._http.delete(f"/users/{user_id}")
491
+ logger.info(f"Deleted user: {user_id} and all associated data")
@@ -0,0 +1,12 @@
1
+ DEFAULT_TIMEOUT = 30.0
2
+ DEFAULT_MAX_RETRIES = 3
3
+ DEFAULT_RETRY_DELAY = 0.5
4
+ DEFAULT_HEALTH_CHECK_TIMEOUT = 5.0
5
+
6
+ DEFAULT_LIST_MEMORIES_LIMIT = 20
7
+ DEFAULT_LIST_MEMORIES_OFFSET = 0
8
+ DEFAULT_LIST_MEMORIES_MAX_LIMIT = 200
9
+ DEFAULT_MEMORY_ORDER_BY = "ingested_at"
10
+ MEMORY_ORDER_BY_FIELDS = ("ingested_at", "created_at")
11
+
12
+ DEFAULT_LOG_FORMAT = "[agentmemory] %(levelname)s: %(message)s"