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.
- agentmemory/__init__.py +131 -0
- agentmemory/client.py +491 -0
- agentmemory/defaults.py +12 -0
- agentmemory/exceptions.py +230 -0
- agentmemory/http.py +827 -0
- agentmemory/logger.py +104 -0
- agentmemory/models.py +346 -0
- agentmemory/py.typed +0 -0
- agentmemory/resources/__init__.py +27 -0
- agentmemory/resources/health.py +251 -0
- agentmemory/resources/session.py +897 -0
- agentmemory/resources/user.py +586 -0
- couchbase_agent_memory-1.0.0.dist-info/METADATA +31 -0
- couchbase_agent_memory-1.0.0.dist-info/RECORD +16 -0
- couchbase_agent_memory-1.0.0.dist-info/WHEEL +5 -0
- couchbase_agent_memory-1.0.0.dist-info/top_level.txt +1 -0
agentmemory/__init__.py
ADDED
|
@@ -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")
|
agentmemory/defaults.py
ADDED
|
@@ -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"
|