smartmemory-client 0.5.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.
- smartmemory_client/__init__.py +61 -0
- smartmemory_client/client.py +3279 -0
- smartmemory_client/models/__init__.py +10 -0
- smartmemory_client/models/conversation.py +29 -0
- smartmemory_client/models/memory_item.py +94 -0
- smartmemory_client-0.5.0.dist-info/METADATA +547 -0
- smartmemory_client-0.5.0.dist-info/RECORD +10 -0
- smartmemory_client-0.5.0.dist-info/WHEEL +5 -0
- smartmemory_client-0.5.0.dist-info/licenses/LICENSE +21 -0
- smartmemory_client-0.5.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,3279 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SmartMemory HTTP Client
|
|
3
|
+
|
|
4
|
+
A clean, manually-maintained HTTP client for the SmartMemory Service API.
|
|
5
|
+
|
|
6
|
+
Features:
|
|
7
|
+
- JWT authentication with automatic token handling
|
|
8
|
+
- Type-safe operations with Pydantic models
|
|
9
|
+
- Comprehensive error handling
|
|
10
|
+
- Full API coverage (CRUD, search, ingestion, links, etc.)
|
|
11
|
+
|
|
12
|
+
For more information, see: https://github.com/smartmemory/smart-memory-client
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
import os
|
|
17
|
+
from typing import Any, Dict, List, Optional, Union
|
|
18
|
+
import httpx
|
|
19
|
+
|
|
20
|
+
# Use local model instead of core dependency
|
|
21
|
+
from smartmemory_client.models.memory_item import MemoryItem
|
|
22
|
+
from smartmemory_client.models.conversation import ConversationContextModel
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SmartMemoryClientError(Exception):
|
|
28
|
+
"""Base exception for SmartMemory client errors"""
|
|
29
|
+
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SmartMemoryClient:
|
|
34
|
+
"""
|
|
35
|
+
SmartMemory HTTP Client
|
|
36
|
+
|
|
37
|
+
A clean, manually-maintained HTTP client for the SmartMemory Service API.
|
|
38
|
+
|
|
39
|
+
Features:
|
|
40
|
+
- JWT authentication with automatic token handling
|
|
41
|
+
- API key authentication for automation/scripts
|
|
42
|
+
- Type-safe operations with Pydantic models
|
|
43
|
+
- Comprehensive error handling
|
|
44
|
+
- Full API coverage
|
|
45
|
+
|
|
46
|
+
Usage:
|
|
47
|
+
```python
|
|
48
|
+
from smartmemory_client import SmartMemoryClient
|
|
49
|
+
|
|
50
|
+
# With API key (for automation/scripts)
|
|
51
|
+
client = SmartMemoryClient(
|
|
52
|
+
base_url="http://localhost:9001",
|
|
53
|
+
api_key="sk_your_api_key"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# With JWT token (if you already have one)
|
|
57
|
+
client = SmartMemoryClient(
|
|
58
|
+
base_url="http://localhost:9001",
|
|
59
|
+
token="eyJ..."
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# With login (interactive flow)
|
|
63
|
+
client = SmartMemoryClient(base_url="http://localhost:9001")
|
|
64
|
+
client.login(email="user@example.com", password="secret")
|
|
65
|
+
|
|
66
|
+
# Add memory
|
|
67
|
+
item_id = client.add("This is a test memory")
|
|
68
|
+
|
|
69
|
+
# Search
|
|
70
|
+
results = client.search("test", top_k=5)
|
|
71
|
+
|
|
72
|
+
# Ingest with full pipeline
|
|
73
|
+
result = client.ingest(
|
|
74
|
+
content="Complex content",
|
|
75
|
+
extractor_name="llm",
|
|
76
|
+
context={"key": "value"}
|
|
77
|
+
)
|
|
78
|
+
```
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
base_url: Optional[str] = None,
|
|
84
|
+
api_key: Optional[str] = None,
|
|
85
|
+
token: Optional[str] = None,
|
|
86
|
+
timeout: float = 30.0,
|
|
87
|
+
verify_ssl: bool = True,
|
|
88
|
+
workspace_id: Optional[str] = None,
|
|
89
|
+
team_id: Optional[
|
|
90
|
+
str
|
|
91
|
+
] = None, # deprecated alias for workspace_id, removed in v0.5.0
|
|
92
|
+
):
|
|
93
|
+
"""
|
|
94
|
+
Initialize SmartMemory client wrapper.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
base_url: Base URL of the SmartMemory service
|
|
98
|
+
api_key: API key (sk_...) for automation (or set SMARTMEMORY_API_KEY env var)
|
|
99
|
+
token: JWT token for authentication (or set SMARTMEMORY_TOKEN env var)
|
|
100
|
+
timeout: Request timeout in seconds
|
|
101
|
+
verify_ssl: Whether to verify SSL certificates
|
|
102
|
+
workspace_id: Workspace ID for multi-tenant isolation (preferred)
|
|
103
|
+
team_id: Deprecated alias for workspace_id. Removed in v0.5.0.
|
|
104
|
+
|
|
105
|
+
Note:
|
|
106
|
+
Provide either api_key OR token, not both. If neither provided,
|
|
107
|
+
use login() method to authenticate.
|
|
108
|
+
"""
|
|
109
|
+
# Determine base URL from parameter or environment
|
|
110
|
+
if base_url is None:
|
|
111
|
+
host = os.getenv("SMARTMEMORY_CLIENT_HOST") or os.getenv(
|
|
112
|
+
"SMARTMEMORY_SERVER_HOST", "localhost"
|
|
113
|
+
)
|
|
114
|
+
if host in ("0.0.0.0", "::"):
|
|
115
|
+
host = "localhost"
|
|
116
|
+
try:
|
|
117
|
+
port = int(os.getenv("SMARTMEMORY_SERVER_PORT", "9001"))
|
|
118
|
+
except Exception:
|
|
119
|
+
port = 9001
|
|
120
|
+
base_url = f"http://{host}:{port}"
|
|
121
|
+
|
|
122
|
+
self.base_url = base_url
|
|
123
|
+
self.timeout = timeout
|
|
124
|
+
self.verify_ssl = verify_ssl
|
|
125
|
+
|
|
126
|
+
# Store tokens separately for clarity
|
|
127
|
+
self._api_key: Optional[str] = None
|
|
128
|
+
self._token: Optional[str] = None
|
|
129
|
+
self._refresh_token: Optional[str] = None
|
|
130
|
+
|
|
131
|
+
# Resolve auth from parameters or environment
|
|
132
|
+
# Priority: explicit param > env var
|
|
133
|
+
resolved_api_key = api_key or os.getenv("SMARTMEMORY_API_KEY")
|
|
134
|
+
resolved_token = token or os.getenv("SMARTMEMORY_TOKEN")
|
|
135
|
+
|
|
136
|
+
# Use token if provided, otherwise api_key
|
|
137
|
+
if resolved_token:
|
|
138
|
+
self._token = resolved_token
|
|
139
|
+
logger.info(f"Using JWT token (length: {len(resolved_token)})")
|
|
140
|
+
elif resolved_api_key:
|
|
141
|
+
self._api_key = resolved_api_key
|
|
142
|
+
logger.info(f"Using API key: {resolved_api_key[:10]}...")
|
|
143
|
+
else:
|
|
144
|
+
logger.warning("No auth credentials provided. Use login() to authenticate.")
|
|
145
|
+
|
|
146
|
+
# Resolve workspace_id; team_id is a deprecated alias (removed in v0.5.0).
|
|
147
|
+
# Warn only when team_id is actually used as the fallback (workspace_id not provided).
|
|
148
|
+
self.team_id = (
|
|
149
|
+
workspace_id
|
|
150
|
+
or team_id
|
|
151
|
+
or os.getenv("SMARTMEMORY_WORKSPACE_ID")
|
|
152
|
+
or os.getenv("SMARTMEMORY_TEAM_ID")
|
|
153
|
+
or "team_default_demo"
|
|
154
|
+
)
|
|
155
|
+
if team_id is not None and not workspace_id:
|
|
156
|
+
import warnings
|
|
157
|
+
|
|
158
|
+
warnings.warn(
|
|
159
|
+
"The 'team_id' parameter is deprecated and will be removed in v0.5.0. "
|
|
160
|
+
"Use 'workspace_id' instead.",
|
|
161
|
+
DeprecationWarning,
|
|
162
|
+
stacklevel=2,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Build default headers (auth header added dynamically)
|
|
166
|
+
self._base_headers = {
|
|
167
|
+
"Content-Type": "application/json",
|
|
168
|
+
"X-Workspace-Id": self.team_id,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if self.is_authenticated:
|
|
172
|
+
logger.info(
|
|
173
|
+
f"SmartMemoryClient initialized with authentication. "
|
|
174
|
+
f"Base URL: {self.base_url}, Team ID: {self.team_id}"
|
|
175
|
+
)
|
|
176
|
+
else:
|
|
177
|
+
logger.warning(
|
|
178
|
+
"SmartMemoryClient initialized WITHOUT authentication - "
|
|
179
|
+
"most endpoints will fail. Call login() or provide api_key/token."
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
@property
|
|
183
|
+
def is_authenticated(self) -> bool:
|
|
184
|
+
"""Check if client has valid authentication credentials."""
|
|
185
|
+
return bool(self._token or self._api_key)
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def headers(self) -> Dict[str, str]:
|
|
189
|
+
"""Get headers with current auth token."""
|
|
190
|
+
headers = self._base_headers.copy()
|
|
191
|
+
if self._token:
|
|
192
|
+
headers["Authorization"] = f"Bearer {self._token}"
|
|
193
|
+
elif self._api_key:
|
|
194
|
+
headers["Authorization"] = f"Bearer {self._api_key}"
|
|
195
|
+
return headers
|
|
196
|
+
|
|
197
|
+
# Legacy property for backwards compatibility
|
|
198
|
+
@property
|
|
199
|
+
def api_key(self) -> Optional[str]:
|
|
200
|
+
"""Get current auth credential (token or api_key)."""
|
|
201
|
+
return self._token or self._api_key
|
|
202
|
+
|
|
203
|
+
def refresh_token(
|
|
204
|
+
self, refresh_token_value: Optional[str] = None
|
|
205
|
+
) -> Dict[str, Any]:
|
|
206
|
+
"""
|
|
207
|
+
Refresh the JWT access token using the refresh token.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
refresh_token_value: Refresh token to use. If not provided, uses internally stored token.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Dict with new tokens
|
|
214
|
+
|
|
215
|
+
Raises:
|
|
216
|
+
SmartMemoryClientError: If refresh fails or no refresh token available
|
|
217
|
+
"""
|
|
218
|
+
token_to_use = refresh_token_value or self._refresh_token
|
|
219
|
+
if not token_to_use:
|
|
220
|
+
raise SmartMemoryClientError(
|
|
221
|
+
"No refresh token available. Call login() first or provide refresh_token."
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
body = {"refresh_token": token_to_use}
|
|
225
|
+
result = self._request("POST", "/auth/refresh", json_body=body)
|
|
226
|
+
|
|
227
|
+
if "access_token" in result:
|
|
228
|
+
self._token = result["access_token"]
|
|
229
|
+
if "refresh_token" in result:
|
|
230
|
+
self._refresh_token = result["refresh_token"]
|
|
231
|
+
|
|
232
|
+
logger.info("Token refreshed successfully")
|
|
233
|
+
return result
|
|
234
|
+
|
|
235
|
+
def logout(self) -> None:
|
|
236
|
+
"""
|
|
237
|
+
Logout user and clear authentication tokens.
|
|
238
|
+
|
|
239
|
+
Calls the server logout endpoint and clears local state.
|
|
240
|
+
"""
|
|
241
|
+
try:
|
|
242
|
+
self._request("POST", "/auth/logout")
|
|
243
|
+
except Exception:
|
|
244
|
+
pass # Ignore errors on logout - still clear local state
|
|
245
|
+
|
|
246
|
+
self._token = None
|
|
247
|
+
self._refresh_token = None
|
|
248
|
+
self._api_key = None
|
|
249
|
+
logger.info("Logged out - credentials cleared")
|
|
250
|
+
|
|
251
|
+
def health_check(self) -> Dict[str, Any]:
|
|
252
|
+
"""
|
|
253
|
+
Check the health status of the SmartMemory service.
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
Health status information
|
|
257
|
+
|
|
258
|
+
Example:
|
|
259
|
+
```python
|
|
260
|
+
status = client.health_check()
|
|
261
|
+
print(status) # {'status': 'healthy'}
|
|
262
|
+
```
|
|
263
|
+
"""
|
|
264
|
+
try:
|
|
265
|
+
response = httpx.get(
|
|
266
|
+
f"{self.base_url}/health", headers=self.headers, timeout=self.timeout
|
|
267
|
+
)
|
|
268
|
+
response.raise_for_status()
|
|
269
|
+
return {"status": "healthy"}
|
|
270
|
+
except httpx.HTTPStatusError as e:
|
|
271
|
+
raise SmartMemoryClientError(f"Health check failed: {e}")
|
|
272
|
+
except Exception as e:
|
|
273
|
+
raise SmartMemoryClientError(f"Health check failed: {str(e)}")
|
|
274
|
+
|
|
275
|
+
def add(
|
|
276
|
+
self,
|
|
277
|
+
item: Union[str, MemoryItem, Dict[str, Any]],
|
|
278
|
+
memory_type: str = "semantic",
|
|
279
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
280
|
+
use_pipeline: bool = True,
|
|
281
|
+
conversation_context: Optional[
|
|
282
|
+
Union[ConversationContextModel, Dict[str, Any]]
|
|
283
|
+
] = None,
|
|
284
|
+
) -> str:
|
|
285
|
+
"""
|
|
286
|
+
Add a memory item to the system.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
item: Memory content (string) or MemoryItem object
|
|
290
|
+
memory_type: Type of memory (semantic, episodic, procedural, working)
|
|
291
|
+
metadata: Additional metadata for the memory
|
|
292
|
+
use_pipeline: Whether to run full extraction pipeline (default: True)
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
Memory item ID
|
|
296
|
+
|
|
297
|
+
Example:
|
|
298
|
+
```python
|
|
299
|
+
# Simple add
|
|
300
|
+
item_id = client.add("Remember this")
|
|
301
|
+
|
|
302
|
+
# With pipeline disabled (faster)
|
|
303
|
+
item_id = client.add("Quick note", use_pipeline=False)
|
|
304
|
+
|
|
305
|
+
# With metadata
|
|
306
|
+
item_id = client.add(
|
|
307
|
+
"Important fact",
|
|
308
|
+
metadata={"source": "user", "priority": "high"},
|
|
309
|
+
use_pipeline=True
|
|
310
|
+
)
|
|
311
|
+
```
|
|
312
|
+
"""
|
|
313
|
+
# Handle different input types
|
|
314
|
+
if isinstance(item, str):
|
|
315
|
+
content = item
|
|
316
|
+
elif isinstance(item, MemoryItem):
|
|
317
|
+
content = item.content
|
|
318
|
+
memory_type = item.memory_type or memory_type
|
|
319
|
+
metadata = metadata or item.metadata
|
|
320
|
+
elif isinstance(item, dict):
|
|
321
|
+
content = item.get("content", str(item))
|
|
322
|
+
memory_type = item.get("memory_type", memory_type)
|
|
323
|
+
metadata = metadata or item.get("metadata")
|
|
324
|
+
else:
|
|
325
|
+
content = str(item)
|
|
326
|
+
|
|
327
|
+
body_dict = {
|
|
328
|
+
"content": content,
|
|
329
|
+
"memory_type": memory_type,
|
|
330
|
+
"metadata": metadata or {},
|
|
331
|
+
"use_pipeline": use_pipeline,
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if conversation_context:
|
|
335
|
+
import dataclasses
|
|
336
|
+
|
|
337
|
+
if isinstance(conversation_context, ConversationContextModel):
|
|
338
|
+
body_dict["conversation_context"] = dataclasses.asdict(
|
|
339
|
+
conversation_context
|
|
340
|
+
)
|
|
341
|
+
elif dataclasses.is_dataclass(conversation_context) and not isinstance(
|
|
342
|
+
conversation_context, type
|
|
343
|
+
):
|
|
344
|
+
# Handle core ConversationContext or any other dataclass passed directly
|
|
345
|
+
body_dict["conversation_context"] = dataclasses.asdict(
|
|
346
|
+
conversation_context
|
|
347
|
+
)
|
|
348
|
+
else:
|
|
349
|
+
body_dict["conversation_context"] = conversation_context
|
|
350
|
+
|
|
351
|
+
try:
|
|
352
|
+
result = self._request("POST", "/memory/add", json_body=body_dict)
|
|
353
|
+
except SmartMemoryClientError as e:
|
|
354
|
+
if "401" in str(e):
|
|
355
|
+
logger.warning(
|
|
356
|
+
"Authentication required for add. Set SMARTMEMORY_API_KEY environment variable."
|
|
357
|
+
)
|
|
358
|
+
raise
|
|
359
|
+
|
|
360
|
+
if not result:
|
|
361
|
+
raise SmartMemoryClientError("Failed to add memory")
|
|
362
|
+
|
|
363
|
+
if isinstance(result, dict):
|
|
364
|
+
return result.get("id")
|
|
365
|
+
elif hasattr(result, "id"):
|
|
366
|
+
return result.id
|
|
367
|
+
else:
|
|
368
|
+
raise SmartMemoryClientError(f"Unexpected response format: {result}")
|
|
369
|
+
|
|
370
|
+
def get(self, item_id: str) -> Optional[MemoryItem]:
|
|
371
|
+
"""
|
|
372
|
+
Retrieve a memory item by ID.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
item_id: Memory item ID
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
MemoryItem object or None if not found
|
|
379
|
+
|
|
380
|
+
Example:
|
|
381
|
+
```python
|
|
382
|
+
memory = client.get("item_123")
|
|
383
|
+
if memory:
|
|
384
|
+
print(memory.content)
|
|
385
|
+
```
|
|
386
|
+
"""
|
|
387
|
+
try:
|
|
388
|
+
response = self._request("GET", f"/memory/{item_id}")
|
|
389
|
+
|
|
390
|
+
if response is None:
|
|
391
|
+
return None
|
|
392
|
+
|
|
393
|
+
# Convert response to MemoryItem using factory method
|
|
394
|
+
return MemoryItem.from_dict(response)
|
|
395
|
+
except Exception as e:
|
|
396
|
+
logger.error(f"Error getting memory {item_id}: {e}")
|
|
397
|
+
return None
|
|
398
|
+
|
|
399
|
+
def search(
|
|
400
|
+
self,
|
|
401
|
+
query: str,
|
|
402
|
+
top_k: int = 5,
|
|
403
|
+
memory_type: Optional[str] = None,
|
|
404
|
+
use_ssg: Optional[bool] = None,
|
|
405
|
+
enable_hybrid: Optional[bool] = True,
|
|
406
|
+
channel_weights: Optional[Dict[str, float]] = None,
|
|
407
|
+
multi_hop: bool = False,
|
|
408
|
+
max_hops: int = 3,
|
|
409
|
+
budget_ms: int = 1500,
|
|
410
|
+
) -> List[MemoryItem]:
|
|
411
|
+
"""
|
|
412
|
+
Search for memory items using semantic matching.
|
|
413
|
+
|
|
414
|
+
Args:
|
|
415
|
+
query: Search query
|
|
416
|
+
top_k: Maximum number of results
|
|
417
|
+
memory_type: Type of memory to search (optional)
|
|
418
|
+
use_ssg: Use Similarity Graph Traversal for better multi-hop reasoning (optional)
|
|
419
|
+
If None, uses config default. If True, uses SSG. If False, uses basic vector search.
|
|
420
|
+
enable_hybrid: Enable hybrid retrieval (vector + keyword search with RRF fusion).
|
|
421
|
+
Default: True. Set to False for vector-only search.
|
|
422
|
+
channel_weights: Per-channel weight multipliers for RRF fusion (CORE-SEARCH-2a).
|
|
423
|
+
Keys: entity-graph, ssg-traversal, semantic, regex-text, contains, keyword-bm25.
|
|
424
|
+
Values: float multipliers (default varies by channel).
|
|
425
|
+
|
|
426
|
+
Returns:
|
|
427
|
+
List of MemoryItem objects
|
|
428
|
+
|
|
429
|
+
Example:
|
|
430
|
+
```python
|
|
431
|
+
# Simple search (hybrid enabled by default)
|
|
432
|
+
results = client.search("AI concepts", top_k=10)
|
|
433
|
+
|
|
434
|
+
# Search with SSG for better multi-hop reasoning
|
|
435
|
+
results = client.search("AI concepts", top_k=10, use_ssg=True)
|
|
436
|
+
|
|
437
|
+
# Vector-only search (disable hybrid)
|
|
438
|
+
results = client.search("AI concepts", enable_hybrid=False)
|
|
439
|
+
|
|
440
|
+
# Search specific memory type
|
|
441
|
+
results = client.search("conversation", memory_type="episodic")
|
|
442
|
+
|
|
443
|
+
for item in results:
|
|
444
|
+
print(f"{item.item_id}: {item.content}")
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
Note:
|
|
448
|
+
user_id is automatically determined from the JWT token.
|
|
449
|
+
No need to pass it as a parameter.
|
|
450
|
+
"""
|
|
451
|
+
# The FastAPI route expects a top-level SearchRequest body
|
|
452
|
+
body_dict: Dict[str, Any] = {
|
|
453
|
+
"query": query,
|
|
454
|
+
"top_k": top_k,
|
|
455
|
+
"enable_hybrid": enable_hybrid,
|
|
456
|
+
}
|
|
457
|
+
if memory_type is not None:
|
|
458
|
+
body_dict["memory_type"] = memory_type
|
|
459
|
+
if use_ssg is not None:
|
|
460
|
+
body_dict["use_ssg"] = use_ssg
|
|
461
|
+
if channel_weights is not None:
|
|
462
|
+
body_dict["channel_weights"] = channel_weights
|
|
463
|
+
if multi_hop:
|
|
464
|
+
body_dict["multi_hop"] = True
|
|
465
|
+
body_dict["max_hops"] = max_hops
|
|
466
|
+
body_dict["budget_ms"] = budget_ms
|
|
467
|
+
|
|
468
|
+
# SELF-IMPROVE-6: use _request_raw to capture X-Search-Session-Id header
|
|
469
|
+
import httpx
|
|
470
|
+
|
|
471
|
+
url = f"{self.base_url}/memory/search"
|
|
472
|
+
req_headers = {"X-Workspace-Id": self.team_id}
|
|
473
|
+
if self.api_key:
|
|
474
|
+
req_headers["Authorization"] = f"Bearer {self.api_key}"
|
|
475
|
+
|
|
476
|
+
try:
|
|
477
|
+
response = httpx.request(
|
|
478
|
+
"POST",
|
|
479
|
+
url,
|
|
480
|
+
json=body_dict,
|
|
481
|
+
headers=req_headers,
|
|
482
|
+
timeout=self.timeout,
|
|
483
|
+
)
|
|
484
|
+
response.raise_for_status()
|
|
485
|
+
self._last_search_session_id = response.headers.get("X-Search-Session-Id")
|
|
486
|
+
response_data = response.json()
|
|
487
|
+
except httpx.HTTPStatusError as e:
|
|
488
|
+
error_detail = e.response.text if hasattr(e, "response") else str(e)
|
|
489
|
+
raise SmartMemoryClientError(
|
|
490
|
+
f"Request failed: {e} - Detail: {error_detail}"
|
|
491
|
+
)
|
|
492
|
+
except Exception as e:
|
|
493
|
+
raise SmartMemoryClientError(f"Request failed: {str(e)}")
|
|
494
|
+
|
|
495
|
+
if not response_data:
|
|
496
|
+
return []
|
|
497
|
+
|
|
498
|
+
# Convert response to MemoryItem objects
|
|
499
|
+
results: List[MemoryItem] = []
|
|
500
|
+
response_list = (
|
|
501
|
+
response_data if isinstance(response_data, list) else [response_data]
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
for item_data in response_list:
|
|
505
|
+
if hasattr(item_data, "to_dict"):
|
|
506
|
+
item_dict = item_data.to_dict()
|
|
507
|
+
elif isinstance(item_data, dict):
|
|
508
|
+
item_dict = item_data
|
|
509
|
+
else:
|
|
510
|
+
continue
|
|
511
|
+
|
|
512
|
+
# Use factory method for consistent parsing
|
|
513
|
+
results.append(MemoryItem.from_dict(item_dict))
|
|
514
|
+
|
|
515
|
+
return results
|
|
516
|
+
|
|
517
|
+
@property
|
|
518
|
+
def last_search_session_id(self) -> Optional[str]:
|
|
519
|
+
"""Return the search_session_id from the most recent search() call.
|
|
520
|
+
|
|
521
|
+
Use this with submit_result_feedback() to report which results were used.
|
|
522
|
+
"""
|
|
523
|
+
return getattr(self, "_last_search_session_id", None)
|
|
524
|
+
|
|
525
|
+
def get_working_context(
|
|
526
|
+
self,
|
|
527
|
+
session_id: str,
|
|
528
|
+
query: str,
|
|
529
|
+
k: int = 20,
|
|
530
|
+
max_tokens: Optional[int] = None,
|
|
531
|
+
strategy: Optional[str] = None,
|
|
532
|
+
) -> Dict[str, Any]:
|
|
533
|
+
"""Request a surfacing response from ``POST /memory/context``.
|
|
534
|
+
|
|
535
|
+
Replacement for the legacy ``memory_recall`` surface (CORE-MEMORY-DYNAMICS-1 M1a).
|
|
536
|
+
Response shape matches ``context-api-contract.json``.
|
|
537
|
+
|
|
538
|
+
Args:
|
|
539
|
+
session_id: Session scope for anchor resolution.
|
|
540
|
+
query: Natural-language query for the surfacing turn.
|
|
541
|
+
k: Item-count cap on returned items (1-100).
|
|
542
|
+
max_tokens: Token budget cap; None disables.
|
|
543
|
+
strategy: Override surfacing strategy; None lets the router decide.
|
|
544
|
+
|
|
545
|
+
Returns:
|
|
546
|
+
Dict with ``decision_id``, ``items``, ``drift_warnings``,
|
|
547
|
+
``strategy_used``, ``tokens_used``, ``tokens_budget``,
|
|
548
|
+
``deprecation`` keys (see contract).
|
|
549
|
+
|
|
550
|
+
Raises:
|
|
551
|
+
SmartMemoryClientError: Server returned 4xx/5xx (including
|
|
552
|
+
``budget_too_small``) or a transport error occurred.
|
|
553
|
+
"""
|
|
554
|
+
body: Dict[str, Any] = {
|
|
555
|
+
"session_id": session_id,
|
|
556
|
+
"query": query,
|
|
557
|
+
"k": k,
|
|
558
|
+
}
|
|
559
|
+
if max_tokens is not None:
|
|
560
|
+
body["max_tokens"] = max_tokens
|
|
561
|
+
if strategy is not None:
|
|
562
|
+
body["strategy"] = strategy
|
|
563
|
+
return self._request("POST", "/memory/context", json_body=body)
|
|
564
|
+
|
|
565
|
+
def search_advanced(
|
|
566
|
+
self,
|
|
567
|
+
query: str,
|
|
568
|
+
algorithm: str = "query_traversal",
|
|
569
|
+
max_results: int = 15,
|
|
570
|
+
use_ssg: bool = True,
|
|
571
|
+
) -> List[MemoryItem]:
|
|
572
|
+
"""
|
|
573
|
+
Advanced search using Similarity Graph Traversal (SSG) algorithms.
|
|
574
|
+
|
|
575
|
+
SSG provides superior multi-hop reasoning and contextual retrieval compared to basic vector search.
|
|
576
|
+
|
|
577
|
+
Args:
|
|
578
|
+
query: Search query
|
|
579
|
+
algorithm: SSG algorithm to use:
|
|
580
|
+
- "query_traversal": Best for general queries (100% test pass, 0.91 precision/recall)
|
|
581
|
+
- "triangulation_fulldim": Best for high precision (highest faithfulness)
|
|
582
|
+
max_results: Maximum number of results to return
|
|
583
|
+
use_ssg: Enable SSG traversal (vs basic vector search)
|
|
584
|
+
|
|
585
|
+
Returns:
|
|
586
|
+
List of MemoryItem objects
|
|
587
|
+
|
|
588
|
+
Example:
|
|
589
|
+
```python
|
|
590
|
+
# Best for general queries
|
|
591
|
+
results = client.search_advanced("AI concepts", algorithm="query_traversal")
|
|
592
|
+
|
|
593
|
+
# Best for high precision factual queries
|
|
594
|
+
results = client.search_advanced("specific fact", algorithm="triangulation_fulldim")
|
|
595
|
+
|
|
596
|
+
# Disable SSG (fallback to basic search)
|
|
597
|
+
results = client.search_advanced("query", use_ssg=False)
|
|
598
|
+
|
|
599
|
+
for item in results:
|
|
600
|
+
print(f"{item.item_id}: {item.content}")
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
Note:
|
|
604
|
+
Based on research: github.com/glacier-creative-git/similarity-graph-traversal-semantic-rag-research
|
|
605
|
+
"""
|
|
606
|
+
payload = {
|
|
607
|
+
"query": query,
|
|
608
|
+
"algorithm": algorithm,
|
|
609
|
+
"max_results": max_results,
|
|
610
|
+
"use_ssg": use_ssg,
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
data = self._request("POST", "/memory/search/advanced", json_body=payload)
|
|
614
|
+
|
|
615
|
+
# Parse response using factory method
|
|
616
|
+
results = []
|
|
617
|
+
for item_dict in data.get("results", []):
|
|
618
|
+
results.append(MemoryItem.from_dict(item_dict))
|
|
619
|
+
|
|
620
|
+
return results
|
|
621
|
+
|
|
622
|
+
def code_search(
|
|
623
|
+
self,
|
|
624
|
+
query: str,
|
|
625
|
+
entity_type: Optional[str] = None,
|
|
626
|
+
repo: Optional[str] = None,
|
|
627
|
+
limit: int = 20,
|
|
628
|
+
semantic: bool = False,
|
|
629
|
+
) -> List[Dict[str, Any]]:
|
|
630
|
+
"""Search for code entities (classes, functions, routes, tests).
|
|
631
|
+
|
|
632
|
+
Args:
|
|
633
|
+
query: Search string (partial name match, or natural language when semantic=True)
|
|
634
|
+
entity_type: Filter by type: module, class, function, route, test
|
|
635
|
+
repo: Filter by repository name
|
|
636
|
+
limit: Maximum results (default 20)
|
|
637
|
+
semantic: Use vector similarity instead of name substring match
|
|
638
|
+
|
|
639
|
+
Returns:
|
|
640
|
+
List of code entity dicts with item_id, name, entity_type,
|
|
641
|
+
file_path, line_number, docstring, repo, score (when semantic=True).
|
|
642
|
+
|
|
643
|
+
Example:
|
|
644
|
+
```python
|
|
645
|
+
# Name substring search (default)
|
|
646
|
+
results = client.code_search("auth", entity_type="class")
|
|
647
|
+
|
|
648
|
+
# Semantic search — natural language
|
|
649
|
+
results = client.code_search("functions that handle payments", semantic=True)
|
|
650
|
+
```
|
|
651
|
+
"""
|
|
652
|
+
params: Dict[str, Any] = {"query": query, "limit": limit, "semantic": semantic}
|
|
653
|
+
if entity_type:
|
|
654
|
+
params["entity_type"] = entity_type
|
|
655
|
+
if repo:
|
|
656
|
+
params["repo"] = repo
|
|
657
|
+
return self._request("GET", "/memory/code/search", params=params)
|
|
658
|
+
|
|
659
|
+
def code_index(self, path: str, repo: Optional[str] = None, commit: Optional[str] = None) -> Dict[str, Any]:
|
|
660
|
+
"""Index code entities from a file or directory."""
|
|
661
|
+
body: Dict[str, Any] = {"path": path}
|
|
662
|
+
if repo:
|
|
663
|
+
body["repo"] = repo
|
|
664
|
+
if commit:
|
|
665
|
+
body["commit"] = commit
|
|
666
|
+
return self._request("POST", "/memory/code/index", json_body=body)
|
|
667
|
+
|
|
668
|
+
def code_context(self, entity_name: str, repo: Optional[str] = None) -> Dict[str, Any]:
|
|
669
|
+
"""Get rich context for a code entity."""
|
|
670
|
+
params: Dict[str, Any] = {"entity_name": entity_name}
|
|
671
|
+
if repo:
|
|
672
|
+
params["repo"] = repo
|
|
673
|
+
return self._request("GET", "/memory/code/context", params=params)
|
|
674
|
+
|
|
675
|
+
def code_dead_code(self, repo: str) -> Dict[str, Any]:
|
|
676
|
+
"""Find unreferenced code entities in a repository."""
|
|
677
|
+
return self._request("GET", "/memory/code/dead-code", params={"repo": repo})
|
|
678
|
+
|
|
679
|
+
def code_dependencies(self, entity_name: str, direction: str = "both", repo: Optional[str] = None) -> Dict[str, Any]:
|
|
680
|
+
"""Trace dependencies for a code entity."""
|
|
681
|
+
params: Dict[str, Any] = {"entity_name": entity_name, "direction": direction}
|
|
682
|
+
if repo:
|
|
683
|
+
params["repo"] = repo
|
|
684
|
+
return self._request("GET", "/memory/code/dependencies", params=params)
|
|
685
|
+
|
|
686
|
+
def get_plan(self, plan_id: str) -> Dict[str, Any]:
|
|
687
|
+
"""Get a plan container with all its tasks."""
|
|
688
|
+
return self._request("GET", f"/memory/{plan_id}")
|
|
689
|
+
|
|
690
|
+
def delete_plan(self, plan_id: str) -> bool:
|
|
691
|
+
"""Delete a plan."""
|
|
692
|
+
try:
|
|
693
|
+
self._request("DELETE", f"/memory/{plan_id}")
|
|
694
|
+
return True
|
|
695
|
+
except Exception:
|
|
696
|
+
return False
|
|
697
|
+
|
|
698
|
+
def update_plan_task(self, plan_id: str, task_id: str, status: str, outcome: Optional[str] = None) -> Dict[str, Any]:
|
|
699
|
+
"""Update a task's status within a plan."""
|
|
700
|
+
body: Dict[str, Any] = {"task_id": task_id, "status": status}
|
|
701
|
+
if outcome:
|
|
702
|
+
body["outcome"] = outcome
|
|
703
|
+
return self._request("PATCH", f"/memory/{plan_id}/task", json_body=body)
|
|
704
|
+
|
|
705
|
+
def complete_plan(self, plan_id: str, summary: Optional[str] = None, graduate_to_decision: bool = False) -> Dict[str, Any]:
|
|
706
|
+
"""Mark a plan as completed."""
|
|
707
|
+
body: Dict[str, Any] = {"graduate_to_decision": graduate_to_decision}
|
|
708
|
+
if summary:
|
|
709
|
+
body["summary"] = summary
|
|
710
|
+
return self._request("POST", f"/memory/{plan_id}/complete", json_body=body)
|
|
711
|
+
|
|
712
|
+
def fail_plan(self, plan_id: str, reason: str) -> Dict[str, Any]:
|
|
713
|
+
"""Mark a plan as failed."""
|
|
714
|
+
return self._request("POST", f"/memory/{plan_id}/fail", json_body={"reason": reason})
|
|
715
|
+
|
|
716
|
+
def update(
|
|
717
|
+
self,
|
|
718
|
+
item_id: str,
|
|
719
|
+
content: Optional[str] = None,
|
|
720
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
721
|
+
properties: Optional[Dict[str, Any]] = None,
|
|
722
|
+
write_mode: Optional[str] = None,
|
|
723
|
+
) -> bool:
|
|
724
|
+
"""
|
|
725
|
+
Update a memory item (CORE-CRUD-UPDATE-1 contract).
|
|
726
|
+
|
|
727
|
+
Args:
|
|
728
|
+
item_id: Memory item ID
|
|
729
|
+
content: Convenience — folded into properties["content"]
|
|
730
|
+
metadata: Convenience — deep-merged with existing metadata
|
|
731
|
+
properties: Advanced — direct node-property dict. Takes precedence
|
|
732
|
+
over content/metadata when provided.
|
|
733
|
+
write_mode: "merge" (default) or "replace"
|
|
734
|
+
|
|
735
|
+
Returns:
|
|
736
|
+
True if successful, False on any HTTP error
|
|
737
|
+
|
|
738
|
+
Examples:
|
|
739
|
+
```python
|
|
740
|
+
# Simple updates (convenience surface)
|
|
741
|
+
client.update("item_123", content="Updated content")
|
|
742
|
+
client.update("item_123", metadata={"updated": True})
|
|
743
|
+
|
|
744
|
+
# Advanced update (direct properties)
|
|
745
|
+
client.update("item_123",
|
|
746
|
+
properties={"importance_score": 0.9, "tags": ["v2"]})
|
|
747
|
+
|
|
748
|
+
# Replace all properties (preserves memory_type + node_category)
|
|
749
|
+
client.update("item_123",
|
|
750
|
+
properties={"content": "fresh", "tags": ["reset"]},
|
|
751
|
+
write_mode="replace")
|
|
752
|
+
```
|
|
753
|
+
"""
|
|
754
|
+
body: Dict[str, Any] = {}
|
|
755
|
+
if content is not None:
|
|
756
|
+
body["content"] = content
|
|
757
|
+
if metadata is not None:
|
|
758
|
+
body["metadata"] = metadata
|
|
759
|
+
if properties is not None:
|
|
760
|
+
body["properties"] = properties
|
|
761
|
+
if write_mode is not None:
|
|
762
|
+
body["write_mode"] = write_mode
|
|
763
|
+
|
|
764
|
+
try:
|
|
765
|
+
self._request("PATCH", f"/memory/{item_id}", json_body=body)
|
|
766
|
+
return True
|
|
767
|
+
except Exception:
|
|
768
|
+
return False
|
|
769
|
+
|
|
770
|
+
def delete(self, item_id: str) -> bool:
|
|
771
|
+
"""
|
|
772
|
+
Delete a memory item.
|
|
773
|
+
|
|
774
|
+
Args:
|
|
775
|
+
item_id: Memory item ID
|
|
776
|
+
|
|
777
|
+
Returns:
|
|
778
|
+
True if successful
|
|
779
|
+
|
|
780
|
+
Example:
|
|
781
|
+
```python
|
|
782
|
+
if client.delete("item_123"):
|
|
783
|
+
print("Memory deleted")
|
|
784
|
+
```
|
|
785
|
+
"""
|
|
786
|
+
try:
|
|
787
|
+
self._request("DELETE", f"/memory/{item_id}")
|
|
788
|
+
return True
|
|
789
|
+
except Exception:
|
|
790
|
+
return False
|
|
791
|
+
|
|
792
|
+
def ingest(
|
|
793
|
+
self,
|
|
794
|
+
content: str,
|
|
795
|
+
extractor_name: str = "llm",
|
|
796
|
+
context: Optional[Dict[str, Any]] = None,
|
|
797
|
+
) -> Dict[str, Any]:
|
|
798
|
+
"""
|
|
799
|
+
Ingest content with full extraction and enrichment pipeline.
|
|
800
|
+
|
|
801
|
+
Args:
|
|
802
|
+
content: Content to ingest
|
|
803
|
+
extractor_name: Name of the extractor to use (default: "llm")
|
|
804
|
+
context: Additional context information
|
|
805
|
+
|
|
806
|
+
Returns:
|
|
807
|
+
Ingestion result with item_id, user_id, tenant_id, queued status
|
|
808
|
+
|
|
809
|
+
Example:
|
|
810
|
+
```python
|
|
811
|
+
# Ingest conversation
|
|
812
|
+
result = client.ingest(
|
|
813
|
+
content="User: Hello\nAssistant: Hi there!",
|
|
814
|
+
extractor_name="llm",
|
|
815
|
+
context={"conversation_id": "123", "timestamp": "2025-11-07"}
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
print(f"Ingested: {result['item_id']}")
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
Note:
|
|
822
|
+
user_id is automatically determined from the JWT token.
|
|
823
|
+
"""
|
|
824
|
+
body_dict = {
|
|
825
|
+
"content": content,
|
|
826
|
+
"extractor_name": extractor_name,
|
|
827
|
+
"context": context or {},
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
try:
|
|
831
|
+
return self._request("POST", "/memory/ingest", json_body=body_dict)
|
|
832
|
+
except Exception as e:
|
|
833
|
+
raise SmartMemoryClientError(f"Failed to ingest content: {str(e)}")
|
|
834
|
+
|
|
835
|
+
def ingest_conversation(
|
|
836
|
+
self,
|
|
837
|
+
turns: List[Dict[str, str]],
|
|
838
|
+
session_boundaries: Optional[List[int]] = None,
|
|
839
|
+
conversation_id: Optional[str] = None,
|
|
840
|
+
session_dates: Optional[List[str]] = None,
|
|
841
|
+
turns_per_chunk: int = 15,
|
|
842
|
+
max_chunk_chars: int = 12000,
|
|
843
|
+
max_concurrent: int = 4,
|
|
844
|
+
) -> Dict[str, Any]:
|
|
845
|
+
"""Ingest a conversation as session chunks through the full pipeline (RLM-1g).
|
|
846
|
+
|
|
847
|
+
Args:
|
|
848
|
+
turns: List of turn dicts with "role"/"speaker" and "content" keys.
|
|
849
|
+
session_boundaries: Turn indices where sessions start (e.g. [0, 50, 120]).
|
|
850
|
+
conversation_id: User-supplied ID (auto-generated if None).
|
|
851
|
+
session_dates: ISO date per session for metadata.
|
|
852
|
+
turns_per_chunk: Max turns per auto-chunk (default 15).
|
|
853
|
+
max_chunk_chars: Safety split for oversized chunks (default 12000).
|
|
854
|
+
max_concurrent: Semaphore limit for parallel chunk ingestion (default 4).
|
|
855
|
+
|
|
856
|
+
Returns:
|
|
857
|
+
Dict with conversation_id, conversation_node_id, chunks_ingested,
|
|
858
|
+
chunks_failed, total_turns, total_chunks, chunk_results, total_duration_ms.
|
|
859
|
+
"""
|
|
860
|
+
body_dict: Dict[str, Any] = {"turns": turns}
|
|
861
|
+
if session_boundaries is not None:
|
|
862
|
+
body_dict["session_boundaries"] = session_boundaries
|
|
863
|
+
if conversation_id is not None:
|
|
864
|
+
body_dict["conversation_id"] = conversation_id
|
|
865
|
+
if session_dates is not None:
|
|
866
|
+
body_dict["session_dates"] = session_dates
|
|
867
|
+
if turns_per_chunk != 15:
|
|
868
|
+
body_dict["turns_per_chunk"] = turns_per_chunk
|
|
869
|
+
if max_chunk_chars != 12000:
|
|
870
|
+
body_dict["max_chunk_chars"] = max_chunk_chars
|
|
871
|
+
if max_concurrent != 4:
|
|
872
|
+
body_dict["max_concurrent"] = max_concurrent
|
|
873
|
+
|
|
874
|
+
try:
|
|
875
|
+
return self._request(
|
|
876
|
+
"POST", "/memory/ingest/conversation", json_body=body_dict
|
|
877
|
+
)
|
|
878
|
+
except Exception as e:
|
|
879
|
+
raise SmartMemoryClientError(f"Failed to ingest conversation: {str(e)}")
|
|
880
|
+
|
|
881
|
+
def link(self, source_id: str, target_id: str, link_type: str = "RELATED") -> bool:
|
|
882
|
+
"""
|
|
883
|
+
Create a link between two memory items.
|
|
884
|
+
|
|
885
|
+
Args:
|
|
886
|
+
source_id: Source memory item ID
|
|
887
|
+
target_id: Target memory item ID
|
|
888
|
+
link_type: Type of link (RELATED, CAUSES, FOLLOWS, etc.)
|
|
889
|
+
|
|
890
|
+
Returns:
|
|
891
|
+
True if successful
|
|
892
|
+
|
|
893
|
+
Example:
|
|
894
|
+
```python
|
|
895
|
+
# Create relationship
|
|
896
|
+
client.link("concept_1", "concept_2", link_type="RELATED")
|
|
897
|
+
client.link("cause_id", "effect_id", link_type="CAUSES")
|
|
898
|
+
```
|
|
899
|
+
"""
|
|
900
|
+
body_dict = {
|
|
901
|
+
"source_id": source_id,
|
|
902
|
+
"target_id": target_id,
|
|
903
|
+
"link_type": link_type,
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
try:
|
|
907
|
+
self._request("POST", "/memory/link", json_body=body_dict)
|
|
908
|
+
return True
|
|
909
|
+
except Exception as e:
|
|
910
|
+
# Link endpoint may not exist or be disabled - fail gracefully
|
|
911
|
+
logger.debug(f"Link operation not supported or failed: {e}")
|
|
912
|
+
return False
|
|
913
|
+
|
|
914
|
+
def add_edge(
|
|
915
|
+
self,
|
|
916
|
+
source_id: str,
|
|
917
|
+
target_id: str,
|
|
918
|
+
relation_type: str,
|
|
919
|
+
properties: Optional[Dict[str, Any]] = None,
|
|
920
|
+
) -> Dict[str, Any]:
|
|
921
|
+
"""
|
|
922
|
+
Add a direct edge between two nodes in the graph.
|
|
923
|
+
|
|
924
|
+
This is a lower-level operation than link() - it creates a raw edge
|
|
925
|
+
with custom properties. Use link() for standard memory linking.
|
|
926
|
+
|
|
927
|
+
Args:
|
|
928
|
+
source_id: Source node ID
|
|
929
|
+
target_id: Target node ID
|
|
930
|
+
relation_type: Type of relation/edge
|
|
931
|
+
properties: Optional edge properties
|
|
932
|
+
|
|
933
|
+
Returns:
|
|
934
|
+
Dict with edge creation result
|
|
935
|
+
|
|
936
|
+
Example:
|
|
937
|
+
```python
|
|
938
|
+
result = client.add_edge(
|
|
939
|
+
source_id="node_1",
|
|
940
|
+
target_id="node_2",
|
|
941
|
+
relation_type="INFLUENCES",
|
|
942
|
+
properties={"weight": 0.8, "confidence": 0.95}
|
|
943
|
+
)
|
|
944
|
+
```
|
|
945
|
+
"""
|
|
946
|
+
body = {
|
|
947
|
+
"source_id": source_id,
|
|
948
|
+
"target_id": target_id,
|
|
949
|
+
"relation_type": relation_type,
|
|
950
|
+
"properties": properties or {},
|
|
951
|
+
}
|
|
952
|
+
return self._request("POST", "/memory/edge", json_body=body)
|
|
953
|
+
|
|
954
|
+
def get_neighbors(self, item_id: str) -> List[Dict[str, Any]]:
|
|
955
|
+
"""
|
|
956
|
+
Get neighboring memory items (linked items).
|
|
957
|
+
|
|
958
|
+
Args:
|
|
959
|
+
item_id: Memory item ID
|
|
960
|
+
|
|
961
|
+
Returns:
|
|
962
|
+
List of neighbor information with item and link_type
|
|
963
|
+
|
|
964
|
+
Example:
|
|
965
|
+
```python
|
|
966
|
+
neighbors = client.get_neighbors("item_123")
|
|
967
|
+
for neighbor in neighbors:
|
|
968
|
+
print(f"{neighbor['item_id']}: {neighbor['link_type']}")
|
|
969
|
+
```
|
|
970
|
+
"""
|
|
971
|
+
try:
|
|
972
|
+
result = self._request("GET", f"/memory/{item_id}/neighbors")
|
|
973
|
+
return result.get("neighbors", [])
|
|
974
|
+
except Exception as e:
|
|
975
|
+
logger.warning(f"Error getting neighbors: {e}")
|
|
976
|
+
return []
|
|
977
|
+
|
|
978
|
+
def get_lineage(self, item_id: str) -> Dict[str, Any]:
|
|
979
|
+
"""Get the supersession lineage chain for a memory item."""
|
|
980
|
+
return self._request("GET", f"/memory/{item_id}/lineage")
|
|
981
|
+
|
|
982
|
+
def get_links(self, item_id: str) -> Dict[str, Any]:
|
|
983
|
+
"""Get all edges (links) for a memory item."""
|
|
984
|
+
return self._request("GET", f"/memory/{item_id}/links")
|
|
985
|
+
|
|
986
|
+
def search_by_metadata(
|
|
987
|
+
self, filters: Dict[str, Any], limit: int = 50, offset: int = 0
|
|
988
|
+
) -> List[Dict[str, Any]]:
|
|
989
|
+
"""Search memory items by metadata key-value filters."""
|
|
990
|
+
params = {"limit": limit, "offset": offset, **filters}
|
|
991
|
+
return self._request("GET", "/memory/by-metadata", params=params)
|
|
992
|
+
|
|
993
|
+
def get_recall_profile(self, agent_id: str) -> Dict[str, Any]:
|
|
994
|
+
"""Get an agent's recall profile for personality-aware retrieval."""
|
|
995
|
+
return self._request("GET", f"/memory/agents/{agent_id}/recall-profile")
|
|
996
|
+
|
|
997
|
+
def set_recall_profile(self, agent_id: str, recall_profile: Dict[str, Any]) -> Dict[str, Any]:
|
|
998
|
+
"""Set an agent's recall profile. Send {} to clear."""
|
|
999
|
+
return self._request("PUT", f"/memory/agents/{agent_id}/recall-profile", json_body={"recall_profile": recall_profile})
|
|
1000
|
+
|
|
1001
|
+
def summary(self) -> Dict[str, Any]:
|
|
1002
|
+
"""Get summary statistics about the memory system."""
|
|
1003
|
+
return self._request("GET", "/memory/summary")
|
|
1004
|
+
|
|
1005
|
+
def enrich(
|
|
1006
|
+
self, item_id: str, routines: Optional[List[str]] = None
|
|
1007
|
+
) -> Dict[str, Any]:
|
|
1008
|
+
"""
|
|
1009
|
+
Enrich a memory item with additional processing.
|
|
1010
|
+
|
|
1011
|
+
Args:
|
|
1012
|
+
item_id: Memory item ID
|
|
1013
|
+
routines: List of enrichment routines to run
|
|
1014
|
+
|
|
1015
|
+
Returns:
|
|
1016
|
+
Enrichment result
|
|
1017
|
+
|
|
1018
|
+
Example:
|
|
1019
|
+
```python
|
|
1020
|
+
result = client.enrich("item_123", routines=["sentiment", "keywords"])
|
|
1021
|
+
```
|
|
1022
|
+
"""
|
|
1023
|
+
body = {"item_id": item_id, "routines": routines or []}
|
|
1024
|
+
return self._request("POST", f"/memory/{item_id}/enrich", json_body=body)
|
|
1025
|
+
|
|
1026
|
+
def personalize(
|
|
1027
|
+
self,
|
|
1028
|
+
traits: Optional[Dict[str, Any]] = None,
|
|
1029
|
+
preferences: Optional[Dict[str, Any]] = None,
|
|
1030
|
+
) -> Dict[str, Any]:
|
|
1031
|
+
"""
|
|
1032
|
+
Update personalization settings for the authenticated user.
|
|
1033
|
+
|
|
1034
|
+
Args:
|
|
1035
|
+
traits: User traits
|
|
1036
|
+
preferences: User preferences
|
|
1037
|
+
|
|
1038
|
+
Returns:
|
|
1039
|
+
Personalization result
|
|
1040
|
+
|
|
1041
|
+
Example:
|
|
1042
|
+
```python
|
|
1043
|
+
result = client.personalize(
|
|
1044
|
+
traits={"learning_style": "visual"},
|
|
1045
|
+
preferences={"language": "en", "complexity": "advanced"}
|
|
1046
|
+
)
|
|
1047
|
+
```
|
|
1048
|
+
|
|
1049
|
+
Note:
|
|
1050
|
+
user_id is automatically determined from the JWT token.
|
|
1051
|
+
"""
|
|
1052
|
+
body = {"traits": traits or {}, "preferences": preferences or {}}
|
|
1053
|
+
return self._request("POST", "/memory/personalize", json_body=body)
|
|
1054
|
+
|
|
1055
|
+
def provide_feedback(
|
|
1056
|
+
self, feedback: Dict[str, Any], memory_type: str = "semantic"
|
|
1057
|
+
) -> Dict[str, Any]:
|
|
1058
|
+
"""
|
|
1059
|
+
Provide feedback to improve the memory system.
|
|
1060
|
+
|
|
1061
|
+
Args:
|
|
1062
|
+
feedback: Feedback information
|
|
1063
|
+
memory_type: Type of memory to update
|
|
1064
|
+
|
|
1065
|
+
Returns:
|
|
1066
|
+
Feedback processing result
|
|
1067
|
+
|
|
1068
|
+
Example:
|
|
1069
|
+
```python
|
|
1070
|
+
client.provide_feedback(
|
|
1071
|
+
feedback={"rating": 5, "comment": "Great result"},
|
|
1072
|
+
memory_type="semantic"
|
|
1073
|
+
)
|
|
1074
|
+
```
|
|
1075
|
+
"""
|
|
1076
|
+
body = {"feedback": feedback, "memory_type": memory_type}
|
|
1077
|
+
return self._request("POST", "/memory/feedback", json_body=body)
|
|
1078
|
+
|
|
1079
|
+
def cluster(
|
|
1080
|
+
self, distance_threshold: float = 0.1, dry_run: bool = False
|
|
1081
|
+
) -> Dict[str, Any]:
|
|
1082
|
+
"""
|
|
1083
|
+
Run entity clustering/deduplication for the workspace.
|
|
1084
|
+
|
|
1085
|
+
Args:
|
|
1086
|
+
distance_threshold: Similarity threshold (0.0-1.0, default 0.1)
|
|
1087
|
+
dry_run: If true, preview clusters without merging
|
|
1088
|
+
|
|
1089
|
+
Returns:
|
|
1090
|
+
Clustering results (merged_count, clusters_found, etc.)
|
|
1091
|
+
"""
|
|
1092
|
+
params = {"distance_threshold": distance_threshold, "dry_run": dry_run}
|
|
1093
|
+
return self._request("POST", "/memory/clustering/run", params=params)
|
|
1094
|
+
|
|
1095
|
+
def get_clustering_stats(self) -> Dict[str, Any]:
|
|
1096
|
+
"""
|
|
1097
|
+
Get clustering statistics for the workspace.
|
|
1098
|
+
|
|
1099
|
+
Returns:
|
|
1100
|
+
Clustering statistics
|
|
1101
|
+
"""
|
|
1102
|
+
return self._request("GET", "/memory/clustering/stats")
|
|
1103
|
+
|
|
1104
|
+
def ground(
|
|
1105
|
+
self, item_id: str, source_url: str, validation: Optional[Dict[str, Any]] = None
|
|
1106
|
+
) -> Dict[str, Any]:
|
|
1107
|
+
"""
|
|
1108
|
+
Ground a memory item to an external source for provenance.
|
|
1109
|
+
|
|
1110
|
+
Args:
|
|
1111
|
+
item_id: Memory item ID
|
|
1112
|
+
source_url: URL of the source
|
|
1113
|
+
validation: Optional validation data
|
|
1114
|
+
|
|
1115
|
+
Returns:
|
|
1116
|
+
Result message
|
|
1117
|
+
"""
|
|
1118
|
+
body = {"item_id": item_id, "source_url": source_url, "validation": validation}
|
|
1119
|
+
return self._request("POST", f"/memory/{item_id}/ground", json_body=body)
|
|
1120
|
+
|
|
1121
|
+
def get_summarize_prompt(self, item_id: str) -> Dict[str, Any]:
|
|
1122
|
+
"""
|
|
1123
|
+
Generate a prompt template for summarizing a memory item.
|
|
1124
|
+
|
|
1125
|
+
Args:
|
|
1126
|
+
item_id: Memory item ID
|
|
1127
|
+
|
|
1128
|
+
Returns:
|
|
1129
|
+
Prompt template and metadata
|
|
1130
|
+
"""
|
|
1131
|
+
return self._request("GET", f"/memory/{item_id}/prompt/summarize")
|
|
1132
|
+
|
|
1133
|
+
def get_analyze_prompt(self, item_id: str) -> Dict[str, Any]:
|
|
1134
|
+
"""
|
|
1135
|
+
Generate a prompt template for analyzing memory connections.
|
|
1136
|
+
|
|
1137
|
+
Args:
|
|
1138
|
+
item_id: Memory item ID
|
|
1139
|
+
|
|
1140
|
+
Returns:
|
|
1141
|
+
Prompt template and metadata
|
|
1142
|
+
"""
|
|
1143
|
+
return self._request("GET", f"/memory/{item_id}/prompt/analyze")
|
|
1144
|
+
|
|
1145
|
+
def ingest_full(
|
|
1146
|
+
self,
|
|
1147
|
+
content: str,
|
|
1148
|
+
extractor_name: str = "llm",
|
|
1149
|
+
context: Optional[Dict[str, Any]] = None,
|
|
1150
|
+
) -> Dict[str, Any]:
|
|
1151
|
+
"""
|
|
1152
|
+
Ingest content with full synchronous entity/relation extraction pipeline.
|
|
1153
|
+
|
|
1154
|
+
Args:
|
|
1155
|
+
content: Content to ingest
|
|
1156
|
+
extractor_name: Name of the extractor to use (default: "llm")
|
|
1157
|
+
context: Additional context information
|
|
1158
|
+
|
|
1159
|
+
Returns:
|
|
1160
|
+
Full ingestion result with entities and relations
|
|
1161
|
+
"""
|
|
1162
|
+
body = {
|
|
1163
|
+
"content": content,
|
|
1164
|
+
"extractor_name": extractor_name,
|
|
1165
|
+
"context": context or {},
|
|
1166
|
+
}
|
|
1167
|
+
return self._request("POST", "/memory/ingest/full", json_body=body)
|
|
1168
|
+
|
|
1169
|
+
# ============================================================================
|
|
1170
|
+
# Admin & Monitoring
|
|
1171
|
+
# ============================================================================
|
|
1172
|
+
|
|
1173
|
+
def orphaned_notes(self) -> Dict[str, Any]:
|
|
1174
|
+
"""Find orphaned notes (notes with no connections)."""
|
|
1175
|
+
return self._request("GET", "/memory/admin/orphaned-notes")
|
|
1176
|
+
|
|
1177
|
+
def prune(
|
|
1178
|
+
self, strategy: str = "old", days: int = 365, dry_run: bool = True
|
|
1179
|
+
) -> Dict[str, Any]:
|
|
1180
|
+
"""Prune old or unused memories."""
|
|
1181
|
+
params = {"strategy": strategy, "days": days, "dry_run": dry_run}
|
|
1182
|
+
return self._request("POST", "/memory/admin/prune", params=params)
|
|
1183
|
+
|
|
1184
|
+
def find_old_notes(self, days: int = 365) -> Dict[str, Any]:
|
|
1185
|
+
"""Find notes older than N days."""
|
|
1186
|
+
return self._request("GET", "/memory/admin/old-notes", params={"days": days})
|
|
1187
|
+
|
|
1188
|
+
def self_monitor(self) -> Dict[str, Any]:
|
|
1189
|
+
"""Get self-monitoring metrics."""
|
|
1190
|
+
return self._request("GET", "/memory/admin/self-monitor")
|
|
1191
|
+
|
|
1192
|
+
def get_system_stats(self) -> Dict[str, Any]:
|
|
1193
|
+
"""Get comprehensive system statistics."""
|
|
1194
|
+
return self._request("GET", "/memory/admin/stats")
|
|
1195
|
+
|
|
1196
|
+
def reflect(self, top_k: int = 5) -> Dict[str, Any]:
|
|
1197
|
+
"""
|
|
1198
|
+
Reflect on memory patterns and insights.
|
|
1199
|
+
|
|
1200
|
+
Analyzes memory content to identify patterns, themes,
|
|
1201
|
+
and potential connections.
|
|
1202
|
+
|
|
1203
|
+
Args:
|
|
1204
|
+
top_k: Number of top items to reflect on
|
|
1205
|
+
|
|
1206
|
+
Returns:
|
|
1207
|
+
Dict with reflection results including themes, patterns, suggestions
|
|
1208
|
+
|
|
1209
|
+
Example:
|
|
1210
|
+
```python
|
|
1211
|
+
reflection = client.reflect(top_k=10)
|
|
1212
|
+
print(reflection["reflection"]["themes"])
|
|
1213
|
+
```
|
|
1214
|
+
"""
|
|
1215
|
+
return self._request("GET", "/memory/admin/reflect", params={"top_k": top_k})
|
|
1216
|
+
|
|
1217
|
+
def summarize(self, max_items: int = 10) -> Dict[str, Any]:
|
|
1218
|
+
"""
|
|
1219
|
+
Generate a summary of memory contents.
|
|
1220
|
+
|
|
1221
|
+
Creates a high-level overview of stored memories,
|
|
1222
|
+
including key topics, recent additions, and knowledge distribution.
|
|
1223
|
+
|
|
1224
|
+
Args:
|
|
1225
|
+
max_items: Maximum items to include in summary
|
|
1226
|
+
|
|
1227
|
+
Returns:
|
|
1228
|
+
Dict with summary including topic distribution, memory type breakdown
|
|
1229
|
+
|
|
1230
|
+
Example:
|
|
1231
|
+
```python
|
|
1232
|
+
summary = client.summarize(max_items=20)
|
|
1233
|
+
print(summary["summary"]["topic_distribution"])
|
|
1234
|
+
```
|
|
1235
|
+
"""
|
|
1236
|
+
return self._request(
|
|
1237
|
+
"GET", "/memory/admin/summarize", params={"max_items": max_items}
|
|
1238
|
+
)
|
|
1239
|
+
|
|
1240
|
+
# ============================================================================
|
|
1241
|
+
# Agents
|
|
1242
|
+
# ============================================================================
|
|
1243
|
+
|
|
1244
|
+
def create_agent(
|
|
1245
|
+
self,
|
|
1246
|
+
name: str,
|
|
1247
|
+
description: Optional[str] = None,
|
|
1248
|
+
agent_config: Optional[Dict[str, Any]] = None,
|
|
1249
|
+
roles: Optional[List[str]] = None,
|
|
1250
|
+
) -> Dict[str, Any]:
|
|
1251
|
+
"""Create a new AI agent."""
|
|
1252
|
+
body = {
|
|
1253
|
+
"name": name,
|
|
1254
|
+
"description": description,
|
|
1255
|
+
"agent_config": agent_config or {},
|
|
1256
|
+
"roles": roles or ["user"],
|
|
1257
|
+
}
|
|
1258
|
+
return self._request("POST", "/memory/agents", json_body=body)
|
|
1259
|
+
|
|
1260
|
+
def list_agents(self) -> List[Dict[str, Any]]:
|
|
1261
|
+
"""List all agents in the current tenant."""
|
|
1262
|
+
return self._request("GET", "/memory/agents")
|
|
1263
|
+
|
|
1264
|
+
def get_agent(self, agent_id: str) -> Dict[str, Any]:
|
|
1265
|
+
"""Get details of a specific agent."""
|
|
1266
|
+
return self._request("GET", f"/memory/agents/{agent_id}")
|
|
1267
|
+
|
|
1268
|
+
def delete_agent(self, agent_id: str) -> None:
|
|
1269
|
+
"""Delete (deactivate) an agent."""
|
|
1270
|
+
self._request("DELETE", f"/memory/agents/{agent_id}")
|
|
1271
|
+
|
|
1272
|
+
# ============================================================================
|
|
1273
|
+
# Analytics
|
|
1274
|
+
# ============================================================================
|
|
1275
|
+
|
|
1276
|
+
def get_analytics_status(self) -> Dict[str, Any]:
|
|
1277
|
+
"""Return analytics feature status."""
|
|
1278
|
+
return self._request("GET", "/memory/analytics/status")
|
|
1279
|
+
|
|
1280
|
+
def detect_drift(self, time_window_days: int = 30) -> Dict[str, Any]:
|
|
1281
|
+
"""Run concept drift detection."""
|
|
1282
|
+
return self._request(
|
|
1283
|
+
"GET",
|
|
1284
|
+
"/memory/analytics/drift",
|
|
1285
|
+
params={"time_window_days": time_window_days},
|
|
1286
|
+
)
|
|
1287
|
+
|
|
1288
|
+
def detect_bias(
|
|
1289
|
+
self,
|
|
1290
|
+
protected_attributes: Optional[List[str]] = None,
|
|
1291
|
+
sentiment_analysis: Optional[bool] = None,
|
|
1292
|
+
topic_analysis: Optional[bool] = None,
|
|
1293
|
+
) -> Dict[str, Any]:
|
|
1294
|
+
"""Run bias detection."""
|
|
1295
|
+
body = {
|
|
1296
|
+
"protected_attributes": protected_attributes,
|
|
1297
|
+
"sentiment_analysis": sentiment_analysis,
|
|
1298
|
+
"topic_analysis": topic_analysis,
|
|
1299
|
+
}
|
|
1300
|
+
return self._request("POST", "/memory/analytics/bias", json_body=body)
|
|
1301
|
+
|
|
1302
|
+
# ============================================================================
|
|
1303
|
+
# API Keys
|
|
1304
|
+
# ============================================================================
|
|
1305
|
+
|
|
1306
|
+
def create_api_key(
|
|
1307
|
+
self,
|
|
1308
|
+
name: str,
|
|
1309
|
+
scopes: Optional[List[str]] = None,
|
|
1310
|
+
expires_in_days: Optional[int] = None,
|
|
1311
|
+
) -> Dict[str, Any]:
|
|
1312
|
+
"""Create a new API key."""
|
|
1313
|
+
body = {
|
|
1314
|
+
"name": name,
|
|
1315
|
+
"scopes": scopes or ["read:memories"],
|
|
1316
|
+
"expires_in_days": expires_in_days,
|
|
1317
|
+
}
|
|
1318
|
+
return self._request("POST", "/memory/api-keys", json_body=body)
|
|
1319
|
+
|
|
1320
|
+
def list_api_keys(self) -> List[Dict[str, Any]]:
|
|
1321
|
+
"""List all API keys."""
|
|
1322
|
+
return self._request("GET", "/memory/api-keys")
|
|
1323
|
+
|
|
1324
|
+
def revoke_api_key(self, key_id: str) -> None:
|
|
1325
|
+
"""Revoke (delete) an API key."""
|
|
1326
|
+
self._request("DELETE", f"/memory/api-keys/{key_id}")
|
|
1327
|
+
|
|
1328
|
+
# ============================================================================
|
|
1329
|
+
# Auth
|
|
1330
|
+
# ============================================================================
|
|
1331
|
+
|
|
1332
|
+
def get_me(self) -> Dict[str, Any]:
|
|
1333
|
+
"""Get current authenticated user info."""
|
|
1334
|
+
return self._request("GET", "/auth/me")
|
|
1335
|
+
|
|
1336
|
+
def logout_all(self) -> None:
|
|
1337
|
+
"""Logout from all devices."""
|
|
1338
|
+
self._request("POST", "/auth/logout-all")
|
|
1339
|
+
self._token = None
|
|
1340
|
+
self._refresh_token = None
|
|
1341
|
+
self._api_key = None
|
|
1342
|
+
|
|
1343
|
+
def update_llm_keys(
|
|
1344
|
+
self,
|
|
1345
|
+
openai_key: Optional[str] = None,
|
|
1346
|
+
anthropic_key: Optional[str] = None,
|
|
1347
|
+
groq_key: Optional[str] = None,
|
|
1348
|
+
) -> Dict[str, Any]:
|
|
1349
|
+
"""Update user's LLM provider API keys."""
|
|
1350
|
+
body = {
|
|
1351
|
+
"openai_key": openai_key,
|
|
1352
|
+
"anthropic_key": anthropic_key,
|
|
1353
|
+
"groq_key": groq_key,
|
|
1354
|
+
}
|
|
1355
|
+
return self._request("PATCH", "/auth/llm-keys", json_body=body)
|
|
1356
|
+
|
|
1357
|
+
def get_llm_keys(self) -> Dict[str, Any]:
|
|
1358
|
+
"""Get user's LLM provider API keys (masked)."""
|
|
1359
|
+
return self._request("GET", "/auth/llm-keys")
|
|
1360
|
+
|
|
1361
|
+
# ============================================================================
|
|
1362
|
+
# Evolve
|
|
1363
|
+
# ============================================================================
|
|
1364
|
+
|
|
1365
|
+
def trigger_evolution(self) -> Dict[str, Any]:
|
|
1366
|
+
"""Manually trigger memory evolution processes."""
|
|
1367
|
+
return self._request("POST", "/memory/evolution/trigger")
|
|
1368
|
+
|
|
1369
|
+
def run_dream_phase(self) -> Dict[str, Any]:
|
|
1370
|
+
"""Run a 'dream' phase: promote working memory to episodic/procedural."""
|
|
1371
|
+
return self._request("POST", "/memory/evolution/dream")
|
|
1372
|
+
|
|
1373
|
+
def get_evolution_status(self) -> Dict[str, Any]:
|
|
1374
|
+
"""Get status of memory evolution processes."""
|
|
1375
|
+
return self._request("GET", "/memory/evolution/status")
|
|
1376
|
+
|
|
1377
|
+
# ============================================================================
|
|
1378
|
+
# Governance
|
|
1379
|
+
# ============================================================================
|
|
1380
|
+
|
|
1381
|
+
def run_governance_analysis(
|
|
1382
|
+
self,
|
|
1383
|
+
query: str = "*",
|
|
1384
|
+
top_k: int = 100,
|
|
1385
|
+
memory_items: Optional[List[Dict[str, Any]]] = None,
|
|
1386
|
+
) -> Dict[str, Any]:
|
|
1387
|
+
"""Run governance analysis."""
|
|
1388
|
+
body = {"query": query, "top_k": top_k, "memory_items": memory_items or []}
|
|
1389
|
+
return self._request("POST", "/memory/governance/run-analysis", json_body=body)
|
|
1390
|
+
|
|
1391
|
+
def list_violations(
|
|
1392
|
+
self, severity: Optional[str] = None, auto_fixable_only: bool = False
|
|
1393
|
+
) -> Dict[str, Any]:
|
|
1394
|
+
"""List violations available for review."""
|
|
1395
|
+
params = {"severity": severity, "auto_fixable_only": auto_fixable_only}
|
|
1396
|
+
return self._request("GET", "/memory/governance/violations", params=params)
|
|
1397
|
+
|
|
1398
|
+
def get_violation(self, violation_id: str) -> Dict[str, Any]:
|
|
1399
|
+
"""Get a specific violation by ID."""
|
|
1400
|
+
return self._request("GET", f"/memory/governance/violations/{violation_id}")
|
|
1401
|
+
|
|
1402
|
+
def apply_governance_decision(
|
|
1403
|
+
self,
|
|
1404
|
+
violation_id: str,
|
|
1405
|
+
action: str = "approve",
|
|
1406
|
+
rationale: str = "",
|
|
1407
|
+
decided_by: str = "human",
|
|
1408
|
+
) -> Dict[str, Any]:
|
|
1409
|
+
"""Apply a governance decision for a violation."""
|
|
1410
|
+
body = {
|
|
1411
|
+
"violation_id": violation_id,
|
|
1412
|
+
"action": action,
|
|
1413
|
+
"rationale": rationale,
|
|
1414
|
+
"decided_by": decided_by,
|
|
1415
|
+
}
|
|
1416
|
+
return self._request(
|
|
1417
|
+
"POST", "/memory/governance/apply-decision", json_body=body
|
|
1418
|
+
)
|
|
1419
|
+
|
|
1420
|
+
def auto_fix_violations(self, confidence_threshold: float = 0.8) -> Dict[str, Any]:
|
|
1421
|
+
"""Run auto-fix for high-confidence violations."""
|
|
1422
|
+
body = {"confidence_threshold": confidence_threshold}
|
|
1423
|
+
return self._request("POST", "/memory/governance/auto-fix", json_body=body)
|
|
1424
|
+
|
|
1425
|
+
def get_governance_summary(self) -> Dict[str, Any]:
|
|
1426
|
+
"""Get a summary of governance state."""
|
|
1427
|
+
return self._request("GET", "/memory/governance/summary")
|
|
1428
|
+
|
|
1429
|
+
# ============================================================================
|
|
1430
|
+
# Ontology
|
|
1431
|
+
# ============================================================================
|
|
1432
|
+
|
|
1433
|
+
def run_inference(
|
|
1434
|
+
self,
|
|
1435
|
+
raw_chunks: List[Dict[str, str]],
|
|
1436
|
+
registry_id: str = "default",
|
|
1437
|
+
params: Optional[Dict[str, Any]] = None,
|
|
1438
|
+
) -> Dict[str, Any]:
|
|
1439
|
+
"""Run ontology inference over provided raw text chunks."""
|
|
1440
|
+
body = {
|
|
1441
|
+
"registry_id": registry_id,
|
|
1442
|
+
"raw_chunks": raw_chunks,
|
|
1443
|
+
"params": params or {},
|
|
1444
|
+
}
|
|
1445
|
+
return self._request("POST", "/memory/ontology/inference/run", json_body=body)
|
|
1446
|
+
|
|
1447
|
+
def list_registries(self) -> Dict[str, Any]:
|
|
1448
|
+
"""List all ontology registries."""
|
|
1449
|
+
return self._request("GET", "/memory/ontology/registries")
|
|
1450
|
+
|
|
1451
|
+
def create_registry(
|
|
1452
|
+
self, name: str, description: str = "", domain: str = "general"
|
|
1453
|
+
) -> Dict[str, Any]:
|
|
1454
|
+
"""Create a new ontology registry."""
|
|
1455
|
+
body = {"name": name, "description": description, "domain": domain}
|
|
1456
|
+
return self._request("POST", "/memory/ontology/registries", json_body=body)
|
|
1457
|
+
|
|
1458
|
+
def get_registry_snapshot(
|
|
1459
|
+
self, registry_id: str, version: Optional[str] = None
|
|
1460
|
+
) -> Dict[str, Any]:
|
|
1461
|
+
"""Get a snapshot of a registry (current or specific version)."""
|
|
1462
|
+
params = {"version": version} if version else None
|
|
1463
|
+
return self._request(
|
|
1464
|
+
"GET", f"/memory/ontology/registry/{registry_id}/snapshot", params=params
|
|
1465
|
+
)
|
|
1466
|
+
|
|
1467
|
+
def apply_changeset(
|
|
1468
|
+
self,
|
|
1469
|
+
registry_id: str,
|
|
1470
|
+
changeset: Dict[str, Any],
|
|
1471
|
+
base_version: str = "",
|
|
1472
|
+
message: str = "",
|
|
1473
|
+
) -> Dict[str, Any]:
|
|
1474
|
+
"""Apply a changeset to create a new version of the registry."""
|
|
1475
|
+
body = {
|
|
1476
|
+
"base_version": base_version,
|
|
1477
|
+
"changeset": changeset,
|
|
1478
|
+
"message": message,
|
|
1479
|
+
}
|
|
1480
|
+
return self._request(
|
|
1481
|
+
"POST", f"/memory/ontology/registry/{registry_id}/apply", json_body=body
|
|
1482
|
+
)
|
|
1483
|
+
|
|
1484
|
+
def list_registry_snapshots(
|
|
1485
|
+
self, registry_id: str, limit: int = 50
|
|
1486
|
+
) -> Dict[str, Any]:
|
|
1487
|
+
"""List snapshots for a registry."""
|
|
1488
|
+
return self._request(
|
|
1489
|
+
"GET",
|
|
1490
|
+
f"/memory/ontology/registry/{registry_id}/snapshots",
|
|
1491
|
+
params={"limit": limit},
|
|
1492
|
+
)
|
|
1493
|
+
|
|
1494
|
+
def get_registry_changelog(
|
|
1495
|
+
self, registry_id: str, limit: int = 50
|
|
1496
|
+
) -> Dict[str, Any]:
|
|
1497
|
+
"""Get change history for a registry."""
|
|
1498
|
+
return self._request(
|
|
1499
|
+
"GET",
|
|
1500
|
+
f"/memory/ontology/registry/{registry_id}/changelog",
|
|
1501
|
+
params={"limit": limit},
|
|
1502
|
+
)
|
|
1503
|
+
|
|
1504
|
+
def rollback_registry(
|
|
1505
|
+
self, registry_id: str, target_version: str = "", message: str = ""
|
|
1506
|
+
) -> Dict[str, Any]:
|
|
1507
|
+
"""Rollback registry to a previous version."""
|
|
1508
|
+
body = {"target_version": target_version, "message": message}
|
|
1509
|
+
return self._request(
|
|
1510
|
+
"POST", f"/memory/ontology/registry/{registry_id}/rollback", json_body=body
|
|
1511
|
+
)
|
|
1512
|
+
|
|
1513
|
+
def export_registry(
|
|
1514
|
+
self,
|
|
1515
|
+
registry_id: str,
|
|
1516
|
+
version: Optional[str] = None,
|
|
1517
|
+
user_id: Optional[str] = None,
|
|
1518
|
+
) -> Dict[str, Any]:
|
|
1519
|
+
"""Export a registry snapshot."""
|
|
1520
|
+
params = {}
|
|
1521
|
+
if version:
|
|
1522
|
+
params["version"] = version
|
|
1523
|
+
if user_id:
|
|
1524
|
+
params["user_id"] = user_id
|
|
1525
|
+
return self._request(
|
|
1526
|
+
"GET", f"/memory/ontology/registry/{registry_id}/export", params=params
|
|
1527
|
+
)
|
|
1528
|
+
|
|
1529
|
+
def import_registry(
|
|
1530
|
+
self, registry_id: str, data: Dict[str, Any], message: str = ""
|
|
1531
|
+
) -> Dict[str, Any]:
|
|
1532
|
+
"""Import data into a registry."""
|
|
1533
|
+
body = {"data": data, "message": message}
|
|
1534
|
+
return self._request(
|
|
1535
|
+
"POST", f"/memory/ontology/registry/{registry_id}/import", json_body=body
|
|
1536
|
+
)
|
|
1537
|
+
|
|
1538
|
+
def list_enrichment_providers(self) -> Dict[str, Any]:
|
|
1539
|
+
"""List available enrichment providers."""
|
|
1540
|
+
return self._request("GET", "/memory/ontology/enrichment/providers")
|
|
1541
|
+
|
|
1542
|
+
def run_enrichment(
|
|
1543
|
+
self,
|
|
1544
|
+
entities: List[str],
|
|
1545
|
+
provider: str = "wikipedia",
|
|
1546
|
+
user_id: Optional[str] = None,
|
|
1547
|
+
) -> Dict[str, Any]:
|
|
1548
|
+
"""Run enrichment operation using specified provider."""
|
|
1549
|
+
body = {"provider": provider, "entities": entities, "user_id": user_id}
|
|
1550
|
+
return self._request("POST", "/memory/ontology/enrichment/run", json_body=body)
|
|
1551
|
+
|
|
1552
|
+
def run_grounding_ontology(
|
|
1553
|
+
self,
|
|
1554
|
+
item_id: str,
|
|
1555
|
+
candidates: List[str],
|
|
1556
|
+
grounder: str = "wikipedia",
|
|
1557
|
+
user_id: Optional[str] = None,
|
|
1558
|
+
) -> Dict[str, Any]:
|
|
1559
|
+
"""Run grounding operation using specified grounder."""
|
|
1560
|
+
body = {
|
|
1561
|
+
"grounder": grounder,
|
|
1562
|
+
"item_id": item_id,
|
|
1563
|
+
"candidates": candidates,
|
|
1564
|
+
"user_id": user_id,
|
|
1565
|
+
}
|
|
1566
|
+
return self._request("POST", "/memory/ontology/grounding/run", json_body=body)
|
|
1567
|
+
|
|
1568
|
+
# ============================================================================
|
|
1569
|
+
# Pipeline
|
|
1570
|
+
# ============================================================================
|
|
1571
|
+
|
|
1572
|
+
def run_extraction_stage(
|
|
1573
|
+
self, content: str, extractor_name: str = "llm"
|
|
1574
|
+
) -> Dict[str, Any]:
|
|
1575
|
+
"""Run extraction pipeline stage."""
|
|
1576
|
+
body = {"content": content, "extractor_name": extractor_name}
|
|
1577
|
+
return self._request("POST", "/memory/pipeline/extraction", json_body=body)
|
|
1578
|
+
|
|
1579
|
+
def run_storage_stage(
|
|
1580
|
+
self, extracted_data: Dict[str, Any], storage_strategy: str = "standard"
|
|
1581
|
+
) -> Dict[str, Any]:
|
|
1582
|
+
"""Run storage pipeline stage."""
|
|
1583
|
+
body = {"extracted_data": extracted_data, "storage_strategy": storage_strategy}
|
|
1584
|
+
return self._request("POST", "/memory/pipeline/storage", json_body=body)
|
|
1585
|
+
|
|
1586
|
+
def run_linking_stage(
|
|
1587
|
+
self, stored_entities: List[Dict[str, Any]], linking_algorithm: str = "exact"
|
|
1588
|
+
) -> Dict[str, Any]:
|
|
1589
|
+
"""Run linking pipeline stage."""
|
|
1590
|
+
body = {
|
|
1591
|
+
"stored_entities": stored_entities,
|
|
1592
|
+
"linking_algorithm": linking_algorithm,
|
|
1593
|
+
}
|
|
1594
|
+
return self._request("POST", "/memory/pipeline/linking", json_body=body)
|
|
1595
|
+
|
|
1596
|
+
def run_enrichment_stage(
|
|
1597
|
+
self,
|
|
1598
|
+
linked_entities: List[Dict[str, Any]],
|
|
1599
|
+
enrichment_types: Optional[List[str]] = None,
|
|
1600
|
+
) -> Dict[str, Any]:
|
|
1601
|
+
"""Run enrichment pipeline stage."""
|
|
1602
|
+
body = {
|
|
1603
|
+
"linked_entities": linked_entities,
|
|
1604
|
+
"enrichment_types": enrichment_types or ["sentiment", "topics"],
|
|
1605
|
+
}
|
|
1606
|
+
return self._request("POST", "/memory/pipeline/enrichment", json_body=body)
|
|
1607
|
+
|
|
1608
|
+
def run_grounding_stage(
|
|
1609
|
+
self,
|
|
1610
|
+
enriched_entities: List[Dict[str, Any]],
|
|
1611
|
+
grounding_sources: Optional[List[str]] = None,
|
|
1612
|
+
) -> Dict[str, Any]:
|
|
1613
|
+
"""Run grounding pipeline stage."""
|
|
1614
|
+
body = {
|
|
1615
|
+
"enriched_entities": enriched_entities,
|
|
1616
|
+
"grounding_sources": grounding_sources or ["wikipedia"],
|
|
1617
|
+
}
|
|
1618
|
+
return self._request("POST", "/memory/pipeline/grounding", json_body=body)
|
|
1619
|
+
|
|
1620
|
+
def get_pipeline_state(
|
|
1621
|
+
self, pipeline_id: str, run_id: Optional[str] = None
|
|
1622
|
+
) -> Dict[str, Any]:
|
|
1623
|
+
"""Get pipeline state for a specific pipeline and run."""
|
|
1624
|
+
params = {"run_id": run_id} if run_id else None
|
|
1625
|
+
return self._request(
|
|
1626
|
+
"GET", f"/memory/pipeline/{pipeline_id}/state", params=params
|
|
1627
|
+
)
|
|
1628
|
+
|
|
1629
|
+
def reset_pipeline(self, pipeline_id: str) -> Dict[str, Any]:
|
|
1630
|
+
"""Reset a pipeline, clearing its state."""
|
|
1631
|
+
return self._request("DELETE", f"/memory/pipeline/{pipeline_id}")
|
|
1632
|
+
|
|
1633
|
+
def clear_run_state(self, pipeline_id: str, run_id: str) -> Dict[str, Any]:
|
|
1634
|
+
"""Clear all stage states for a specific pipeline run."""
|
|
1635
|
+
return self._request("DELETE", f"/memory/pipeline/{pipeline_id}/run/{run_id}")
|
|
1636
|
+
|
|
1637
|
+
# ============================================================================
|
|
1638
|
+
# Subscription
|
|
1639
|
+
# ============================================================================
|
|
1640
|
+
|
|
1641
|
+
def create_checkout_session(
|
|
1642
|
+
self,
|
|
1643
|
+
tier: str,
|
|
1644
|
+
billing_period: str = "monthly",
|
|
1645
|
+
trial_days: Optional[int] = None,
|
|
1646
|
+
) -> Dict[str, Any]:
|
|
1647
|
+
"""Create a Stripe Checkout session for subscription upgrade."""
|
|
1648
|
+
body = {
|
|
1649
|
+
"tier": tier,
|
|
1650
|
+
"billing_period": billing_period,
|
|
1651
|
+
"trial_days": trial_days,
|
|
1652
|
+
}
|
|
1653
|
+
return self._request("POST", "/subscription/checkout", json_body=body)
|
|
1654
|
+
|
|
1655
|
+
def upgrade_subscription(
|
|
1656
|
+
self,
|
|
1657
|
+
tier: str,
|
|
1658
|
+
billing_period: str = "monthly",
|
|
1659
|
+
payment_method_id: Optional[str] = None,
|
|
1660
|
+
use_checkout: bool = False,
|
|
1661
|
+
) -> Dict[str, Any]:
|
|
1662
|
+
"""Upgrade subscription tier."""
|
|
1663
|
+
body = {
|
|
1664
|
+
"tier": tier,
|
|
1665
|
+
"billing_period": billing_period,
|
|
1666
|
+
"payment_method_id": payment_method_id,
|
|
1667
|
+
"use_checkout": use_checkout,
|
|
1668
|
+
}
|
|
1669
|
+
return self._request("POST", "/subscription/upgrade", json_body=body)
|
|
1670
|
+
|
|
1671
|
+
def get_subscription(self) -> Dict[str, Any]:
|
|
1672
|
+
"""Get current subscription details."""
|
|
1673
|
+
return self._request("GET", "/subscription/current")
|
|
1674
|
+
|
|
1675
|
+
def cancel_subscription(self, immediately: bool = False) -> Dict[str, Any]:
|
|
1676
|
+
"""Cancel subscription."""
|
|
1677
|
+
return self._request(
|
|
1678
|
+
"POST", "/subscription/cancel", params={"immediately": immediately}
|
|
1679
|
+
)
|
|
1680
|
+
|
|
1681
|
+
# ============================================================================
|
|
1682
|
+
# Teams
|
|
1683
|
+
# ============================================================================
|
|
1684
|
+
|
|
1685
|
+
def create_team(
|
|
1686
|
+
self,
|
|
1687
|
+
name: str,
|
|
1688
|
+
description: Optional[str] = None,
|
|
1689
|
+
data_classification: str = "internal",
|
|
1690
|
+
cost_center: Optional[str] = None,
|
|
1691
|
+
) -> Dict[str, Any]:
|
|
1692
|
+
"""Create a new team."""
|
|
1693
|
+
body = {
|
|
1694
|
+
"name": name,
|
|
1695
|
+
"description": description,
|
|
1696
|
+
"data_classification": data_classification,
|
|
1697
|
+
"cost_center": cost_center,
|
|
1698
|
+
}
|
|
1699
|
+
return self._request("POST", "/memory/teams", json_body=body)
|
|
1700
|
+
|
|
1701
|
+
def list_teams(self) -> List[Dict[str, Any]]:
|
|
1702
|
+
"""List all teams the user has access to."""
|
|
1703
|
+
return self._request("GET", "/memory/teams")
|
|
1704
|
+
|
|
1705
|
+
def get_team(self, team_id: str) -> Dict[str, Any]:
|
|
1706
|
+
"""Get details of a specific team."""
|
|
1707
|
+
return self._request("GET", f"/memory/teams/{team_id}")
|
|
1708
|
+
|
|
1709
|
+
def update_team(
|
|
1710
|
+
self,
|
|
1711
|
+
team_id: str,
|
|
1712
|
+
name: Optional[str] = None,
|
|
1713
|
+
description: Optional[str] = None,
|
|
1714
|
+
data_classification: Optional[str] = None,
|
|
1715
|
+
cost_center: Optional[str] = None,
|
|
1716
|
+
) -> Dict[str, Any]:
|
|
1717
|
+
"""Update team details."""
|
|
1718
|
+
body = {}
|
|
1719
|
+
if name:
|
|
1720
|
+
body["name"] = name
|
|
1721
|
+
if description:
|
|
1722
|
+
body["description"] = description
|
|
1723
|
+
if data_classification:
|
|
1724
|
+
body["data_classification"] = data_classification
|
|
1725
|
+
if cost_center:
|
|
1726
|
+
body["cost_center"] = cost_center
|
|
1727
|
+
return self._request("PATCH", f"/memory/teams/{team_id}", json_body=body)
|
|
1728
|
+
|
|
1729
|
+
def delete_team(self, team_id: str) -> Dict[str, Any]:
|
|
1730
|
+
"""Delete a team."""
|
|
1731
|
+
return self._request("DELETE", f"/memory/teams/{team_id}")
|
|
1732
|
+
|
|
1733
|
+
def list_team_members(self, team_id: str) -> Dict[str, Any]:
|
|
1734
|
+
"""List all members of a team."""
|
|
1735
|
+
return self._request("GET", f"/memory/teams/{team_id}/members")
|
|
1736
|
+
|
|
1737
|
+
def add_team_member(
|
|
1738
|
+
self, team_id: str, user_id: str, role: str = "member"
|
|
1739
|
+
) -> Dict[str, Any]:
|
|
1740
|
+
"""Add a user to a team."""
|
|
1741
|
+
body = {"user_id": user_id, "role": role}
|
|
1742
|
+
return self._request("POST", f"/memory/teams/{team_id}/members", json_body=body)
|
|
1743
|
+
|
|
1744
|
+
def update_team_member(
|
|
1745
|
+
self, team_id: str, member_user_id: str, role: str
|
|
1746
|
+
) -> Dict[str, Any]:
|
|
1747
|
+
"""Update a team member's role."""
|
|
1748
|
+
body = {"role": role}
|
|
1749
|
+
return self._request(
|
|
1750
|
+
"PATCH", f"/memory/teams/{team_id}/members/{member_user_id}", json_body=body
|
|
1751
|
+
)
|
|
1752
|
+
|
|
1753
|
+
def remove_team_member(self, team_id: str, member_user_id: str) -> Dict[str, Any]:
|
|
1754
|
+
"""Remove a user from a team."""
|
|
1755
|
+
return self._request(
|
|
1756
|
+
"DELETE", f"/memory/teams/{team_id}/members/{member_user_id}"
|
|
1757
|
+
)
|
|
1758
|
+
|
|
1759
|
+
def get_team_permissions(self, team_id: str) -> Dict[str, Any]:
|
|
1760
|
+
"""Get available permissions for a team."""
|
|
1761
|
+
return self._request("GET", f"/memory/teams/{team_id}/permissions")
|
|
1762
|
+
|
|
1763
|
+
# ============================================================================
|
|
1764
|
+
# Temporal
|
|
1765
|
+
# ============================================================================
|
|
1766
|
+
|
|
1767
|
+
def get_history(
|
|
1768
|
+
self,
|
|
1769
|
+
item_id: str,
|
|
1770
|
+
start_time: Optional[str] = None,
|
|
1771
|
+
end_time: Optional[str] = None,
|
|
1772
|
+
limit: int = 100,
|
|
1773
|
+
) -> Dict[str, Any]:
|
|
1774
|
+
"""Get complete version history of a memory item."""
|
|
1775
|
+
params = {"limit": limit}
|
|
1776
|
+
if start_time:
|
|
1777
|
+
params["start_time"] = start_time
|
|
1778
|
+
if end_time:
|
|
1779
|
+
params["end_time"] = end_time
|
|
1780
|
+
return self._request(
|
|
1781
|
+
"GET", f"/memory/temporal/{item_id}/history", params=params
|
|
1782
|
+
)
|
|
1783
|
+
|
|
1784
|
+
def time_travel(
|
|
1785
|
+
self, timestamp: str, query: Optional[str] = None, limit: int = 100
|
|
1786
|
+
) -> Dict[str, Any]:
|
|
1787
|
+
"""Time-travel query - get state at specific time."""
|
|
1788
|
+
params = {"limit": limit}
|
|
1789
|
+
if query:
|
|
1790
|
+
params["query"] = query
|
|
1791
|
+
return self._request("GET", f"/memory/temporal/at/{timestamp}", params=params)
|
|
1792
|
+
|
|
1793
|
+
def get_item_at_time(self, item_id: str, timestamp: str) -> Dict[str, Any]:
|
|
1794
|
+
"""Get specific item as it existed at timestamp."""
|
|
1795
|
+
return self._request("GET", f"/memory/temporal/{item_id}/at/{timestamp}")
|
|
1796
|
+
|
|
1797
|
+
def get_changes(
|
|
1798
|
+
self,
|
|
1799
|
+
item_id: str,
|
|
1800
|
+
since: Optional[str] = None,
|
|
1801
|
+
until: Optional[str] = None,
|
|
1802
|
+
change_type: Optional[str] = None,
|
|
1803
|
+
) -> Dict[str, Any]:
|
|
1804
|
+
"""Get all changes to an item in time range."""
|
|
1805
|
+
params = {}
|
|
1806
|
+
if since:
|
|
1807
|
+
params["since"] = since
|
|
1808
|
+
if until:
|
|
1809
|
+
params["until"] = until
|
|
1810
|
+
if change_type:
|
|
1811
|
+
params["change_type"] = change_type
|
|
1812
|
+
return self._request(
|
|
1813
|
+
"GET", f"/memory/temporal/{item_id}/changes", params=params
|
|
1814
|
+
)
|
|
1815
|
+
|
|
1816
|
+
def compare_versions(self, item_id: str, v1: int, v2: int) -> Dict[str, Any]:
|
|
1817
|
+
"""Compare two versions of an item."""
|
|
1818
|
+
params = {"v1": v1, "v2": v2}
|
|
1819
|
+
return self._request(
|
|
1820
|
+
"POST", f"/memory/temporal/{item_id}/compare", params=params
|
|
1821
|
+
)
|
|
1822
|
+
|
|
1823
|
+
def rollback(
|
|
1824
|
+
self,
|
|
1825
|
+
item_id: str,
|
|
1826
|
+
to_version: Optional[int] = None,
|
|
1827
|
+
to_time: Optional[str] = None,
|
|
1828
|
+
) -> Dict[str, Any]:
|
|
1829
|
+
"""Rollback item to previous version."""
|
|
1830
|
+
params = {}
|
|
1831
|
+
if to_version:
|
|
1832
|
+
params["to_version"] = to_version
|
|
1833
|
+
if to_time:
|
|
1834
|
+
params["to_time"] = to_time
|
|
1835
|
+
return self._request(
|
|
1836
|
+
"POST", f"/memory/temporal/{item_id}/rollback", params=params
|
|
1837
|
+
)
|
|
1838
|
+
|
|
1839
|
+
def get_audit_trail(
|
|
1840
|
+
self,
|
|
1841
|
+
item_id: str,
|
|
1842
|
+
change_type: Optional[str] = None,
|
|
1843
|
+
user_id: Optional[str] = None,
|
|
1844
|
+
start_time: Optional[str] = None,
|
|
1845
|
+
end_time: Optional[str] = None,
|
|
1846
|
+
) -> Dict[str, Any]:
|
|
1847
|
+
"""Get complete audit trail for an item."""
|
|
1848
|
+
params = {}
|
|
1849
|
+
if change_type:
|
|
1850
|
+
params["change_type"] = change_type
|
|
1851
|
+
if user_id:
|
|
1852
|
+
params["user_id"] = user_id
|
|
1853
|
+
if start_time:
|
|
1854
|
+
params["start_time"] = start_time
|
|
1855
|
+
if end_time:
|
|
1856
|
+
params["end_time"] = end_time
|
|
1857
|
+
return self._request("GET", f"/memory/temporal/{item_id}/audit", params=params)
|
|
1858
|
+
|
|
1859
|
+
def search_during_range(
|
|
1860
|
+
self, query: str, start_time: str, end_time: str, limit: int = 100
|
|
1861
|
+
) -> Dict[str, Any]:
|
|
1862
|
+
"""Search memories that existed during time range."""
|
|
1863
|
+
params = {
|
|
1864
|
+
"query": query,
|
|
1865
|
+
"start_time": start_time,
|
|
1866
|
+
"end_time": end_time,
|
|
1867
|
+
"limit": limit,
|
|
1868
|
+
}
|
|
1869
|
+
return self._request("GET", "/memory/temporal/search/during", params=params)
|
|
1870
|
+
|
|
1871
|
+
def generate_compliance_report(
|
|
1872
|
+
self,
|
|
1873
|
+
start_date: str,
|
|
1874
|
+
end_date: str,
|
|
1875
|
+
report_type: str = "HIPAA",
|
|
1876
|
+
item_ids: Optional[List[str]] = None,
|
|
1877
|
+
) -> Dict[str, Any]:
|
|
1878
|
+
"""Generate compliance report (HIPAA, GDPR, SOC2)."""
|
|
1879
|
+
params = {
|
|
1880
|
+
"start_date": start_date,
|
|
1881
|
+
"end_date": end_date,
|
|
1882
|
+
"report_type": report_type,
|
|
1883
|
+
}
|
|
1884
|
+
if item_ids:
|
|
1885
|
+
params["item_ids"] = item_ids
|
|
1886
|
+
return self._request("GET", "/memory/temporal/compliance/report", params=params)
|
|
1887
|
+
|
|
1888
|
+
def get_relationship_history(self, rel_id: str) -> Dict[str, Any]:
|
|
1889
|
+
"""Get history of a relationship."""
|
|
1890
|
+
return self._request("GET", f"/memory/temporal/relationships/{rel_id}/history")
|
|
1891
|
+
|
|
1892
|
+
def get_relationships_at_time(
|
|
1893
|
+
self, timestamp: str, limit: int = 100
|
|
1894
|
+
) -> Dict[str, Any]:
|
|
1895
|
+
"""Get all relationships that existed at specific time."""
|
|
1896
|
+
params = {"limit": limit}
|
|
1897
|
+
return self._request(
|
|
1898
|
+
"GET", f"/memory/temporal/relationships/at/{timestamp}", params=params
|
|
1899
|
+
)
|
|
1900
|
+
|
|
1901
|
+
def get_relationship_valid_periods(self, rel_id: str) -> Dict[str, Any]:
|
|
1902
|
+
"""Get valid time periods for a relationship."""
|
|
1903
|
+
return self._request(
|
|
1904
|
+
"GET", f"/memory/temporal/relationships/{rel_id}/valid-periods"
|
|
1905
|
+
)
|
|
1906
|
+
|
|
1907
|
+
# ============================================================================
|
|
1908
|
+
# Usage
|
|
1909
|
+
# ============================================================================
|
|
1910
|
+
|
|
1911
|
+
def get_usage_dashboard(self) -> Dict[str, Any]:
|
|
1912
|
+
"""Get usage dashboard with current quotas and limits."""
|
|
1913
|
+
return self._request("GET", "/usage/dashboard")
|
|
1914
|
+
|
|
1915
|
+
def get_usage_limits(self) -> Dict[str, Any]:
|
|
1916
|
+
"""Get quota limits for current subscription tier."""
|
|
1917
|
+
return self._request("GET", "/usage/limits")
|
|
1918
|
+
|
|
1919
|
+
def get_current_usage(self) -> Dict[str, Any]:
|
|
1920
|
+
"""Get current usage statistics."""
|
|
1921
|
+
return self._request("GET", "/usage/current")
|
|
1922
|
+
|
|
1923
|
+
def get_available_tiers(self) -> Dict[str, Any]:
|
|
1924
|
+
"""Get available subscription tiers."""
|
|
1925
|
+
return self._request("GET", "/usage/tiers")
|
|
1926
|
+
|
|
1927
|
+
# ============================================================================
|
|
1928
|
+
# Token Usage (CFS-1)
|
|
1929
|
+
# ============================================================================
|
|
1930
|
+
|
|
1931
|
+
def get_token_usage(
|
|
1932
|
+
self,
|
|
1933
|
+
start_date: Optional[str] = None,
|
|
1934
|
+
end_date: Optional[str] = None,
|
|
1935
|
+
group_by: Optional[str] = None,
|
|
1936
|
+
limit: int = 100,
|
|
1937
|
+
) -> Dict[str, Any]:
|
|
1938
|
+
"""Get aggregated token usage history for the current workspace.
|
|
1939
|
+
|
|
1940
|
+
Args:
|
|
1941
|
+
start_date: ISO date string (inclusive), e.g. "2026-02-01"
|
|
1942
|
+
end_date: ISO date string (inclusive), e.g. "2026-02-11"
|
|
1943
|
+
group_by: Group results by "stage", "profile", or "day"
|
|
1944
|
+
limit: Max records to return (1-1000, default 100)
|
|
1945
|
+
|
|
1946
|
+
Returns:
|
|
1947
|
+
Dict with workspace_id, record_count, total_spent, total_avoided,
|
|
1948
|
+
savings_pct, records list, and optional grouping data.
|
|
1949
|
+
"""
|
|
1950
|
+
params: Dict[str, Any] = {"limit": limit}
|
|
1951
|
+
if start_date:
|
|
1952
|
+
params["start_date"] = start_date
|
|
1953
|
+
if end_date:
|
|
1954
|
+
params["end_date"] = end_date
|
|
1955
|
+
if group_by:
|
|
1956
|
+
params["group_by"] = group_by
|
|
1957
|
+
return self._request("GET", "/memory/token-usage", params=params)
|
|
1958
|
+
|
|
1959
|
+
def get_token_usage_current(self) -> Dict[str, Any]:
|
|
1960
|
+
"""Get real-time token usage: cache stats + last 10 pipeline runs.
|
|
1961
|
+
|
|
1962
|
+
Returns:
|
|
1963
|
+
Dict with workspace_id, cache_stats, and recent_runs list.
|
|
1964
|
+
"""
|
|
1965
|
+
return self._request("GET", "/memory/token-usage/current")
|
|
1966
|
+
|
|
1967
|
+
# ============================================================================
|
|
1968
|
+
# Procedure Matches (CFS-2)
|
|
1969
|
+
# ============================================================================
|
|
1970
|
+
|
|
1971
|
+
def list_procedure_matches(
|
|
1972
|
+
self,
|
|
1973
|
+
start_date: Optional[str] = None,
|
|
1974
|
+
end_date: Optional[str] = None,
|
|
1975
|
+
procedure_id: Optional[str] = None,
|
|
1976
|
+
feedback: Optional[str] = None,
|
|
1977
|
+
limit: int = 100,
|
|
1978
|
+
) -> Dict[str, Any]:
|
|
1979
|
+
"""List procedure match history for the current workspace.
|
|
1980
|
+
|
|
1981
|
+
Args:
|
|
1982
|
+
start_date: ISO date string (inclusive), e.g. "2026-02-01"
|
|
1983
|
+
end_date: ISO date string (inclusive), e.g. "2026-02-12"
|
|
1984
|
+
procedure_id: Filter by matched procedure ID
|
|
1985
|
+
feedback: Filter by feedback value: "success", "failure", or "neutral"
|
|
1986
|
+
limit: Max records to return (1-1000, default 100)
|
|
1987
|
+
|
|
1988
|
+
Returns:
|
|
1989
|
+
Dict with workspace_id, record_count, and records list.
|
|
1990
|
+
"""
|
|
1991
|
+
params: Dict[str, Any] = {"limit": limit}
|
|
1992
|
+
if start_date:
|
|
1993
|
+
params["start_date"] = start_date
|
|
1994
|
+
if end_date:
|
|
1995
|
+
params["end_date"] = end_date
|
|
1996
|
+
if procedure_id:
|
|
1997
|
+
params["procedure_id"] = procedure_id
|
|
1998
|
+
if feedback:
|
|
1999
|
+
params["feedback"] = feedback
|
|
2000
|
+
return self._request("GET", "/memory/procedures/matches", params=params)
|
|
2001
|
+
|
|
2002
|
+
def submit_procedure_match_feedback(
|
|
2003
|
+
self,
|
|
2004
|
+
match_id: str,
|
|
2005
|
+
feedback: str,
|
|
2006
|
+
note: Optional[str] = None,
|
|
2007
|
+
) -> Dict[str, Any]:
|
|
2008
|
+
"""Submit feedback for a procedure match.
|
|
2009
|
+
|
|
2010
|
+
Args:
|
|
2011
|
+
match_id: The match ID (uuid) from ingest response or match list.
|
|
2012
|
+
feedback: One of "success", "failure", "neutral".
|
|
2013
|
+
note: Optional explanation (max 500 chars).
|
|
2014
|
+
|
|
2015
|
+
Returns:
|
|
2016
|
+
Dict with status, match_id, and feedback.
|
|
2017
|
+
"""
|
|
2018
|
+
body: Dict[str, Any] = {"feedback": feedback}
|
|
2019
|
+
if note:
|
|
2020
|
+
body["note"] = note
|
|
2021
|
+
return self._request(
|
|
2022
|
+
"POST", f"/memory/procedures/matches/{match_id}/feedback", json_body=body
|
|
2023
|
+
)
|
|
2024
|
+
|
|
2025
|
+
def submit_result_feedback(
|
|
2026
|
+
self,
|
|
2027
|
+
session_id: str,
|
|
2028
|
+
result_used: List[str],
|
|
2029
|
+
) -> Dict[str, Any]:
|
|
2030
|
+
"""Submit result-selection feedback for a completed search session (SELF-IMPROVE-6).
|
|
2031
|
+
|
|
2032
|
+
Call this after using results from ``search()`` to tell SmartMemory which
|
|
2033
|
+
results were actually useful. The ``session_id`` is available in the
|
|
2034
|
+
``X-Search-Session-Id`` response header from ``POST /memory/search``.
|
|
2035
|
+
|
|
2036
|
+
Args:
|
|
2037
|
+
session_id: Server-generated session ID from the search response
|
|
2038
|
+
(``X-Search-Session-Id`` header).
|
|
2039
|
+
result_used: Item IDs from the search results that you incorporated.
|
|
2040
|
+
Pass an empty list if none of the results were useful.
|
|
2041
|
+
|
|
2042
|
+
Returns:
|
|
2043
|
+
Dict with ``status``, ``search_session_id``, ``result_used_count``,
|
|
2044
|
+
``result_shown_count``.
|
|
2045
|
+
|
|
2046
|
+
Raises:
|
|
2047
|
+
HTTPError 404: Session not found or expired (> 1 hour since search).
|
|
2048
|
+
HTTPError 400: result_used contains IDs not in the original result set.
|
|
2049
|
+
HTTPError 409: Feedback already submitted for this session.
|
|
2050
|
+
"""
|
|
2051
|
+
return self._request(
|
|
2052
|
+
"POST",
|
|
2053
|
+
"/memory/result-feedback",
|
|
2054
|
+
json_body={"search_session_id": session_id, "result_used": result_used},
|
|
2055
|
+
)
|
|
2056
|
+
|
|
2057
|
+
def get_procedure_match_stats(self) -> Dict[str, Any]:
|
|
2058
|
+
"""Get aggregated procedure match statistics for the current workspace.
|
|
2059
|
+
|
|
2060
|
+
Returns:
|
|
2061
|
+
Dict with total_matches, successful, failed, neutral, no_feedback,
|
|
2062
|
+
avg_confidence, and by_procedure breakdown.
|
|
2063
|
+
"""
|
|
2064
|
+
return self._request("GET", "/memory/procedures/matches/stats")
|
|
2065
|
+
|
|
2066
|
+
# ============================================================================
|
|
2067
|
+
# Procedure Catalog (CFS-3)
|
|
2068
|
+
# ============================================================================
|
|
2069
|
+
|
|
2070
|
+
def list_procedures(
|
|
2071
|
+
self,
|
|
2072
|
+
limit: int = 50,
|
|
2073
|
+
offset: int = 0,
|
|
2074
|
+
sort_by: Optional[str] = None,
|
|
2075
|
+
sort_order: str = "desc",
|
|
2076
|
+
) -> Dict[str, Any]:
|
|
2077
|
+
"""List all procedural memories with aggregated match statistics.
|
|
2078
|
+
|
|
2079
|
+
Args:
|
|
2080
|
+
limit: Max procedures to return (1-200, default 50)
|
|
2081
|
+
offset: Pagination offset (default 0)
|
|
2082
|
+
sort_by: Sort field - "match_count", "success_rate", "created_at", or "name"
|
|
2083
|
+
sort_order: Sort direction - "asc" or "desc" (default "desc")
|
|
2084
|
+
|
|
2085
|
+
Returns:
|
|
2086
|
+
Dict with workspace_id, total_count, and procedures list.
|
|
2087
|
+
Each procedure has id, name, description, created_at, metadata, and match_stats.
|
|
2088
|
+
"""
|
|
2089
|
+
params: Dict[str, Any] = {
|
|
2090
|
+
"limit": limit,
|
|
2091
|
+
"offset": offset,
|
|
2092
|
+
"sort_order": sort_order,
|
|
2093
|
+
}
|
|
2094
|
+
if sort_by:
|
|
2095
|
+
params["sort_by"] = sort_by
|
|
2096
|
+
return self._request("GET", "/memory/procedures", params=params)
|
|
2097
|
+
|
|
2098
|
+
def get_procedure(
|
|
2099
|
+
self,
|
|
2100
|
+
procedure_id: str,
|
|
2101
|
+
include_matches: bool = True,
|
|
2102
|
+
match_limit: int = 20,
|
|
2103
|
+
) -> Dict[str, Any]:
|
|
2104
|
+
"""Get full procedure detail with recent match history.
|
|
2105
|
+
|
|
2106
|
+
Args:
|
|
2107
|
+
procedure_id: The procedure ID to retrieve.
|
|
2108
|
+
include_matches: Whether to include recent match history (default True).
|
|
2109
|
+
match_limit: Max matches to include (1-100, default 20).
|
|
2110
|
+
|
|
2111
|
+
Returns:
|
|
2112
|
+
Dict with id, name, description, content, created_at, updated_at,
|
|
2113
|
+
metadata, match_stats, and optionally recent_matches list.
|
|
2114
|
+
|
|
2115
|
+
Raises:
|
|
2116
|
+
SmartMemoryClientError: If procedure not found (404).
|
|
2117
|
+
"""
|
|
2118
|
+
params: Dict[str, Any] = {
|
|
2119
|
+
"include_matches": include_matches,
|
|
2120
|
+
"match_limit": match_limit,
|
|
2121
|
+
}
|
|
2122
|
+
return self._request("GET", f"/memory/procedures/{procedure_id}", params=params)
|
|
2123
|
+
|
|
2124
|
+
# ============================================================================
|
|
2125
|
+
# Webhooks
|
|
2126
|
+
# ============================================================================
|
|
2127
|
+
|
|
2128
|
+
def trigger_stripe_webhook(
|
|
2129
|
+
self, payload: Dict[str, Any], signature: str
|
|
2130
|
+
) -> Dict[str, Any]:
|
|
2131
|
+
"""Trigger a Stripe webhook event (mostly for testing)."""
|
|
2132
|
+
# Note: This sends raw body, but our _request helper assumes JSON or params.
|
|
2133
|
+
# For webhooks, we might need to send raw bytes if we were simulating real Stripe calls.
|
|
2134
|
+
# However, the client is typically used to INTERACT with the API, not send webhooks TO it.
|
|
2135
|
+
# But if we need to simulate it:
|
|
2136
|
+
headers = {"stripe-signature": signature}
|
|
2137
|
+
# We'll use json_body for convenience, but real Stripe sends raw bytes.
|
|
2138
|
+
# This method might be limited by _request implementation if strict raw body is needed.
|
|
2139
|
+
return self._request(
|
|
2140
|
+
"POST", "/webhooks/stripe", json_body=payload, headers=headers
|
|
2141
|
+
)
|
|
2142
|
+
|
|
2143
|
+
# ============================================================================
|
|
2144
|
+
# Zettelkasten
|
|
2145
|
+
# ============================================================================
|
|
2146
|
+
|
|
2147
|
+
def get_backlinks(self, note_id: str) -> Dict[str, Any]:
|
|
2148
|
+
"""Get notes that link TO this note (backlinks)."""
|
|
2149
|
+
return self._request("GET", f"/memory/zettel/{note_id}/backlinks")
|
|
2150
|
+
|
|
2151
|
+
def get_forward_links(self, note_id: str) -> Dict[str, Any]:
|
|
2152
|
+
"""Get notes this note links TO (forward links)."""
|
|
2153
|
+
return self._request("GET", f"/memory/zettel/{note_id}/forward-links")
|
|
2154
|
+
|
|
2155
|
+
def get_connections(self, note_id: str) -> Dict[str, Any]:
|
|
2156
|
+
"""Get all connections (backlinks + forward links)."""
|
|
2157
|
+
return self._request("GET", f"/memory/zettel/{note_id}/connections")
|
|
2158
|
+
|
|
2159
|
+
def get_clusters(
|
|
2160
|
+
self, min_size: int = 3, algorithm: str = "louvain"
|
|
2161
|
+
) -> Dict[str, Any]:
|
|
2162
|
+
"""Detect knowledge clusters in your Zettelkasten."""
|
|
2163
|
+
params = {"min_size": min_size, "algorithm": algorithm}
|
|
2164
|
+
return self._request("GET", "/memory/zettel/clusters", params=params)
|
|
2165
|
+
|
|
2166
|
+
def get_hubs(self, min_connections: int = 5, limit: int = 20) -> Dict[str, Any]:
|
|
2167
|
+
"""Find hub notes (highly connected notes)."""
|
|
2168
|
+
params = {"min_connections": min_connections, "limit": limit}
|
|
2169
|
+
return self._request("GET", "/memory/zettel/hubs", params=params)
|
|
2170
|
+
|
|
2171
|
+
def get_bridges(self, limit: int = 20) -> Dict[str, Any]:
|
|
2172
|
+
"""Find bridge notes (notes connecting different clusters)."""
|
|
2173
|
+
params = {"limit": limit}
|
|
2174
|
+
return self._request("GET", "/memory/zettel/bridges", params=params)
|
|
2175
|
+
|
|
2176
|
+
def get_discoveries(
|
|
2177
|
+
self, note_id: str, max_distance: int = 3, min_surprise: float = 0.5
|
|
2178
|
+
) -> Dict[str, Any]:
|
|
2179
|
+
"""Find unexpected connections (serendipitous discovery)."""
|
|
2180
|
+
params = {"max_distance": max_distance, "min_surprise": min_surprise}
|
|
2181
|
+
return self._request(
|
|
2182
|
+
"GET", f"/memory/zettel/{note_id}/discoveries", params=params
|
|
2183
|
+
)
|
|
2184
|
+
|
|
2185
|
+
def get_path(
|
|
2186
|
+
self, note_id: str, target_id: str, max_paths: int = 5
|
|
2187
|
+
) -> Dict[str, Any]:
|
|
2188
|
+
"""Find paths between two notes."""
|
|
2189
|
+
params = {"max_paths": max_paths}
|
|
2190
|
+
return self._request(
|
|
2191
|
+
"GET", f"/memory/zettel/{note_id}/path/{target_id}", params=params
|
|
2192
|
+
)
|
|
2193
|
+
|
|
2194
|
+
def parse_wikilinks(self, content: str, auto_create: bool = True) -> Dict[str, Any]:
|
|
2195
|
+
"""Parse [[wikilinks]] in content."""
|
|
2196
|
+
params = {"content": content, "auto_create": auto_create}
|
|
2197
|
+
return self._request("POST", "/memory/zettel/wikilink/parse", params=params)
|
|
2198
|
+
|
|
2199
|
+
def resolve_wikilink(self, link: str) -> Dict[str, Any]:
|
|
2200
|
+
"""Resolve a wikilink to a note."""
|
|
2201
|
+
params = {"link": link}
|
|
2202
|
+
return self._request("GET", "/memory/zettel/wikilink/resolve", params=params)
|
|
2203
|
+
|
|
2204
|
+
def get_subgraph(
|
|
2205
|
+
self, note_id: str, depth: int = 2, include_metadata: bool = True
|
|
2206
|
+
) -> Dict[str, Any]:
|
|
2207
|
+
"""Get subgraph around a note (for visualization)."""
|
|
2208
|
+
params = {"depth": depth, "include_metadata": include_metadata}
|
|
2209
|
+
return self._request("GET", f"/memory/zettel/{note_id}/graph", params=params)
|
|
2210
|
+
|
|
2211
|
+
def detect_concept_emergence(self, limit: int = 20) -> Dict[str, Any]:
|
|
2212
|
+
"""Detect emerging concepts from connection patterns."""
|
|
2213
|
+
params = {"limit": limit}
|
|
2214
|
+
return self._request("GET", "/memory/zettel/concept-emergence", params=params)
|
|
2215
|
+
|
|
2216
|
+
def suggest_related_notes(self, note_id: str, count: int = 5) -> Dict[str, Any]:
|
|
2217
|
+
"""Suggest related notes for serendipitous discovery."""
|
|
2218
|
+
params = {"count": count}
|
|
2219
|
+
return self._request(
|
|
2220
|
+
"GET", f"/memory/zettel/{note_id}/suggestions", params=params
|
|
2221
|
+
)
|
|
2222
|
+
|
|
2223
|
+
def random_walk_discovery(self, note_id: str, length: int = 5) -> Dict[str, Any]:
|
|
2224
|
+
"""Perform random walk for serendipitous discovery."""
|
|
2225
|
+
params = {"length": length}
|
|
2226
|
+
return self._request(
|
|
2227
|
+
"GET", f"/memory/zettel/{note_id}/random-walk", params=params
|
|
2228
|
+
)
|
|
2229
|
+
|
|
2230
|
+
def find_notes_by_tag(self, tag: str, limit: int = 100) -> Dict[str, Any]:
|
|
2231
|
+
"""Find notes by tag."""
|
|
2232
|
+
params = {"limit": limit}
|
|
2233
|
+
return self._request("GET", f"/memory/zettel/by-tag/{tag}", params=params)
|
|
2234
|
+
|
|
2235
|
+
def find_notes_by_property(
|
|
2236
|
+
self, key: str, value: str, limit: int = 100
|
|
2237
|
+
) -> Dict[str, Any]:
|
|
2238
|
+
"""Find notes by property."""
|
|
2239
|
+
params = {"key": key, "value": value, "limit": limit}
|
|
2240
|
+
return self._request("GET", "/memory/zettel/by-property", params=params)
|
|
2241
|
+
|
|
2242
|
+
def find_notes_mentioning(self, entity_id: str, limit: int = 100) -> Dict[str, Any]:
|
|
2243
|
+
"""Find notes mentioning an entity."""
|
|
2244
|
+
params = {"limit": limit}
|
|
2245
|
+
return self._request(
|
|
2246
|
+
"GET", f"/memory/zettel/mentioning/{entity_id}", params=params
|
|
2247
|
+
)
|
|
2248
|
+
|
|
2249
|
+
def query_by_dynamic_relation(
|
|
2250
|
+
self, source_id: str, relation_type: str, limit: int = 100
|
|
2251
|
+
) -> Dict[str, Any]:
|
|
2252
|
+
"""Query notes by dynamic relation type."""
|
|
2253
|
+
params = {"limit": limit}
|
|
2254
|
+
return self._request(
|
|
2255
|
+
"GET",
|
|
2256
|
+
f"/memory/zettel/by-relation/{source_id}/{relation_type}",
|
|
2257
|
+
params=params,
|
|
2258
|
+
)
|
|
2259
|
+
|
|
2260
|
+
# =========================================================================
|
|
2261
|
+
# Reasoning / Assertion Challenging
|
|
2262
|
+
# =========================================================================
|
|
2263
|
+
|
|
2264
|
+
def challenge(
|
|
2265
|
+
self, assertion: str, memory_type: str = "semantic", use_llm: bool = True
|
|
2266
|
+
) -> Dict[str, Any]:
|
|
2267
|
+
"""
|
|
2268
|
+
Challenge an assertion against existing knowledge to detect contradictions.
|
|
2269
|
+
|
|
2270
|
+
Uses a multi-method detection cascade:
|
|
2271
|
+
1. LLM-based (if enabled) - most accurate
|
|
2272
|
+
2. Graph-based - structural analysis
|
|
2273
|
+
3. Embedding-based - semantic similarity + polarity
|
|
2274
|
+
4. Heuristic - pattern matching fallback
|
|
2275
|
+
|
|
2276
|
+
Args:
|
|
2277
|
+
assertion: The assertion to challenge
|
|
2278
|
+
memory_type: Type of memory to search (default: "semantic")
|
|
2279
|
+
use_llm: Use LLM for deep contradiction analysis
|
|
2280
|
+
|
|
2281
|
+
Returns:
|
|
2282
|
+
Challenge result with conflicts, confidence, etc.
|
|
2283
|
+
|
|
2284
|
+
Example:
|
|
2285
|
+
```python
|
|
2286
|
+
result = client.challenge("Paris is the capital of Germany")
|
|
2287
|
+
if result["has_conflicts"]:
|
|
2288
|
+
for conflict in result["conflicts"]:
|
|
2289
|
+
print(f"Contradicts: {conflict['existing_fact']}")
|
|
2290
|
+
```
|
|
2291
|
+
"""
|
|
2292
|
+
body = {"assertion": assertion, "memory_type": memory_type, "use_llm": use_llm}
|
|
2293
|
+
return self._request("POST", "/memory/reasoning/challenge", json_body=body)
|
|
2294
|
+
|
|
2295
|
+
def resolve_conflict(
|
|
2296
|
+
self,
|
|
2297
|
+
existing_item_id: str,
|
|
2298
|
+
new_fact: str,
|
|
2299
|
+
auto_resolve: bool = True,
|
|
2300
|
+
strategy: Optional[str] = None,
|
|
2301
|
+
use_wikipedia: bool = True,
|
|
2302
|
+
use_llm: bool = True,
|
|
2303
|
+
) -> Dict[str, Any]:
|
|
2304
|
+
"""
|
|
2305
|
+
Resolve a conflict between assertions.
|
|
2306
|
+
|
|
2307
|
+
Auto-resolution cascade (if enabled):
|
|
2308
|
+
1. Wikipedia lookup - verify against Wikipedia
|
|
2309
|
+
2. LLM reasoning - ask GPT to fact-check
|
|
2310
|
+
3. Grounding check - check existing provenance
|
|
2311
|
+
4. Recency heuristic - prefer recent info for temporal conflicts
|
|
2312
|
+
|
|
2313
|
+
Args:
|
|
2314
|
+
existing_item_id: ID of the existing memory item in conflict
|
|
2315
|
+
new_fact: The new fact that conflicts
|
|
2316
|
+
auto_resolve: Attempt auto-resolution before manual strategy
|
|
2317
|
+
strategy: Manual resolution strategy if auto fails
|
|
2318
|
+
("keep_existing", "accept_new", "keep_both", "defer")
|
|
2319
|
+
use_wikipedia: Use Wikipedia for verification
|
|
2320
|
+
use_llm: Use LLM for reasoning
|
|
2321
|
+
|
|
2322
|
+
Returns:
|
|
2323
|
+
Resolution result with method, evidence, confidence
|
|
2324
|
+
|
|
2325
|
+
Example:
|
|
2326
|
+
```python
|
|
2327
|
+
result = client.resolve_conflict(
|
|
2328
|
+
existing_item_id="item_123",
|
|
2329
|
+
new_fact="Paris is the capital of Germany",
|
|
2330
|
+
auto_resolve=True
|
|
2331
|
+
)
|
|
2332
|
+
if result["auto_resolved"]:
|
|
2333
|
+
print(f"Resolved via {result['method']}: {result['evidence']}")
|
|
2334
|
+
```
|
|
2335
|
+
"""
|
|
2336
|
+
body = {
|
|
2337
|
+
"existing_item_id": existing_item_id,
|
|
2338
|
+
"new_fact": new_fact,
|
|
2339
|
+
"auto_resolve": auto_resolve,
|
|
2340
|
+
"strategy": strategy,
|
|
2341
|
+
"use_wikipedia": use_wikipedia,
|
|
2342
|
+
"use_llm": use_llm,
|
|
2343
|
+
}
|
|
2344
|
+
return self._request("POST", "/memory/reasoning/resolve", json_body=body)
|
|
2345
|
+
|
|
2346
|
+
def list_conflicts(
|
|
2347
|
+
self, needs_review: bool = True, limit: int = 50
|
|
2348
|
+
) -> Dict[str, Any]:
|
|
2349
|
+
"""
|
|
2350
|
+
List memory items that have unresolved conflicts.
|
|
2351
|
+
|
|
2352
|
+
Args:
|
|
2353
|
+
needs_review: Filter to items needing review
|
|
2354
|
+
limit: Maximum number of items to return
|
|
2355
|
+
|
|
2356
|
+
Returns:
|
|
2357
|
+
List of conflicting items with details
|
|
2358
|
+
|
|
2359
|
+
Example:
|
|
2360
|
+
```python
|
|
2361
|
+
conflicts = client.list_conflicts()
|
|
2362
|
+
for item in conflicts["conflicts"]:
|
|
2363
|
+
print(f"{item['item_id']}: {item['review_reason']}")
|
|
2364
|
+
```
|
|
2365
|
+
"""
|
|
2366
|
+
params = {"needs_review": needs_review, "limit": limit}
|
|
2367
|
+
return self._request("GET", "/memory/reasoning/conflicts", params=params)
|
|
2368
|
+
|
|
2369
|
+
def get_low_confidence_items(
|
|
2370
|
+
self, threshold: float = 0.5, limit: int = 50
|
|
2371
|
+
) -> Dict[str, Any]:
|
|
2372
|
+
"""
|
|
2373
|
+
Get items with confidence below threshold.
|
|
2374
|
+
|
|
2375
|
+
Useful for finding facts that have been challenged multiple times
|
|
2376
|
+
and may need review or removal.
|
|
2377
|
+
|
|
2378
|
+
Args:
|
|
2379
|
+
threshold: Confidence threshold (0.0-1.0)
|
|
2380
|
+
limit: Maximum items to return
|
|
2381
|
+
|
|
2382
|
+
Returns:
|
|
2383
|
+
Items sorted by confidence (lowest first)
|
|
2384
|
+
|
|
2385
|
+
Example:
|
|
2386
|
+
```python
|
|
2387
|
+
low_conf = client.get_low_confidence_items(threshold=0.3)
|
|
2388
|
+
for item in low_conf["items"]:
|
|
2389
|
+
print(f"{item['item_id']}: {item['confidence']:.2f} ({item['challenge_count']} challenges)")
|
|
2390
|
+
```
|
|
2391
|
+
"""
|
|
2392
|
+
params = {"threshold": threshold, "limit": limit}
|
|
2393
|
+
return self._request("GET", "/memory/reasoning/low-confidence", params=params)
|
|
2394
|
+
|
|
2395
|
+
def get_confidence_history(self, item_id: str) -> Dict[str, Any]:
|
|
2396
|
+
"""
|
|
2397
|
+
Get the confidence decay history for a specific item.
|
|
2398
|
+
|
|
2399
|
+
Args:
|
|
2400
|
+
item_id: Memory item ID
|
|
2401
|
+
|
|
2402
|
+
Returns:
|
|
2403
|
+
Confidence history with timestamps, reasons, and conflicting facts
|
|
2404
|
+
|
|
2405
|
+
Example:
|
|
2406
|
+
```python
|
|
2407
|
+
history = client.get_confidence_history("item_123")
|
|
2408
|
+
print(f"Current confidence: {history['current_confidence']}")
|
|
2409
|
+
for event in history["history"]:
|
|
2410
|
+
print(f" {event['timestamp']}: {event['old_confidence']:.2f} -> {event['new_confidence']:.2f}")
|
|
2411
|
+
print(f" Reason: {event['reason']}")
|
|
2412
|
+
```
|
|
2413
|
+
"""
|
|
2414
|
+
return self._request("GET", f"/memory/reasoning/confidence-history/{item_id}")
|
|
2415
|
+
|
|
2416
|
+
# =========================================================================
|
|
2417
|
+
# Reasoning Traces (System 2 Memory)
|
|
2418
|
+
# =========================================================================
|
|
2419
|
+
|
|
2420
|
+
def extract_reasoning(
|
|
2421
|
+
self,
|
|
2422
|
+
content: str,
|
|
2423
|
+
min_steps: int = 2,
|
|
2424
|
+
min_quality_score: float = 0.4,
|
|
2425
|
+
use_llm_detection: bool = True,
|
|
2426
|
+
) -> Dict[str, Any]:
|
|
2427
|
+
"""
|
|
2428
|
+
Extract reasoning traces from content.
|
|
2429
|
+
|
|
2430
|
+
Detects chain-of-thought reasoning patterns (Thought:/Action:/Observation:).
|
|
2431
|
+
|
|
2432
|
+
Args:
|
|
2433
|
+
content: Content to extract reasoning from
|
|
2434
|
+
min_steps: Minimum steps required for a valid trace
|
|
2435
|
+
min_quality_score: Minimum quality score threshold
|
|
2436
|
+
use_llm_detection: Use LLM for implicit reasoning detection
|
|
2437
|
+
|
|
2438
|
+
Returns:
|
|
2439
|
+
Extraction result with trace, has_reasoning, quality_score, step_count
|
|
2440
|
+
|
|
2441
|
+
Example:
|
|
2442
|
+
```python
|
|
2443
|
+
result = client.extract_reasoning('''
|
|
2444
|
+
Thought: I need to analyze this bug.
|
|
2445
|
+
Action: Let me search for the function.
|
|
2446
|
+
Observation: Found the issue in line 42.
|
|
2447
|
+
Conclusion: The fix is to add a null check.
|
|
2448
|
+
''')
|
|
2449
|
+
if result['has_reasoning']:
|
|
2450
|
+
print(f"Found {result['step_count']} reasoning steps")
|
|
2451
|
+
```
|
|
2452
|
+
"""
|
|
2453
|
+
body = {
|
|
2454
|
+
"content": content,
|
|
2455
|
+
"min_steps": min_steps,
|
|
2456
|
+
"min_quality_score": min_quality_score,
|
|
2457
|
+
"use_llm_detection": use_llm_detection,
|
|
2458
|
+
}
|
|
2459
|
+
return self._request("POST", "/memory/reasoning-traces/extract", json_body=body)
|
|
2460
|
+
|
|
2461
|
+
def store_reasoning_trace(
|
|
2462
|
+
self, trace: Dict[str, Any], artifact_ids: Optional[List[str]] = None
|
|
2463
|
+
) -> Dict[str, Any]:
|
|
2464
|
+
"""
|
|
2465
|
+
Store a reasoning trace as a memory item.
|
|
2466
|
+
|
|
2467
|
+
Creates a 'reasoning' type memory with CAUSES relations to artifacts.
|
|
2468
|
+
|
|
2469
|
+
Args:
|
|
2470
|
+
trace: Reasoning trace dict with trace_id, steps, task_context
|
|
2471
|
+
artifact_ids: IDs of artifacts this reasoning produced
|
|
2472
|
+
|
|
2473
|
+
Returns:
|
|
2474
|
+
Storage result with trace_id, step_count, artifact_links
|
|
2475
|
+
|
|
2476
|
+
Example:
|
|
2477
|
+
```python
|
|
2478
|
+
result = client.store_reasoning_trace(
|
|
2479
|
+
trace={
|
|
2480
|
+
"trace_id": "trace_123",
|
|
2481
|
+
"steps": [
|
|
2482
|
+
{"type": "thought", "content": "Analyzing the problem"},
|
|
2483
|
+
{"type": "conclusion", "content": "Found the solution"},
|
|
2484
|
+
],
|
|
2485
|
+
"task_context": {"goal": "Fix bug", "domain": "python"},
|
|
2486
|
+
},
|
|
2487
|
+
artifact_ids=["code_fix_456"]
|
|
2488
|
+
)
|
|
2489
|
+
```
|
|
2490
|
+
"""
|
|
2491
|
+
body = {
|
|
2492
|
+
"trace": trace,
|
|
2493
|
+
"artifact_ids": artifact_ids,
|
|
2494
|
+
}
|
|
2495
|
+
return self._request("POST", "/memory/reasoning-traces/store", json_body=body)
|
|
2496
|
+
|
|
2497
|
+
def query_reasoning(
|
|
2498
|
+
self, query: str, artifact_id: Optional[str] = None, limit: int = 10
|
|
2499
|
+
) -> Dict[str, Any]:
|
|
2500
|
+
"""
|
|
2501
|
+
Query reasoning traces.
|
|
2502
|
+
|
|
2503
|
+
Use cases:
|
|
2504
|
+
- "Why did I choose Python?" → finds reasoning traces about Python decisions
|
|
2505
|
+
- artifact_id → finds reasoning that led to this artifact
|
|
2506
|
+
|
|
2507
|
+
Args:
|
|
2508
|
+
query: Query like "why did I choose X?"
|
|
2509
|
+
artifact_id: Find reasoning that led to this artifact
|
|
2510
|
+
limit: Maximum traces to return
|
|
2511
|
+
|
|
2512
|
+
Returns:
|
|
2513
|
+
Query result with traces list and count
|
|
2514
|
+
|
|
2515
|
+
Example:
|
|
2516
|
+
```python
|
|
2517
|
+
# Find reasoning about a decision
|
|
2518
|
+
result = client.query_reasoning("why did I use async/await?")
|
|
2519
|
+
for trace in result['traces']:
|
|
2520
|
+
print(f"Trace {trace['trace_id']}: {trace['content'][:100]}...")
|
|
2521
|
+
|
|
2522
|
+
# Find reasoning that led to an artifact
|
|
2523
|
+
result = client.query_reasoning("", artifact_id="code_123")
|
|
2524
|
+
```
|
|
2525
|
+
"""
|
|
2526
|
+
body = {
|
|
2527
|
+
"query": query,
|
|
2528
|
+
"artifact_id": artifact_id,
|
|
2529
|
+
"limit": limit,
|
|
2530
|
+
}
|
|
2531
|
+
return self._request("POST", "/memory/reasoning-traces/query", json_body=body)
|
|
2532
|
+
|
|
2533
|
+
def get_reasoning_trace(self, trace_id: str) -> Dict[str, Any]:
|
|
2534
|
+
"""
|
|
2535
|
+
Get a specific reasoning trace by ID.
|
|
2536
|
+
|
|
2537
|
+
Args:
|
|
2538
|
+
trace_id: Reasoning trace ID
|
|
2539
|
+
|
|
2540
|
+
Returns:
|
|
2541
|
+
Full reasoning trace with steps, task_context, artifact_ids
|
|
2542
|
+
"""
|
|
2543
|
+
return self._request("GET", f"/memory/reasoning-traces/{trace_id}")
|
|
2544
|
+
|
|
2545
|
+
# =========================================================================
|
|
2546
|
+
# Synthesis Evolution (Opinions & Observations)
|
|
2547
|
+
# =========================================================================
|
|
2548
|
+
|
|
2549
|
+
def synthesize_opinions(self) -> Dict[str, Any]:
|
|
2550
|
+
"""
|
|
2551
|
+
Run opinion synthesis: detect patterns in episodic memories and form opinions.
|
|
2552
|
+
|
|
2553
|
+
Creates 'opinion' type memories with confidence scores based on recurring patterns.
|
|
2554
|
+
|
|
2555
|
+
Returns:
|
|
2556
|
+
Synthesis result with status, message, timestamp
|
|
2557
|
+
|
|
2558
|
+
Example:
|
|
2559
|
+
```python
|
|
2560
|
+
result = client.synthesize_opinions()
|
|
2561
|
+
print(f"Status: {result['status']}")
|
|
2562
|
+
```
|
|
2563
|
+
"""
|
|
2564
|
+
return self._request("POST", "/memory/evolution/synthesize/opinions")
|
|
2565
|
+
|
|
2566
|
+
def synthesize_observations(self) -> Dict[str, Any]:
|
|
2567
|
+
"""
|
|
2568
|
+
Run observation synthesis: create entity summaries from scattered facts.
|
|
2569
|
+
|
|
2570
|
+
Creates 'observation' type memories that summarize what we know about entities.
|
|
2571
|
+
|
|
2572
|
+
Returns:
|
|
2573
|
+
Synthesis result with status, message, timestamp
|
|
2574
|
+
|
|
2575
|
+
Example:
|
|
2576
|
+
```python
|
|
2577
|
+
result = client.synthesize_observations()
|
|
2578
|
+
print(f"Status: {result['status']}")
|
|
2579
|
+
```
|
|
2580
|
+
"""
|
|
2581
|
+
return self._request("POST", "/memory/evolution/synthesize/observations")
|
|
2582
|
+
|
|
2583
|
+
def reinforce_opinions(self) -> Dict[str, Any]:
|
|
2584
|
+
"""
|
|
2585
|
+
Run opinion reinforcement: update confidence scores based on new evidence.
|
|
2586
|
+
|
|
2587
|
+
Reinforces or contradicts existing opinions based on recent episodic memories.
|
|
2588
|
+
Archives opinions that fall below confidence threshold.
|
|
2589
|
+
|
|
2590
|
+
Returns:
|
|
2591
|
+
Reinforcement result with status, message, timestamp
|
|
2592
|
+
|
|
2593
|
+
Example:
|
|
2594
|
+
```python
|
|
2595
|
+
result = client.reinforce_opinions()
|
|
2596
|
+
print(f"Status: {result['status']}")
|
|
2597
|
+
```
|
|
2598
|
+
"""
|
|
2599
|
+
return self._request("POST", "/memory/evolution/reinforce/opinions")
|
|
2600
|
+
|
|
2601
|
+
# =========================================================================
|
|
2602
|
+
# Decision Memory
|
|
2603
|
+
# =========================================================================
|
|
2604
|
+
|
|
2605
|
+
def create_decision(
|
|
2606
|
+
self,
|
|
2607
|
+
content: str,
|
|
2608
|
+
decision_type: str = "inference",
|
|
2609
|
+
confidence: float = 0.8,
|
|
2610
|
+
evidence_ids: Optional[List[str]] = None,
|
|
2611
|
+
domain: Optional[str] = None,
|
|
2612
|
+
tags: Optional[List[str]] = None,
|
|
2613
|
+
source_trace_id: Optional[str] = None,
|
|
2614
|
+
source_session_id: Optional[str] = None,
|
|
2615
|
+
) -> Dict[str, Any]:
|
|
2616
|
+
"""Create a new decision with provenance tracking.
|
|
2617
|
+
|
|
2618
|
+
Args:
|
|
2619
|
+
content: The decision statement.
|
|
2620
|
+
decision_type: One of inference, preference, classification, choice, belief, policy.
|
|
2621
|
+
confidence: Initial confidence score (0.0-1.0).
|
|
2622
|
+
evidence_ids: Memory IDs supporting this decision.
|
|
2623
|
+
domain: Domain tag for filtered retrieval.
|
|
2624
|
+
tags: Additional tags.
|
|
2625
|
+
source_trace_id: ReasoningTrace ID that produced this decision.
|
|
2626
|
+
source_session_id: Conversation session ID.
|
|
2627
|
+
|
|
2628
|
+
Returns:
|
|
2629
|
+
Created decision dict with decision_id, content, confidence, status.
|
|
2630
|
+
"""
|
|
2631
|
+
body: Dict[str, Any] = {
|
|
2632
|
+
"content": content,
|
|
2633
|
+
"decision_type": decision_type,
|
|
2634
|
+
"confidence": confidence,
|
|
2635
|
+
}
|
|
2636
|
+
if evidence_ids:
|
|
2637
|
+
body["evidence_ids"] = evidence_ids
|
|
2638
|
+
if domain:
|
|
2639
|
+
body["domain"] = domain
|
|
2640
|
+
if tags:
|
|
2641
|
+
body["tags"] = tags
|
|
2642
|
+
if source_trace_id:
|
|
2643
|
+
body["source_trace_id"] = source_trace_id
|
|
2644
|
+
if source_session_id:
|
|
2645
|
+
body["source_session_id"] = source_session_id
|
|
2646
|
+
return self._request("POST", "/memory/decisions/create", json_body=body)
|
|
2647
|
+
|
|
2648
|
+
def get_decision(self, decision_id: str) -> Dict[str, Any]:
|
|
2649
|
+
"""Retrieve a decision by ID.
|
|
2650
|
+
|
|
2651
|
+
Args:
|
|
2652
|
+
decision_id: The decision ID to retrieve.
|
|
2653
|
+
|
|
2654
|
+
Returns:
|
|
2655
|
+
Decision dict with all fields.
|
|
2656
|
+
"""
|
|
2657
|
+
return self._request("GET", f"/memory/decisions/{decision_id}")
|
|
2658
|
+
|
|
2659
|
+
def list_decisions(
|
|
2660
|
+
self,
|
|
2661
|
+
domain: Optional[str] = None,
|
|
2662
|
+
decision_type: Optional[str] = None,
|
|
2663
|
+
min_confidence: float = 0.0,
|
|
2664
|
+
limit: int = 50,
|
|
2665
|
+
) -> List[Dict[str, Any]]:
|
|
2666
|
+
"""List active decisions with optional filters.
|
|
2667
|
+
|
|
2668
|
+
Args:
|
|
2669
|
+
domain: Filter by domain.
|
|
2670
|
+
decision_type: Filter by type (inference, preference, etc.).
|
|
2671
|
+
min_confidence: Minimum confidence threshold.
|
|
2672
|
+
limit: Maximum results.
|
|
2673
|
+
|
|
2674
|
+
Returns:
|
|
2675
|
+
List of decision dicts.
|
|
2676
|
+
"""
|
|
2677
|
+
params: Dict[str, Any] = {"min_confidence": min_confidence, "limit": limit}
|
|
2678
|
+
if domain:
|
|
2679
|
+
params["domain"] = domain
|
|
2680
|
+
if decision_type:
|
|
2681
|
+
params["decision_type"] = decision_type
|
|
2682
|
+
result = self._request("GET", "/memory/decisions", params=params)
|
|
2683
|
+
return result.get("decisions", [])
|
|
2684
|
+
|
|
2685
|
+
def supersede_decision(
|
|
2686
|
+
self,
|
|
2687
|
+
decision_id: str,
|
|
2688
|
+
new_content: str,
|
|
2689
|
+
reason: str,
|
|
2690
|
+
new_decision_type: str = "inference",
|
|
2691
|
+
new_confidence: float = 0.8,
|
|
2692
|
+
) -> Dict[str, Any]:
|
|
2693
|
+
"""Replace a decision with a new one.
|
|
2694
|
+
|
|
2695
|
+
Args:
|
|
2696
|
+
decision_id: ID of the decision to supersede.
|
|
2697
|
+
new_content: Content of the replacement decision.
|
|
2698
|
+
reason: Why the old decision is being superseded.
|
|
2699
|
+
new_decision_type: Type of the new decision.
|
|
2700
|
+
new_confidence: Confidence of the new decision.
|
|
2701
|
+
|
|
2702
|
+
Returns:
|
|
2703
|
+
Dict with old_decision_id, new_decision_id, status.
|
|
2704
|
+
"""
|
|
2705
|
+
body = {
|
|
2706
|
+
"new_content": new_content,
|
|
2707
|
+
"reason": reason,
|
|
2708
|
+
"new_decision_type": new_decision_type,
|
|
2709
|
+
"new_confidence": new_confidence,
|
|
2710
|
+
}
|
|
2711
|
+
return self._request(
|
|
2712
|
+
"POST", f"/memory/decisions/{decision_id}/supersede", json_body=body
|
|
2713
|
+
)
|
|
2714
|
+
|
|
2715
|
+
def retract_decision(self, decision_id: str, reason: str) -> Dict[str, Any]:
|
|
2716
|
+
"""Retract a decision (mark as no longer valid).
|
|
2717
|
+
|
|
2718
|
+
Args:
|
|
2719
|
+
decision_id: ID of the decision to retract.
|
|
2720
|
+
reason: Why the decision is being retracted.
|
|
2721
|
+
|
|
2722
|
+
Returns:
|
|
2723
|
+
Dict with decision_id and status.
|
|
2724
|
+
"""
|
|
2725
|
+
return self._request(
|
|
2726
|
+
"POST",
|
|
2727
|
+
f"/memory/decisions/{decision_id}/retract",
|
|
2728
|
+
json_body={"reason": reason},
|
|
2729
|
+
)
|
|
2730
|
+
|
|
2731
|
+
def reinforce_decision(self, decision_id: str, evidence_id: str) -> Dict[str, Any]:
|
|
2732
|
+
"""Record supporting evidence for a decision.
|
|
2733
|
+
|
|
2734
|
+
Args:
|
|
2735
|
+
decision_id: ID of the decision to reinforce.
|
|
2736
|
+
evidence_id: Memory ID of the supporting evidence.
|
|
2737
|
+
|
|
2738
|
+
Returns:
|
|
2739
|
+
Dict with decision_id, confidence, reinforcement_count.
|
|
2740
|
+
"""
|
|
2741
|
+
return self._request(
|
|
2742
|
+
"POST",
|
|
2743
|
+
f"/memory/decisions/{decision_id}/reinforce",
|
|
2744
|
+
json_body={"evidence_id": evidence_id},
|
|
2745
|
+
)
|
|
2746
|
+
|
|
2747
|
+
def get_provenance_chain(self, decision_id: str) -> Dict[str, Any]:
|
|
2748
|
+
"""Get full provenance chain for a decision.
|
|
2749
|
+
|
|
2750
|
+
Args:
|
|
2751
|
+
decision_id: The decision ID.
|
|
2752
|
+
|
|
2753
|
+
Returns:
|
|
2754
|
+
Dict with decision, reasoning_trace, evidence, superseded.
|
|
2755
|
+
"""
|
|
2756
|
+
return self._request("GET", f"/memory/decisions/{decision_id}/provenance")
|
|
2757
|
+
|
|
2758
|
+
def get_causal_chain(
|
|
2759
|
+
self,
|
|
2760
|
+
decision_id: str,
|
|
2761
|
+
direction: str = "both",
|
|
2762
|
+
max_depth: int = 3,
|
|
2763
|
+
) -> Dict[str, Any]:
|
|
2764
|
+
"""Trace causal chain from a decision.
|
|
2765
|
+
|
|
2766
|
+
Args:
|
|
2767
|
+
decision_id: The decision ID.
|
|
2768
|
+
direction: 'causes', 'effects', or 'both'.
|
|
2769
|
+
max_depth: Maximum traversal depth (1-10).
|
|
2770
|
+
|
|
2771
|
+
Returns:
|
|
2772
|
+
Dict with decision, causes, effects.
|
|
2773
|
+
"""
|
|
2774
|
+
params = {"direction": direction, "max_depth": max_depth}
|
|
2775
|
+
return self._request(
|
|
2776
|
+
"GET", f"/memory/decisions/{decision_id}/causal-chain", params=params
|
|
2777
|
+
)
|
|
2778
|
+
|
|
2779
|
+
# =========================================================================
|
|
2780
|
+
# Procedure Evolution (CFS-3b)
|
|
2781
|
+
# =========================================================================
|
|
2782
|
+
|
|
2783
|
+
def get_procedure_evolution(
|
|
2784
|
+
self,
|
|
2785
|
+
procedure_id: str,
|
|
2786
|
+
limit: int = 20,
|
|
2787
|
+
offset: int = 0,
|
|
2788
|
+
) -> Dict[str, Any]:
|
|
2789
|
+
"""Get evolution history for a procedure.
|
|
2790
|
+
|
|
2791
|
+
Returns a list of evolution events showing how the procedure was
|
|
2792
|
+
discovered and refined over time.
|
|
2793
|
+
|
|
2794
|
+
Args:
|
|
2795
|
+
procedure_id: The procedure ID to get history for
|
|
2796
|
+
limit: Maximum number of events to return (default 20)
|
|
2797
|
+
offset: Number of events to skip (default 0)
|
|
2798
|
+
|
|
2799
|
+
Returns:
|
|
2800
|
+
Dict with procedure_id, current_version, total_events, and events list
|
|
2801
|
+
|
|
2802
|
+
Example:
|
|
2803
|
+
```python
|
|
2804
|
+
history = client.get_procedure_evolution("proc_123")
|
|
2805
|
+
print(f"Current version: {history['current_version']}")
|
|
2806
|
+
for event in history['events']:
|
|
2807
|
+
print(f" v{event['version']}: {event['event_type']} - {event['summary']}")
|
|
2808
|
+
```
|
|
2809
|
+
"""
|
|
2810
|
+
params = {"limit": limit, "offset": offset}
|
|
2811
|
+
return self._request(
|
|
2812
|
+
"GET", f"/memory/procedures/{procedure_id}/evolution", params=params
|
|
2813
|
+
)
|
|
2814
|
+
|
|
2815
|
+
def get_procedure_evolution_event(
|
|
2816
|
+
self,
|
|
2817
|
+
procedure_id: str,
|
|
2818
|
+
event_id: str,
|
|
2819
|
+
) -> Dict[str, Any]:
|
|
2820
|
+
"""Get detailed information about a specific evolution event.
|
|
2821
|
+
|
|
2822
|
+
Returns the full content snapshot and diff for a single evolution event.
|
|
2823
|
+
|
|
2824
|
+
Args:
|
|
2825
|
+
procedure_id: The procedure ID
|
|
2826
|
+
event_id: The event ID to retrieve
|
|
2827
|
+
|
|
2828
|
+
Returns:
|
|
2829
|
+
Full event detail including content_snapshot and changes_from_previous
|
|
2830
|
+
|
|
2831
|
+
Example:
|
|
2832
|
+
```python
|
|
2833
|
+
event = client.get_procedure_evolution_event("proc_123", "evt_456")
|
|
2834
|
+
print(f"Content at v{event['version']}:")
|
|
2835
|
+
print(event['content_snapshot']['content'])
|
|
2836
|
+
if event['changes_from_previous']['has_changes']:
|
|
2837
|
+
print(f"Changes: {event['changes_from_previous']['summary']}")
|
|
2838
|
+
```
|
|
2839
|
+
"""
|
|
2840
|
+
return self._request(
|
|
2841
|
+
"GET", f"/memory/procedures/{procedure_id}/evolution/{event_id}"
|
|
2842
|
+
)
|
|
2843
|
+
|
|
2844
|
+
def get_procedure_confidence_trajectory(
|
|
2845
|
+
self,
|
|
2846
|
+
procedure_id: str,
|
|
2847
|
+
) -> Dict[str, Any]:
|
|
2848
|
+
"""Get confidence trajectory data for charting.
|
|
2849
|
+
|
|
2850
|
+
Returns time-series data showing how confidence has changed over the
|
|
2851
|
+
procedure's lifecycle, suitable for rendering in a line chart.
|
|
2852
|
+
|
|
2853
|
+
Args:
|
|
2854
|
+
procedure_id: The procedure ID
|
|
2855
|
+
|
|
2856
|
+
Returns:
|
|
2857
|
+
Dict with procedure_id and data_points list containing timestamp,
|
|
2858
|
+
confidence, matches, and success_rate for each point
|
|
2859
|
+
|
|
2860
|
+
Example:
|
|
2861
|
+
```python
|
|
2862
|
+
trajectory = client.get_procedure_confidence_trajectory("proc_123")
|
|
2863
|
+
for point in trajectory['data_points']:
|
|
2864
|
+
print(f"{point['timestamp']}: confidence={point['confidence']:.2f}")
|
|
2865
|
+
```
|
|
2866
|
+
"""
|
|
2867
|
+
return self._request(
|
|
2868
|
+
"GET", f"/memory/procedures/{procedure_id}/confidence-trajectory"
|
|
2869
|
+
)
|
|
2870
|
+
|
|
2871
|
+
# ============================================================================
|
|
2872
|
+
# Procedure Candidates (CFS-3b Recommendation Engine)
|
|
2873
|
+
# ============================================================================
|
|
2874
|
+
|
|
2875
|
+
def list_procedure_candidates(
|
|
2876
|
+
self,
|
|
2877
|
+
min_score: float = 0.6,
|
|
2878
|
+
min_cluster_size: int = 3,
|
|
2879
|
+
days_back: int = 30,
|
|
2880
|
+
limit: int = 20,
|
|
2881
|
+
) -> Dict[str, Any]:
|
|
2882
|
+
"""
|
|
2883
|
+
List procedure promotion candidates from working memory patterns.
|
|
2884
|
+
|
|
2885
|
+
Analyzes working memory items to find repeated patterns that could be
|
|
2886
|
+
promoted to stored procedures for reuse.
|
|
2887
|
+
|
|
2888
|
+
Args:
|
|
2889
|
+
min_score: Minimum recommendation score (0.0-1.0, default: 0.6)
|
|
2890
|
+
min_cluster_size: Minimum items in cluster (default: 3)
|
|
2891
|
+
days_back: Look back period in days (default: 30)
|
|
2892
|
+
limit: Maximum candidates to return (default: 20)
|
|
2893
|
+
|
|
2894
|
+
Returns:
|
|
2895
|
+
Dict with workspace_id, candidate_count, total_working_items, and candidates list.
|
|
2896
|
+
Each candidate contains cluster_id, suggested_name, suggested_description,
|
|
2897
|
+
representative_content, item_count, scores, common_skills, common_tools,
|
|
2898
|
+
sample_item_ids, and date_range.
|
|
2899
|
+
|
|
2900
|
+
Example:
|
|
2901
|
+
```python
|
|
2902
|
+
result = client.list_procedure_candidates(min_score=0.7)
|
|
2903
|
+
for candidate in result['candidates']:
|
|
2904
|
+
print(f"{candidate['suggested_name']}: {candidate['scores']['recommendation_score']:.2f}")
|
|
2905
|
+
```
|
|
2906
|
+
"""
|
|
2907
|
+
params = {
|
|
2908
|
+
"min_score": min_score,
|
|
2909
|
+
"min_cluster_size": min_cluster_size,
|
|
2910
|
+
"days_back": days_back,
|
|
2911
|
+
"limit": limit,
|
|
2912
|
+
}
|
|
2913
|
+
return self._request("GET", "/memory/procedures/candidates", params=params)
|
|
2914
|
+
|
|
2915
|
+
def promote_procedure_candidate(
|
|
2916
|
+
self,
|
|
2917
|
+
cluster_id: str,
|
|
2918
|
+
name: Optional[str] = None,
|
|
2919
|
+
description: Optional[str] = None,
|
|
2920
|
+
procedure_type: str = "extraction",
|
|
2921
|
+
preferred_profile: str = "quick_extract",
|
|
2922
|
+
remove_working_items: bool = False,
|
|
2923
|
+
) -> Dict[str, Any]:
|
|
2924
|
+
"""
|
|
2925
|
+
Promote a candidate cluster to a stored procedure.
|
|
2926
|
+
|
|
2927
|
+
Creates a new procedural memory item from the candidate cluster's
|
|
2928
|
+
representative content and metadata.
|
|
2929
|
+
|
|
2930
|
+
Args:
|
|
2931
|
+
cluster_id: The cluster ID from list_procedure_candidates
|
|
2932
|
+
name: Optional name for the procedure (uses suggested_name if omitted)
|
|
2933
|
+
description: Optional description for the procedure
|
|
2934
|
+
procedure_type: Type of procedure (default: "extraction")
|
|
2935
|
+
preferred_profile: Preferred pipeline profile (default: "quick_extract")
|
|
2936
|
+
remove_working_items: Remove working items after promotion (default: False)
|
|
2937
|
+
|
|
2938
|
+
Returns:
|
|
2939
|
+
Dict with status, procedure_id, name, items_promoted, and items_removed
|
|
2940
|
+
|
|
2941
|
+
Example:
|
|
2942
|
+
```python
|
|
2943
|
+
result = client.promote_procedure_candidate(
|
|
2944
|
+
cluster_id="abc-123",
|
|
2945
|
+
name="API Error Handler",
|
|
2946
|
+
description="Handles 4xx errors from external APIs"
|
|
2947
|
+
)
|
|
2948
|
+
print(f"Created procedure: {result['procedure_id']}")
|
|
2949
|
+
```
|
|
2950
|
+
"""
|
|
2951
|
+
body = {
|
|
2952
|
+
"name": name,
|
|
2953
|
+
"description": description,
|
|
2954
|
+
"procedure_type": procedure_type,
|
|
2955
|
+
"preferred_profile": preferred_profile,
|
|
2956
|
+
"remove_working_items": remove_working_items,
|
|
2957
|
+
}
|
|
2958
|
+
return self._request(
|
|
2959
|
+
"POST",
|
|
2960
|
+
f"/memory/procedures/candidates/{cluster_id}/promote",
|
|
2961
|
+
json_body=body,
|
|
2962
|
+
)
|
|
2963
|
+
|
|
2964
|
+
def dismiss_procedure_candidate(self, cluster_id: str) -> Dict[str, Any]:
|
|
2965
|
+
"""
|
|
2966
|
+
Dismiss a candidate cluster from future recommendations.
|
|
2967
|
+
|
|
2968
|
+
The candidate will be excluded from future recommendation lists
|
|
2969
|
+
for this workspace.
|
|
2970
|
+
|
|
2971
|
+
Args:
|
|
2972
|
+
cluster_id: The cluster ID to dismiss
|
|
2973
|
+
|
|
2974
|
+
Returns:
|
|
2975
|
+
Dict with status, cluster_id, and message
|
|
2976
|
+
|
|
2977
|
+
Example:
|
|
2978
|
+
```python
|
|
2979
|
+
result = client.dismiss_procedure_candidate("abc-123")
|
|
2980
|
+
print(result['message']) # "Candidate dismissed from future recommendations"
|
|
2981
|
+
```
|
|
2982
|
+
"""
|
|
2983
|
+
return self._request(
|
|
2984
|
+
"DELETE", f"/memory/procedures/candidates/{cluster_id}/dismiss"
|
|
2985
|
+
)
|
|
2986
|
+
|
|
2987
|
+
# ============================================================================
|
|
2988
|
+
# Procedure Schema Drift Detection (CFS-4)
|
|
2989
|
+
# ============================================================================
|
|
2990
|
+
|
|
2991
|
+
def list_drift_events(
|
|
2992
|
+
self,
|
|
2993
|
+
procedure_id: Optional[str] = None,
|
|
2994
|
+
resolved: Optional[bool] = None,
|
|
2995
|
+
breaking_only: Optional[bool] = None,
|
|
2996
|
+
start_date: Optional[str] = None,
|
|
2997
|
+
end_date: Optional[str] = None,
|
|
2998
|
+
limit: int = 100,
|
|
2999
|
+
) -> Dict[str, Any]:
|
|
3000
|
+
"""
|
|
3001
|
+
List schema drift events for the current workspace.
|
|
3002
|
+
|
|
3003
|
+
Args:
|
|
3004
|
+
procedure_id: Filter by procedure ID.
|
|
3005
|
+
resolved: Filter by resolution status.
|
|
3006
|
+
breaking_only: If True, only return events with breaking changes.
|
|
3007
|
+
start_date: ISO 8601 date string for range start.
|
|
3008
|
+
end_date: ISO 8601 date string for range end.
|
|
3009
|
+
limit: Maximum number of events to return (1-1000, default 100).
|
|
3010
|
+
|
|
3011
|
+
Returns:
|
|
3012
|
+
Dict with workspace_id, record_count, and records list of DriftEventSummary dicts.
|
|
3013
|
+
|
|
3014
|
+
Example:
|
|
3015
|
+
```python
|
|
3016
|
+
events = client.list_drift_events(resolved=False, breaking_only=True)
|
|
3017
|
+
for event in events["records"]:
|
|
3018
|
+
print(f"{event['procedure_id']}: {event['diff_summary']}")
|
|
3019
|
+
```
|
|
3020
|
+
"""
|
|
3021
|
+
params: Dict[str, Any] = {"limit": limit}
|
|
3022
|
+
if procedure_id is not None:
|
|
3023
|
+
params["procedure_id"] = procedure_id
|
|
3024
|
+
if resolved is not None:
|
|
3025
|
+
params["resolved"] = resolved
|
|
3026
|
+
if breaking_only is not None:
|
|
3027
|
+
params["breaking_only"] = breaking_only
|
|
3028
|
+
if start_date is not None:
|
|
3029
|
+
params["start_date"] = start_date
|
|
3030
|
+
if end_date is not None:
|
|
3031
|
+
params["end_date"] = end_date
|
|
3032
|
+
return self._request("GET", "/memory/procedures/drift", params=params)
|
|
3033
|
+
|
|
3034
|
+
def get_drift_event(self, event_id: str) -> Dict[str, Any]:
|
|
3035
|
+
"""
|
|
3036
|
+
Get a single drift event with full change details.
|
|
3037
|
+
|
|
3038
|
+
Args:
|
|
3039
|
+
event_id: The drift event ID.
|
|
3040
|
+
|
|
3041
|
+
Returns:
|
|
3042
|
+
DriftEventDetail dict with changes list, resolution info, and full metadata.
|
|
3043
|
+
|
|
3044
|
+
Raises:
|
|
3045
|
+
SmartMemoryClientError: If event not found (404).
|
|
3046
|
+
|
|
3047
|
+
Example:
|
|
3048
|
+
```python
|
|
3049
|
+
event = client.get_drift_event("evt-abc-123")
|
|
3050
|
+
for change in event["changes"]:
|
|
3051
|
+
print(f" {change['path']}: {change['change_type']} (breaking={change['breaking']})")
|
|
3052
|
+
```
|
|
3053
|
+
"""
|
|
3054
|
+
return self._request("GET", f"/memory/procedures/drift/{event_id}")
|
|
3055
|
+
|
|
3056
|
+
def resolve_drift_event(
|
|
3057
|
+
self, event_id: str, note: Optional[str] = None
|
|
3058
|
+
) -> Dict[str, Any]:
|
|
3059
|
+
"""
|
|
3060
|
+
Mark a drift event as resolved.
|
|
3061
|
+
|
|
3062
|
+
Args:
|
|
3063
|
+
event_id: The drift event ID to resolve.
|
|
3064
|
+
note: Optional resolution note (max 500 chars).
|
|
3065
|
+
|
|
3066
|
+
Returns:
|
|
3067
|
+
Dict with status, event_id, and resolved=True.
|
|
3068
|
+
|
|
3069
|
+
Raises:
|
|
3070
|
+
SmartMemoryClientError: If event not found (404).
|
|
3071
|
+
|
|
3072
|
+
Example:
|
|
3073
|
+
```python
|
|
3074
|
+
result = client.resolve_drift_event("evt-abc-123", note="Schema updated intentionally")
|
|
3075
|
+
```
|
|
3076
|
+
"""
|
|
3077
|
+
body: Dict[str, Any] = {}
|
|
3078
|
+
if note is not None:
|
|
3079
|
+
body["note"] = note
|
|
3080
|
+
return self._request(
|
|
3081
|
+
"POST", f"/memory/procedures/drift/{event_id}/resolve", json_body=body
|
|
3082
|
+
)
|
|
3083
|
+
|
|
3084
|
+
def sweep_drift(self) -> Dict[str, Any]:
|
|
3085
|
+
"""
|
|
3086
|
+
Trigger a drift sweep across all procedures in the workspace.
|
|
3087
|
+
|
|
3088
|
+
Checks all procedures for schema drift against their stored snapshots.
|
|
3089
|
+
|
|
3090
|
+
Returns:
|
|
3091
|
+
SweepResult dict with workspace_id, procedures_checked, drift_detected,
|
|
3092
|
+
and events_created counts.
|
|
3093
|
+
|
|
3094
|
+
Example:
|
|
3095
|
+
```python
|
|
3096
|
+
result = client.sweep_drift()
|
|
3097
|
+
print(f"Checked {result['procedures_checked']} procedures, "
|
|
3098
|
+
f"found {result['drift_detected']} with drift")
|
|
3099
|
+
```
|
|
3100
|
+
"""
|
|
3101
|
+
return self._request("POST", "/memory/procedures/drift/sweep", json_body={})
|
|
3102
|
+
|
|
3103
|
+
def list_schema_snapshots(self, procedure_id: str) -> Dict[str, Any]:
|
|
3104
|
+
"""
|
|
3105
|
+
List schema snapshots for a procedure.
|
|
3106
|
+
|
|
3107
|
+
Args:
|
|
3108
|
+
procedure_id: The procedure ID to get snapshots for.
|
|
3109
|
+
|
|
3110
|
+
Returns:
|
|
3111
|
+
Dict with workspace_id, procedure_id, record_count, and snapshots list
|
|
3112
|
+
of SchemaSnapshotSummary dicts.
|
|
3113
|
+
|
|
3114
|
+
Example:
|
|
3115
|
+
```python
|
|
3116
|
+
result = client.list_schema_snapshots("proc-abc-123")
|
|
3117
|
+
for snap in result["snapshots"]:
|
|
3118
|
+
print(f"{snap['captured_at']}: {snap['tool_count']} tools, hash={snap['schema_hash']}")
|
|
3119
|
+
```
|
|
3120
|
+
"""
|
|
3121
|
+
return self._request("GET", f"/memory/procedures/schemas/{procedure_id}")
|
|
3122
|
+
|
|
3123
|
+
# ------------------------------------------------------------------
|
|
3124
|
+
# Memory Snapshots (CORE-SUMMARY-1, E1)
|
|
3125
|
+
# ------------------------------------------------------------------
|
|
3126
|
+
|
|
3127
|
+
def summary_generate(
|
|
3128
|
+
self,
|
|
3129
|
+
window_start: Optional[str] = None,
|
|
3130
|
+
include_markdown: bool = True,
|
|
3131
|
+
) -> Dict[str, Any]:
|
|
3132
|
+
"""Fire a manual snapshot for the current workspace.
|
|
3133
|
+
|
|
3134
|
+
Returns the full snapshot payload. Raises ``SmartMemoryClientError``
|
|
3135
|
+
on conflict (HTTP 409 ``{reason: lock_held}``).
|
|
3136
|
+
"""
|
|
3137
|
+
body: Dict[str, Any] = {"include_markdown": include_markdown}
|
|
3138
|
+
if window_start is not None:
|
|
3139
|
+
body["window_start"] = window_start
|
|
3140
|
+
return self._request("POST", "/memory/summary/generate", json_body=body)
|
|
3141
|
+
|
|
3142
|
+
def summary_latest(self) -> Optional[Dict[str, Any]]:
|
|
3143
|
+
"""Return the most recent snapshot for the current workspace."""
|
|
3144
|
+
try:
|
|
3145
|
+
return self._request("GET", "/memory/summary/latest")
|
|
3146
|
+
except SmartMemoryClientError as e:
|
|
3147
|
+
if "404" in str(e):
|
|
3148
|
+
return None
|
|
3149
|
+
raise
|
|
3150
|
+
|
|
3151
|
+
def summary_get(self, snapshot_id: str) -> Optional[Dict[str, Any]]:
|
|
3152
|
+
"""Return a specific snapshot by id, or ``None`` if not found."""
|
|
3153
|
+
try:
|
|
3154
|
+
return self._request("GET", f"/memory/summary/{snapshot_id}")
|
|
3155
|
+
except SmartMemoryClientError as e:
|
|
3156
|
+
if "404" in str(e):
|
|
3157
|
+
return None
|
|
3158
|
+
raise
|
|
3159
|
+
|
|
3160
|
+
def summary_list(
|
|
3161
|
+
self,
|
|
3162
|
+
is_heartbeat: Optional[bool] = None,
|
|
3163
|
+
limit: int = 20,
|
|
3164
|
+
before: Optional[str] = None,
|
|
3165
|
+
) -> List[Dict[str, Any]]:
|
|
3166
|
+
"""List snapshots for the current workspace, newest first."""
|
|
3167
|
+
params: Dict[str, Any] = {"limit": limit}
|
|
3168
|
+
if is_heartbeat is not None:
|
|
3169
|
+
params["is_heartbeat"] = str(is_heartbeat).lower()
|
|
3170
|
+
if before is not None:
|
|
3171
|
+
params["before"] = before
|
|
3172
|
+
return self._request("GET", "/memory/summary/list", params=params) or []
|
|
3173
|
+
|
|
3174
|
+
def summary_delta(
|
|
3175
|
+
self,
|
|
3176
|
+
from_snapshot_id: str,
|
|
3177
|
+
to_snapshot_id: str,
|
|
3178
|
+
) -> Optional[Dict[str, Any]]:
|
|
3179
|
+
"""Return the SnapshotDelta between two snapshots."""
|
|
3180
|
+
try:
|
|
3181
|
+
return self._request(
|
|
3182
|
+
"GET",
|
|
3183
|
+
"/memory/summary/delta",
|
|
3184
|
+
params={"from": from_snapshot_id, "to": to_snapshot_id},
|
|
3185
|
+
)
|
|
3186
|
+
except SmartMemoryClientError as e:
|
|
3187
|
+
if "404" in str(e):
|
|
3188
|
+
return None
|
|
3189
|
+
raise
|
|
3190
|
+
|
|
3191
|
+
def summary_delete(self, snapshot_id: str) -> None:
|
|
3192
|
+
"""Admin-only. Delete a snapshot. Raises on 403/404/500."""
|
|
3193
|
+
self._request("DELETE", f"/memory/summary/{snapshot_id}")
|
|
3194
|
+
|
|
3195
|
+
def _request(
|
|
3196
|
+
self,
|
|
3197
|
+
method: str,
|
|
3198
|
+
endpoint: str,
|
|
3199
|
+
params: Optional[Dict[str, Any]] = None,
|
|
3200
|
+
json_body: Optional[Dict[str, Any]] = None,
|
|
3201
|
+
data: Optional[Dict[str, Any]] = None,
|
|
3202
|
+
headers: Optional[Dict[str, str]] = None,
|
|
3203
|
+
) -> Any:
|
|
3204
|
+
"""Internal helper for making HTTP requests."""
|
|
3205
|
+
import httpx
|
|
3206
|
+
|
|
3207
|
+
url = f"{self.base_url}{endpoint}"
|
|
3208
|
+
req_headers = {"X-Workspace-Id": self.team_id}
|
|
3209
|
+
if headers:
|
|
3210
|
+
req_headers.update(headers)
|
|
3211
|
+
|
|
3212
|
+
if self.api_key:
|
|
3213
|
+
req_headers["Authorization"] = f"Bearer {self.api_key}"
|
|
3214
|
+
|
|
3215
|
+
try:
|
|
3216
|
+
response = httpx.request(
|
|
3217
|
+
method,
|
|
3218
|
+
url,
|
|
3219
|
+
params=params,
|
|
3220
|
+
json=json_body,
|
|
3221
|
+
data=data,
|
|
3222
|
+
headers=req_headers,
|
|
3223
|
+
timeout=self.timeout,
|
|
3224
|
+
)
|
|
3225
|
+
response.raise_for_status()
|
|
3226
|
+
if response.status_code == 204:
|
|
3227
|
+
return None
|
|
3228
|
+
return response.json()
|
|
3229
|
+
except httpx.HTTPStatusError as e:
|
|
3230
|
+
error_detail = e.response.text if hasattr(e, "response") else str(e)
|
|
3231
|
+
raise SmartMemoryClientError(
|
|
3232
|
+
f"Request failed: {e} - Detail: {error_detail}"
|
|
3233
|
+
)
|
|
3234
|
+
except Exception as e:
|
|
3235
|
+
raise SmartMemoryClientError(f"Request failed: {str(e)}")
|
|
3236
|
+
|
|
3237
|
+
def feedback(
|
|
3238
|
+
self,
|
|
3239
|
+
item_ids: List[str],
|
|
3240
|
+
outcome: str,
|
|
3241
|
+
query: Optional[str] = None,
|
|
3242
|
+
) -> Dict[str, Any]:
|
|
3243
|
+
"""Submit explicit feedback on recalled memory items.
|
|
3244
|
+
|
|
3245
|
+
Adjusts ``retention_score`` immediately and, for ``helpful`` feedback with
|
|
3246
|
+
multiple items, strengthens ``CO_RETRIEVED`` edges between them — feeding
|
|
3247
|
+
directly into the Hebbian co-retrieval evolver.
|
|
3248
|
+
|
|
3249
|
+
Args:
|
|
3250
|
+
item_ids: IDs of items returned by a prior ``search()`` call.
|
|
3251
|
+
outcome: ``"helpful"``, ``"misleading"``, or ``"neutral"``.
|
|
3252
|
+
query: Optional original search query (for context/logging).
|
|
3253
|
+
|
|
3254
|
+
Returns:
|
|
3255
|
+
Dict with keys: ``updated`` (int), ``edges_strengthened`` (int), ``outcome`` (str).
|
|
3256
|
+
|
|
3257
|
+
Example:
|
|
3258
|
+
```python
|
|
3259
|
+
results = client.search("what did we decide about auth?")
|
|
3260
|
+
client.feedback([r.item_id for r in results], outcome="helpful")
|
|
3261
|
+
```
|
|
3262
|
+
"""
|
|
3263
|
+
body: Dict[str, Any] = {"item_ids": item_ids, "outcome": outcome}
|
|
3264
|
+
if query is not None:
|
|
3265
|
+
body["query"] = query
|
|
3266
|
+
return self._request("POST", "/memory/feedback", json_body=body)
|
|
3267
|
+
|
|
3268
|
+
def __repr__(self) -> str:
|
|
3269
|
+
auth_status = "authenticated" if self.api_key else "unauthenticated"
|
|
3270
|
+
return f"SmartMemoryClient(base_url='{self.base_url}', {auth_status})"
|
|
3271
|
+
|
|
3272
|
+
def __enter__(self):
|
|
3273
|
+
"""Context manager entry"""
|
|
3274
|
+
return self
|
|
3275
|
+
|
|
3276
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
3277
|
+
"""Context manager exit"""
|
|
3278
|
+
# Clean up underlying client if needed
|
|
3279
|
+
pass
|