hindsight-api 0.1.4__py3-none-any.whl → 0.1.6__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.
Files changed (63) hide show
  1. hindsight_api/__init__.py +10 -9
  2. hindsight_api/alembic/env.py +5 -8
  3. hindsight_api/alembic/versions/5a366d414dce_initial_schema.py +266 -180
  4. hindsight_api/alembic/versions/b7c4d8e9f1a2_add_chunks_table.py +32 -32
  5. hindsight_api/alembic/versions/c8e5f2a3b4d1_add_retain_params_to_documents.py +11 -11
  6. hindsight_api/alembic/versions/d9f6a3b4c5e2_rename_bank_to_interactions.py +7 -12
  7. hindsight_api/alembic/versions/e0a1b2c3d4e5_disposition_to_3_traits.py +23 -15
  8. hindsight_api/alembic/versions/rename_personality_to_disposition.py +30 -21
  9. hindsight_api/api/__init__.py +10 -10
  10. hindsight_api/api/http.py +575 -593
  11. hindsight_api/api/mcp.py +31 -33
  12. hindsight_api/banner.py +13 -6
  13. hindsight_api/config.py +17 -12
  14. hindsight_api/engine/__init__.py +9 -9
  15. hindsight_api/engine/cross_encoder.py +23 -27
  16. hindsight_api/engine/db_utils.py +5 -4
  17. hindsight_api/engine/embeddings.py +22 -21
  18. hindsight_api/engine/entity_resolver.py +81 -75
  19. hindsight_api/engine/llm_wrapper.py +74 -88
  20. hindsight_api/engine/memory_engine.py +663 -673
  21. hindsight_api/engine/query_analyzer.py +100 -97
  22. hindsight_api/engine/response_models.py +105 -106
  23. hindsight_api/engine/retain/__init__.py +9 -16
  24. hindsight_api/engine/retain/bank_utils.py +34 -58
  25. hindsight_api/engine/retain/chunk_storage.py +4 -12
  26. hindsight_api/engine/retain/deduplication.py +9 -28
  27. hindsight_api/engine/retain/embedding_processing.py +4 -11
  28. hindsight_api/engine/retain/embedding_utils.py +3 -4
  29. hindsight_api/engine/retain/entity_processing.py +7 -17
  30. hindsight_api/engine/retain/fact_extraction.py +155 -165
  31. hindsight_api/engine/retain/fact_storage.py +11 -23
  32. hindsight_api/engine/retain/link_creation.py +11 -39
  33. hindsight_api/engine/retain/link_utils.py +166 -95
  34. hindsight_api/engine/retain/observation_regeneration.py +39 -52
  35. hindsight_api/engine/retain/orchestrator.py +72 -62
  36. hindsight_api/engine/retain/types.py +49 -43
  37. hindsight_api/engine/search/__init__.py +15 -1
  38. hindsight_api/engine/search/fusion.py +6 -15
  39. hindsight_api/engine/search/graph_retrieval.py +234 -0
  40. hindsight_api/engine/search/mpfp_retrieval.py +438 -0
  41. hindsight_api/engine/search/observation_utils.py +9 -16
  42. hindsight_api/engine/search/reranking.py +4 -7
  43. hindsight_api/engine/search/retrieval.py +388 -193
  44. hindsight_api/engine/search/scoring.py +5 -7
  45. hindsight_api/engine/search/temporal_extraction.py +8 -11
  46. hindsight_api/engine/search/think_utils.py +115 -39
  47. hindsight_api/engine/search/trace.py +68 -38
  48. hindsight_api/engine/search/tracer.py +49 -35
  49. hindsight_api/engine/search/types.py +22 -16
  50. hindsight_api/engine/task_backend.py +21 -26
  51. hindsight_api/engine/utils.py +25 -10
  52. hindsight_api/main.py +21 -40
  53. hindsight_api/mcp_local.py +190 -0
  54. hindsight_api/metrics.py +44 -30
  55. hindsight_api/migrations.py +10 -8
  56. hindsight_api/models.py +60 -72
  57. hindsight_api/pg0.py +64 -337
  58. hindsight_api/server.py +3 -6
  59. {hindsight_api-0.1.4.dist-info → hindsight_api-0.1.6.dist-info}/METADATA +6 -5
  60. hindsight_api-0.1.6.dist-info/RECORD +64 -0
  61. {hindsight_api-0.1.4.dist-info → hindsight_api-0.1.6.dist-info}/entry_points.txt +1 -0
  62. hindsight_api-0.1.4.dist-info/RECORD +0 -61
  63. {hindsight_api-0.1.4.dist-info → hindsight_api-0.1.6.dist-info}/WHEEL +0 -0
hindsight_api/api/http.py CHANGED
@@ -4,18 +4,18 @@ FastAPI application factory and API routes for memory system.
4
4
  This module provides the create_app function to create and configure
5
5
  the FastAPI application with all API endpoints.
6
6
  """
7
+
7
8
  import json
8
9
  import logging
9
10
  import uuid
10
- from pathlib import Path
11
- from typing import Optional, List, Dict, Any, Union
12
- from datetime import datetime
13
11
  from contextlib import asynccontextmanager
12
+ from datetime import datetime
13
+ from typing import Any
14
14
 
15
15
  from fastapi import FastAPI, HTTPException, Query
16
16
 
17
17
 
18
- def _parse_metadata(metadata: Any) -> Dict[str, Any]:
18
+ def _parse_metadata(metadata: Any) -> dict[str, Any]:
19
19
  """Parse metadata that may be a dict, JSON string, or None."""
20
20
  if metadata is None:
21
21
  return {}
@@ -29,71 +29,77 @@ def _parse_metadata(metadata: Any) -> Dict[str, Any]:
29
29
  return {}
30
30
 
31
31
 
32
- from fastapi.staticfiles import StaticFiles
33
- from fastapi.responses import FileResponse
34
- from pydantic import BaseModel, Field, ConfigDict
32
+ from pydantic import BaseModel, ConfigDict, Field
35
33
 
36
34
  from hindsight_api import MemoryEngine
37
- from hindsight_api.engine.memory_engine import Budget
38
35
  from hindsight_api.engine.db_utils import acquire_with_retry
36
+ from hindsight_api.engine.memory_engine import Budget
39
37
  from hindsight_api.engine.response_models import VALID_RECALL_FACT_TYPES
40
- from hindsight_api.metrics import get_metrics_collector, initialize_metrics, create_metrics_collector
41
-
38
+ from hindsight_api.metrics import create_metrics_collector, get_metrics_collector, initialize_metrics
42
39
 
43
40
  logger = logging.getLogger(__name__)
44
41
 
45
42
 
46
43
  class EntityIncludeOptions(BaseModel):
47
44
  """Options for including entity observations in recall results."""
45
+
48
46
  max_tokens: int = Field(default=500, description="Maximum tokens for entity observations")
49
47
 
50
48
 
51
49
  class ChunkIncludeOptions(BaseModel):
52
50
  """Options for including chunks in recall results."""
51
+
53
52
  max_tokens: int = Field(default=8192, description="Maximum tokens for chunks (chunks may be truncated)")
54
53
 
55
54
 
56
55
  class IncludeOptions(BaseModel):
57
56
  """Options for including additional data in recall results."""
58
- entities: Optional[EntityIncludeOptions] = Field(
57
+
58
+ entities: EntityIncludeOptions | None = Field(
59
59
  default=EntityIncludeOptions(),
60
- description="Include entity observations. Set to null to disable entity inclusion."
60
+ description="Include entity observations. Set to null to disable entity inclusion.",
61
61
  )
62
- chunks: Optional[ChunkIncludeOptions] = Field(
63
- default=None,
64
- description="Include raw chunks. Set to {} to enable, null to disable (default: disabled)."
62
+ chunks: ChunkIncludeOptions | None = Field(
63
+ default=None, description="Include raw chunks. Set to {} to enable, null to disable (default: disabled)."
65
64
  )
66
65
 
67
66
 
68
67
  class RecallRequest(BaseModel):
69
68
  """Request model for recall endpoint."""
70
- model_config = ConfigDict(json_schema_extra={
71
- "example": {
72
- "query": "What did Alice say about machine learning?",
73
- "types": ["world", "experience"],
74
- "budget": "mid",
75
- "max_tokens": 4096,
76
- "trace": True,
77
- "query_timestamp": "2023-05-30T23:40:00",
78
- "include": {
79
- "entities": {
80
- "max_tokens": 500
81
- }
69
+
70
+ model_config = ConfigDict(
71
+ json_schema_extra={
72
+ "example": {
73
+ "query": "What did Alice say about machine learning?",
74
+ "types": ["world", "experience"],
75
+ "budget": "mid",
76
+ "max_tokens": 4096,
77
+ "trace": True,
78
+ "query_timestamp": "2023-05-30T23:40:00",
79
+ "include": {"entities": {"max_tokens": 500}},
82
80
  }
83
81
  }
84
- })
82
+ )
85
83
 
86
84
  query: str
87
- types: Optional[List[str]] = Field(default=None, description="List of fact types to recall (defaults to all if not specified)")
85
+ types: list[str] | None = Field(
86
+ default=None, description="List of fact types to recall (defaults to all if not specified)"
87
+ )
88
88
  budget: Budget = Budget.MID
89
89
  max_tokens: int = 4096
90
90
  trace: bool = False
91
- query_timestamp: Optional[str] = Field(default=None, description="ISO format date string (e.g., '2023-05-30T23:40:00')")
92
- include: IncludeOptions = Field(default_factory=IncludeOptions, description="Options for including additional data (entities are included by default)")
91
+ query_timestamp: str | None = Field(
92
+ default=None, description="ISO format date string (e.g., '2023-05-30T23:40:00')"
93
+ )
94
+ include: IncludeOptions = Field(
95
+ default_factory=IncludeOptions,
96
+ description="Options for including additional data (entities are included by default)",
97
+ )
93
98
 
94
99
 
95
100
  class RecallResult(BaseModel):
96
101
  """Single recall result item."""
102
+
97
103
  model_config = {
98
104
  "populate_by_name": True,
99
105
  "json_schema_extra": {
@@ -108,102 +114,112 @@ class RecallResult(BaseModel):
108
114
  "mentioned_at": "2024-01-15T10:30:00Z",
109
115
  "document_id": "session_abc123",
110
116
  "metadata": {"source": "slack"},
111
- "chunk_id": "456e7890-e12b-34d5-a678-901234567890"
117
+ "chunk_id": "456e7890-e12b-34d5-a678-901234567890",
112
118
  }
113
- }
119
+ },
114
120
  }
115
121
 
116
122
  id: str
117
123
  text: str
118
- type: Optional[str] = None # fact type: world, experience, opinion, observation
119
- entities: Optional[List[str]] = None # Entity names mentioned in this fact
120
- context: Optional[str] = None
121
- occurred_start: Optional[str] = None # ISO format date when the event started
122
- occurred_end: Optional[str] = None # ISO format date when the event ended
123
- mentioned_at: Optional[str] = None # ISO format date when the fact was mentioned
124
- document_id: Optional[str] = None # Document this memory belongs to
125
- metadata: Optional[Dict[str, str]] = None # User-defined metadata
126
- chunk_id: Optional[str] = None # Chunk this fact was extracted from
124
+ type: str | None = None # fact type: world, experience, opinion, observation
125
+ entities: list[str] | None = None # Entity names mentioned in this fact
126
+ context: str | None = None
127
+ occurred_start: str | None = None # ISO format date when the event started
128
+ occurred_end: str | None = None # ISO format date when the event ended
129
+ mentioned_at: str | None = None # ISO format date when the fact was mentioned
130
+ document_id: str | None = None # Document this memory belongs to
131
+ metadata: dict[str, str] | None = None # User-defined metadata
132
+ chunk_id: str | None = None # Chunk this fact was extracted from
127
133
 
128
134
 
129
135
  class EntityObservationResponse(BaseModel):
130
136
  """An observation about an entity."""
137
+
131
138
  text: str
132
- mentioned_at: Optional[str] = None
139
+ mentioned_at: str | None = None
133
140
 
134
141
 
135
142
  class EntityStateResponse(BaseModel):
136
143
  """Current mental model of an entity."""
144
+
137
145
  entity_id: str
138
146
  canonical_name: str
139
- observations: List[EntityObservationResponse]
147
+ observations: list[EntityObservationResponse]
140
148
 
141
149
 
142
150
  class EntityListItem(BaseModel):
143
151
  """Entity list item with summary."""
144
- model_config = ConfigDict(json_schema_extra={
145
- "example": {
146
- "id": "123e4567-e89b-12d3-a456-426614174000",
147
- "canonical_name": "John",
148
- "mention_count": 15,
149
- "first_seen": "2024-01-15T10:30:00Z",
150
- "last_seen": "2024-02-01T14:00:00Z"
152
+
153
+ model_config = ConfigDict(
154
+ json_schema_extra={
155
+ "example": {
156
+ "id": "123e4567-e89b-12d3-a456-426614174000",
157
+ "canonical_name": "John",
158
+ "mention_count": 15,
159
+ "first_seen": "2024-01-15T10:30:00Z",
160
+ "last_seen": "2024-02-01T14:00:00Z",
161
+ }
151
162
  }
152
- })
163
+ )
153
164
 
154
165
  id: str
155
166
  canonical_name: str
156
167
  mention_count: int
157
- first_seen: Optional[str] = None
158
- last_seen: Optional[str] = None
159
- metadata: Optional[Dict[str, Any]] = None
168
+ first_seen: str | None = None
169
+ last_seen: str | None = None
170
+ metadata: dict[str, Any] | None = None
160
171
 
161
172
 
162
173
  class EntityListResponse(BaseModel):
163
174
  """Response model for entity list endpoint."""
164
- model_config = ConfigDict(json_schema_extra={
165
- "example": {
166
- "items": [
167
- {
168
- "id": "123e4567-e89b-12d3-a456-426614174000",
169
- "canonical_name": "John",
170
- "mention_count": 15,
171
- "first_seen": "2024-01-15T10:30:00Z",
172
- "last_seen": "2024-02-01T14:00:00Z"
173
- }
174
- ]
175
+
176
+ model_config = ConfigDict(
177
+ json_schema_extra={
178
+ "example": {
179
+ "items": [
180
+ {
181
+ "id": "123e4567-e89b-12d3-a456-426614174000",
182
+ "canonical_name": "John",
183
+ "mention_count": 15,
184
+ "first_seen": "2024-01-15T10:30:00Z",
185
+ "last_seen": "2024-02-01T14:00:00Z",
186
+ }
187
+ ]
188
+ }
175
189
  }
176
- })
190
+ )
177
191
 
178
- items: List[EntityListItem]
192
+ items: list[EntityListItem]
179
193
 
180
194
 
181
195
  class EntityDetailResponse(BaseModel):
182
196
  """Response model for entity detail endpoint."""
183
- model_config = ConfigDict(json_schema_extra={
184
- "example": {
185
- "id": "123e4567-e89b-12d3-a456-426614174000",
186
- "canonical_name": "John",
187
- "mention_count": 15,
188
- "first_seen": "2024-01-15T10:30:00Z",
189
- "last_seen": "2024-02-01T14:00:00Z",
190
- "observations": [
191
- {"text": "John works at Google", "mentioned_at": "2024-01-15T10:30:00Z"}
192
- ]
197
+
198
+ model_config = ConfigDict(
199
+ json_schema_extra={
200
+ "example": {
201
+ "id": "123e4567-e89b-12d3-a456-426614174000",
202
+ "canonical_name": "John",
203
+ "mention_count": 15,
204
+ "first_seen": "2024-01-15T10:30:00Z",
205
+ "last_seen": "2024-02-01T14:00:00Z",
206
+ "observations": [{"text": "John works at Google", "mentioned_at": "2024-01-15T10:30:00Z"}],
207
+ }
193
208
  }
194
- })
209
+ )
195
210
 
196
211
  id: str
197
212
  canonical_name: str
198
213
  mention_count: int
199
- first_seen: Optional[str] = None
200
- last_seen: Optional[str] = None
201
- metadata: Optional[Dict[str, Any]] = None
202
- observations: List[EntityObservationResponse]
214
+ first_seen: str | None = None
215
+ last_seen: str | None = None
216
+ metadata: dict[str, Any] | None = None
217
+ observations: list[EntityObservationResponse]
203
218
 
204
219
 
205
220
  class ChunkData(BaseModel):
206
221
  """Chunk data for a single chunk."""
222
+
207
223
  id: str
208
224
  text: str
209
225
  chunk_index: int
@@ -212,223 +228,219 @@ class ChunkData(BaseModel):
212
228
 
213
229
  class RecallResponse(BaseModel):
214
230
  """Response model for recall endpoints."""
215
- model_config = ConfigDict(json_schema_extra={
216
- "example": {
217
- "results": [
218
- {
219
- "id": "123e4567-e89b-12d3-a456-426614174000",
220
- "text": "Alice works at Google on the AI team",
221
- "type": "world",
222
- "entities": ["Alice", "Google"],
223
- "context": "work info",
224
- "occurred_start": "2024-01-15T10:30:00Z",
225
- "occurred_end": "2024-01-15T10:30:00Z",
226
- "chunk_id": "456e7890-e12b-34d5-a678-901234567890"
227
- }
228
- ],
229
- "trace": {
230
- "query": "What did Alice say about machine learning?",
231
- "num_results": 1,
232
- "time_seconds": 0.123
233
- },
234
- "entities": {
235
- "Alice": {
236
- "entity_id": "123e4567-e89b-12d3-a456-426614174001",
237
- "canonical_name": "Alice",
238
- "observations": [
239
- {"text": "Alice works at Google on the AI team", "mentioned_at": "2024-01-15T10:30:00Z"}
240
- ]
241
- }
242
- },
243
- "chunks": {
244
- "456e7890-e12b-34d5-a678-901234567890": {
245
- "id": "456e7890-e12b-34d5-a678-901234567890",
246
- "text": "Alice works at Google on the AI team. She's been there for 3 years...",
247
- "chunk_index": 0
248
- }
231
+
232
+ model_config = ConfigDict(
233
+ json_schema_extra={
234
+ "example": {
235
+ "results": [
236
+ {
237
+ "id": "123e4567-e89b-12d3-a456-426614174000",
238
+ "text": "Alice works at Google on the AI team",
239
+ "type": "world",
240
+ "entities": ["Alice", "Google"],
241
+ "context": "work info",
242
+ "occurred_start": "2024-01-15T10:30:00Z",
243
+ "occurred_end": "2024-01-15T10:30:00Z",
244
+ "chunk_id": "456e7890-e12b-34d5-a678-901234567890",
245
+ }
246
+ ],
247
+ "trace": {
248
+ "query": "What did Alice say about machine learning?",
249
+ "num_results": 1,
250
+ "time_seconds": 0.123,
251
+ },
252
+ "entities": {
253
+ "Alice": {
254
+ "entity_id": "123e4567-e89b-12d3-a456-426614174001",
255
+ "canonical_name": "Alice",
256
+ "observations": [
257
+ {"text": "Alice works at Google on the AI team", "mentioned_at": "2024-01-15T10:30:00Z"}
258
+ ],
259
+ }
260
+ },
261
+ "chunks": {
262
+ "456e7890-e12b-34d5-a678-901234567890": {
263
+ "id": "456e7890-e12b-34d5-a678-901234567890",
264
+ "text": "Alice works at Google on the AI team. She's been there for 3 years...",
265
+ "chunk_index": 0,
266
+ }
267
+ },
249
268
  }
250
269
  }
251
- })
270
+ )
252
271
 
253
- results: List[RecallResult]
254
- trace: Optional[Dict[str, Any]] = None
255
- entities: Optional[Dict[str, EntityStateResponse]] = Field(default=None, description="Entity states for entities mentioned in results")
256
- chunks: Optional[Dict[str, ChunkData]] = Field(default=None, description="Chunks for facts, keyed by chunk_id")
272
+ results: list[RecallResult]
273
+ trace: dict[str, Any] | None = None
274
+ entities: dict[str, EntityStateResponse] | None = Field(
275
+ default=None, description="Entity states for entities mentioned in results"
276
+ )
277
+ chunks: dict[str, ChunkData] | None = Field(default=None, description="Chunks for facts, keyed by chunk_id")
257
278
 
258
279
 
259
280
  class MemoryItem(BaseModel):
260
281
  """Single memory item for retain."""
261
- model_config = ConfigDict(json_schema_extra={
262
- "example": {
263
- "content": "Alice mentioned she's working on a new ML model",
264
- "timestamp": "2024-01-15T10:30:00Z",
265
- "context": "team meeting",
266
- "metadata": {"source": "slack", "channel": "engineering"},
267
- "document_id": "meeting_notes_2024_01_15"
282
+
283
+ model_config = ConfigDict(
284
+ json_schema_extra={
285
+ "example": {
286
+ "content": "Alice mentioned she's working on a new ML model",
287
+ "timestamp": "2024-01-15T10:30:00Z",
288
+ "context": "team meeting",
289
+ "metadata": {"source": "slack", "channel": "engineering"},
290
+ "document_id": "meeting_notes_2024_01_15",
291
+ }
268
292
  }
269
- })
293
+ )
270
294
 
271
295
  content: str
272
- timestamp: Optional[datetime] = None
273
- context: Optional[str] = None
274
- metadata: Optional[Dict[str, str]] = None
275
- document_id: Optional[str] = Field(
276
- default=None,
277
- description="Optional document ID for this memory item."
278
- )
296
+ timestamp: datetime | None = None
297
+ context: str | None = None
298
+ metadata: dict[str, str] | None = None
299
+ document_id: str | None = Field(default=None, description="Optional document ID for this memory item.")
279
300
 
280
301
 
281
302
  class RetainRequest(BaseModel):
282
303
  """Request model for retain endpoint."""
283
- model_config = ConfigDict(json_schema_extra={
284
- "example": {
285
- "items": [
286
- {
287
- "content": "Alice works at Google",
288
- "context": "work",
289
- "document_id": "conversation_123"
290
- },
291
- {
292
- "content": "Bob went hiking yesterday",
293
- "timestamp": "2024-01-15T10:00:00Z",
294
- "document_id": "conversation_123"
295
- }
296
- ],
297
- "async": False
304
+
305
+ model_config = ConfigDict(
306
+ json_schema_extra={
307
+ "example": {
308
+ "items": [
309
+ {"content": "Alice works at Google", "context": "work", "document_id": "conversation_123"},
310
+ {
311
+ "content": "Bob went hiking yesterday",
312
+ "timestamp": "2024-01-15T10:00:00Z",
313
+ "document_id": "conversation_123",
314
+ },
315
+ ],
316
+ "async": False,
317
+ }
298
318
  }
299
- })
319
+ )
300
320
 
301
- items: List[MemoryItem]
321
+ items: list[MemoryItem]
302
322
  async_: bool = Field(
303
323
  default=False,
304
324
  alias="async",
305
- description="If true, process asynchronously in background. If false, wait for completion (default: false)"
325
+ description="If true, process asynchronously in background. If false, wait for completion (default: false)",
306
326
  )
307
327
 
308
328
 
309
329
  class RetainResponse(BaseModel):
310
330
  """Response model for retain endpoint."""
331
+
311
332
  model_config = ConfigDict(
312
333
  populate_by_name=True,
313
- json_schema_extra={
314
- "example": {
315
- "success": True,
316
- "bank_id": "user123",
317
- "items_count": 2,
318
- "async": False
319
- }
320
- }
334
+ json_schema_extra={"example": {"success": True, "bank_id": "user123", "items_count": 2, "async": False}},
321
335
  )
322
336
 
323
337
  success: bool
324
338
  bank_id: str
325
339
  items_count: int
326
- async_: bool = Field(alias="async", serialization_alias="async", description="Whether the operation was processed asynchronously")
340
+ async_: bool = Field(
341
+ alias="async", serialization_alias="async", description="Whether the operation was processed asynchronously"
342
+ )
327
343
 
328
344
 
329
345
  class FactsIncludeOptions(BaseModel):
330
346
  """Options for including facts (based_on) in reflect results."""
347
+
331
348
  pass # No additional options needed, just enable/disable
332
349
 
333
350
 
334
351
  class ReflectIncludeOptions(BaseModel):
335
352
  """Options for including additional data in reflect results."""
336
- facts: Optional[FactsIncludeOptions] = Field(
353
+
354
+ facts: FactsIncludeOptions | None = Field(
337
355
  default=None,
338
- description="Include facts that the answer is based on. Set to {} to enable, null to disable (default: disabled)."
356
+ description="Include facts that the answer is based on. Set to {} to enable, null to disable (default: disabled).",
339
357
  )
340
358
 
341
359
 
342
360
  class ReflectRequest(BaseModel):
343
361
  """Request model for reflect endpoint."""
344
- model_config = ConfigDict(json_schema_extra={
345
- "example": {
346
- "query": "What do you think about artificial intelligence?",
347
- "budget": "low",
348
- "context": "This is for a research paper on AI ethics",
349
- "include": {
350
- "facts": {}
362
+
363
+ model_config = ConfigDict(
364
+ json_schema_extra={
365
+ "example": {
366
+ "query": "What do you think about artificial intelligence?",
367
+ "budget": "low",
368
+ "context": "This is for a research paper on AI ethics",
369
+ "include": {"facts": {}},
351
370
  }
352
371
  }
353
- })
372
+ )
354
373
 
355
374
  query: str
356
375
  budget: Budget = Budget.LOW
357
- context: Optional[str] = None
358
- include: ReflectIncludeOptions = Field(default_factory=ReflectIncludeOptions, description="Options for including additional data (disabled by default)")
376
+ context: str | None = None
377
+ include: ReflectIncludeOptions = Field(
378
+ default_factory=ReflectIncludeOptions, description="Options for including additional data (disabled by default)"
379
+ )
359
380
 
360
381
 
361
382
  class OpinionItem(BaseModel):
362
383
  """Model for an opinion with confidence score."""
384
+
363
385
  text: str
364
386
  confidence: float
365
387
 
366
388
 
367
389
  class ReflectFact(BaseModel):
368
390
  """A fact used in think response."""
369
- model_config = ConfigDict(json_schema_extra={
370
- "example": {
371
- "id": "123e4567-e89b-12d3-a456-426614174000",
372
- "text": "AI is used in healthcare",
373
- "type": "world",
374
- "context": "healthcare discussion",
375
- "occurred_start": "2024-01-15T10:30:00Z",
376
- "occurred_end": "2024-01-15T10:30:00Z"
391
+
392
+ model_config = ConfigDict(
393
+ json_schema_extra={
394
+ "example": {
395
+ "id": "123e4567-e89b-12d3-a456-426614174000",
396
+ "text": "AI is used in healthcare",
397
+ "type": "world",
398
+ "context": "healthcare discussion",
399
+ "occurred_start": "2024-01-15T10:30:00Z",
400
+ "occurred_end": "2024-01-15T10:30:00Z",
401
+ }
377
402
  }
378
- })
403
+ )
379
404
 
380
- id: Optional[str] = None
405
+ id: str | None = None
381
406
  text: str
382
- type: Optional[str] = None # fact type: world, experience, opinion
383
- context: Optional[str] = None
384
- occurred_start: Optional[str] = None
385
- occurred_end: Optional[str] = None
407
+ type: str | None = None # fact type: world, experience, opinion
408
+ context: str | None = None
409
+ occurred_start: str | None = None
410
+ occurred_end: str | None = None
386
411
 
387
412
 
388
413
  class ReflectResponse(BaseModel):
389
414
  """Response model for think endpoint."""
390
- model_config = ConfigDict(json_schema_extra={
391
- "example": {
392
- "text": "Based on my understanding, AI is a transformative technology...",
393
- "based_on": [
394
- {
395
- "id": "123",
396
- "text": "AI is used in healthcare",
397
- "type": "world"
398
- },
399
- {
400
- "id": "456",
401
- "text": "I discussed AI applications last week",
402
- "type": "experience"
403
- }
404
- ]
415
+
416
+ model_config = ConfigDict(
417
+ json_schema_extra={
418
+ "example": {
419
+ "text": "Based on my understanding, AI is a transformative technology...",
420
+ "based_on": [
421
+ {"id": "123", "text": "AI is used in healthcare", "type": "world"},
422
+ {"id": "456", "text": "I discussed AI applications last week", "type": "experience"},
423
+ ],
424
+ }
405
425
  }
406
- })
426
+ )
407
427
 
408
428
  text: str
409
- based_on: List[ReflectFact] = [] # Facts used to generate the response
429
+ based_on: list[ReflectFact] = [] # Facts used to generate the response
410
430
 
411
431
 
412
432
  class BanksResponse(BaseModel):
413
433
  """Response model for banks list endpoint."""
414
- model_config = ConfigDict(json_schema_extra={
415
- "example": {
416
- "banks": ["user123", "bank_alice", "bank_bob"]
417
- }
418
- })
419
434
 
420
- banks: List[str]
435
+ model_config = ConfigDict(json_schema_extra={"example": {"banks": ["user123", "bank_alice", "bank_bob"]}})
436
+
437
+ banks: list[str]
421
438
 
422
439
 
423
440
  class DispositionTraits(BaseModel):
424
441
  """Disposition traits that influence how memories are formed and interpreted."""
425
- model_config = ConfigDict(json_schema_extra={
426
- "example": {
427
- "skepticism": 3,
428
- "literalism": 3,
429
- "empathy": 3
430
- }
431
- })
442
+
443
+ model_config = ConfigDict(json_schema_extra={"example": {"skepticism": 3, "literalism": 3, "empathy": 3}})
432
444
 
433
445
  skepticism: int = Field(ge=1, le=5, description="How skeptical vs trusting (1=trusting, 5=skeptical)")
434
446
  literalism: int = Field(ge=1, le=5, description="How literally to interpret information (1=flexible, 5=literal)")
@@ -437,18 +449,17 @@ class DispositionTraits(BaseModel):
437
449
 
438
450
  class BankProfileResponse(BaseModel):
439
451
  """Response model for bank profile."""
440
- model_config = ConfigDict(json_schema_extra={
441
- "example": {
442
- "bank_id": "user123",
443
- "name": "Alice",
444
- "disposition": {
445
- "skepticism": 3,
446
- "literalism": 3,
447
- "empathy": 3
448
- },
449
- "background": "I am a software engineer with 10 years of experience in startups"
452
+
453
+ model_config = ConfigDict(
454
+ json_schema_extra={
455
+ "example": {
456
+ "bank_id": "user123",
457
+ "name": "Alice",
458
+ "disposition": {"skepticism": 3, "literalism": 3, "empathy": 3},
459
+ "background": "I am a software engineer with 10 years of experience in startups",
460
+ }
450
461
  }
451
- })
462
+ )
452
463
 
453
464
  bank_id: str
454
465
  name: str
@@ -458,140 +469,146 @@ class BankProfileResponse(BaseModel):
458
469
 
459
470
  class UpdateDispositionRequest(BaseModel):
460
471
  """Request model for updating disposition traits."""
472
+
461
473
  disposition: DispositionTraits
462
474
 
463
475
 
464
476
  class AddBackgroundRequest(BaseModel):
465
477
  """Request model for adding/merging background information."""
466
- model_config = ConfigDict(json_schema_extra={
467
- "example": {
468
- "content": "I was born in Texas",
469
- "update_disposition": True
470
- }
471
- })
478
+
479
+ model_config = ConfigDict(
480
+ json_schema_extra={"example": {"content": "I was born in Texas", "update_disposition": True}}
481
+ )
472
482
 
473
483
  content: str = Field(description="New background information to add or merge")
474
484
  update_disposition: bool = Field(
475
- default=True,
476
- description="If true, infer disposition traits from the merged background (default: true)"
485
+ default=True, description="If true, infer disposition traits from the merged background (default: true)"
477
486
  )
478
487
 
479
488
 
480
489
  class BackgroundResponse(BaseModel):
481
490
  """Response model for background update."""
482
- model_config = ConfigDict(json_schema_extra={
483
- "example": {
484
- "background": "I was born in Texas. I am a software engineer with 10 years of experience.",
485
- "disposition": {
486
- "skepticism": 3,
487
- "literalism": 3,
488
- "empathy": 3
491
+
492
+ model_config = ConfigDict(
493
+ json_schema_extra={
494
+ "example": {
495
+ "background": "I was born in Texas. I am a software engineer with 10 years of experience.",
496
+ "disposition": {"skepticism": 3, "literalism": 3, "empathy": 3},
489
497
  }
490
498
  }
491
- })
499
+ )
492
500
 
493
501
  background: str
494
- disposition: Optional[DispositionTraits] = None
502
+ disposition: DispositionTraits | None = None
495
503
 
496
504
 
497
505
  class BankListItem(BaseModel):
498
506
  """Bank list item with profile summary."""
507
+
499
508
  bank_id: str
500
509
  name: str
501
510
  disposition: DispositionTraits
502
511
  background: str
503
- created_at: Optional[str] = None
504
- updated_at: Optional[str] = None
512
+ created_at: str | None = None
513
+ updated_at: str | None = None
505
514
 
506
515
 
507
516
  class BankListResponse(BaseModel):
508
517
  """Response model for listing all banks."""
509
- model_config = ConfigDict(json_schema_extra={
510
- "example": {
511
- "banks": [
512
- {
513
- "bank_id": "user123",
514
- "name": "Alice",
515
- "disposition": {
516
- "skepticism": 3,
517
- "literalism": 3,
518
- "empathy": 3
519
- },
520
- "background": "I am a software engineer",
521
- "created_at": "2024-01-15T10:30:00Z",
522
- "updated_at": "2024-01-16T14:20:00Z"
523
- }
524
- ]
518
+
519
+ model_config = ConfigDict(
520
+ json_schema_extra={
521
+ "example": {
522
+ "banks": [
523
+ {
524
+ "bank_id": "user123",
525
+ "name": "Alice",
526
+ "disposition": {"skepticism": 3, "literalism": 3, "empathy": 3},
527
+ "background": "I am a software engineer",
528
+ "created_at": "2024-01-15T10:30:00Z",
529
+ "updated_at": "2024-01-16T14:20:00Z",
530
+ }
531
+ ]
532
+ }
525
533
  }
526
- })
534
+ )
527
535
 
528
- banks: List[BankListItem]
536
+ banks: list[BankListItem]
529
537
 
530
538
 
531
539
  class CreateBankRequest(BaseModel):
532
540
  """Request model for creating/updating a bank."""
533
- model_config = ConfigDict(json_schema_extra={
534
- "example": {
535
- "name": "Alice",
536
- "disposition": {
537
- "skepticism": 3,
538
- "literalism": 3,
539
- "empathy": 3
540
- },
541
- "background": "I am a creative software engineer with 10 years of experience"
541
+
542
+ model_config = ConfigDict(
543
+ json_schema_extra={
544
+ "example": {
545
+ "name": "Alice",
546
+ "disposition": {"skepticism": 3, "literalism": 3, "empathy": 3},
547
+ "background": "I am a creative software engineer with 10 years of experience",
548
+ }
542
549
  }
543
- })
550
+ )
544
551
 
545
- name: Optional[str] = None
546
- disposition: Optional[DispositionTraits] = None
547
- background: Optional[str] = None
552
+ name: str | None = None
553
+ disposition: DispositionTraits | None = None
554
+ background: str | None = None
548
555
 
549
556
 
550
557
  class GraphDataResponse(BaseModel):
551
558
  """Response model for graph data endpoint."""
552
- model_config = ConfigDict(json_schema_extra={
553
- "example": {
554
- "nodes": [
555
- {"id": "1", "label": "Alice works at Google", "type": "world"},
556
- {"id": "2", "label": "Bob went hiking", "type": "world"}
557
- ],
558
- "edges": [
559
- {"from": "1", "to": "2", "type": "semantic", "weight": 0.8}
560
- ],
561
- "table_rows": [
562
- {"id": "abc12345...", "text": "Alice works at Google", "context": "Work info", "date": "2024-01-15 10:30", "entities": "Alice (PERSON), Google (ORGANIZATION)"}
563
- ],
564
- "total_units": 2
559
+
560
+ model_config = ConfigDict(
561
+ json_schema_extra={
562
+ "example": {
563
+ "nodes": [
564
+ {"id": "1", "label": "Alice works at Google", "type": "world"},
565
+ {"id": "2", "label": "Bob went hiking", "type": "world"},
566
+ ],
567
+ "edges": [{"from": "1", "to": "2", "type": "semantic", "weight": 0.8}],
568
+ "table_rows": [
569
+ {
570
+ "id": "abc12345...",
571
+ "text": "Alice works at Google",
572
+ "context": "Work info",
573
+ "date": "2024-01-15 10:30",
574
+ "entities": "Alice (PERSON), Google (ORGANIZATION)",
575
+ }
576
+ ],
577
+ "total_units": 2,
578
+ }
565
579
  }
566
- })
580
+ )
567
581
 
568
- nodes: List[Dict[str, Any]]
569
- edges: List[Dict[str, Any]]
570
- table_rows: List[Dict[str, Any]]
582
+ nodes: list[dict[str, Any]]
583
+ edges: list[dict[str, Any]]
584
+ table_rows: list[dict[str, Any]]
571
585
  total_units: int
572
586
 
573
587
 
574
588
  class ListMemoryUnitsResponse(BaseModel):
575
589
  """Response model for list memory units endpoint."""
576
- model_config = ConfigDict(json_schema_extra={
577
- "example": {
578
- "items": [
579
- {
580
- "id": "550e8400-e29b-41d4-a716-446655440000",
581
- "text": "Alice works at Google on the AI team",
582
- "context": "Work conversation",
583
- "date": "2024-01-15T10:30:00Z",
584
- "type": "world",
585
- "entities": "Alice (PERSON), Google (ORGANIZATION)"
586
- }
587
- ],
588
- "total": 150,
589
- "limit": 100,
590
- "offset": 0
590
+
591
+ model_config = ConfigDict(
592
+ json_schema_extra={
593
+ "example": {
594
+ "items": [
595
+ {
596
+ "id": "550e8400-e29b-41d4-a716-446655440000",
597
+ "text": "Alice works at Google on the AI team",
598
+ "context": "Work conversation",
599
+ "date": "2024-01-15T10:30:00Z",
600
+ "type": "world",
601
+ "entities": "Alice (PERSON), Google (ORGANIZATION)",
602
+ }
603
+ ],
604
+ "total": 150,
605
+ "limit": 100,
606
+ "offset": 0,
607
+ }
591
608
  }
592
- })
609
+ )
593
610
 
594
- items: List[Dict[str, Any]]
611
+ items: list[dict[str, Any]]
595
612
  total: int
596
613
  limit: int
597
614
  offset: int
@@ -599,26 +616,29 @@ class ListMemoryUnitsResponse(BaseModel):
599
616
 
600
617
  class ListDocumentsResponse(BaseModel):
601
618
  """Response model for list documents endpoint."""
602
- model_config = ConfigDict(json_schema_extra={
603
- "example": {
604
- "items": [
605
- {
606
- "id": "session_1",
607
- "bank_id": "user123",
608
- "content_hash": "abc123",
609
- "created_at": "2024-01-15T10:30:00Z",
610
- "updated_at": "2024-01-15T10:30:00Z",
611
- "text_length": 5420,
612
- "memory_unit_count": 15
613
- }
614
- ],
615
- "total": 50,
616
- "limit": 100,
617
- "offset": 0
619
+
620
+ model_config = ConfigDict(
621
+ json_schema_extra={
622
+ "example": {
623
+ "items": [
624
+ {
625
+ "id": "session_1",
626
+ "bank_id": "user123",
627
+ "content_hash": "abc123",
628
+ "created_at": "2024-01-15T10:30:00Z",
629
+ "updated_at": "2024-01-15T10:30:00Z",
630
+ "text_length": 5420,
631
+ "memory_unit_count": 15,
632
+ }
633
+ ],
634
+ "total": 50,
635
+ "limit": 100,
636
+ "offset": 0,
637
+ }
618
638
  }
619
- })
639
+ )
620
640
 
621
- items: List[Dict[str, Any]]
641
+ items: list[dict[str, Any]]
622
642
  total: int
623
643
  limit: int
624
644
  offset: int
@@ -626,22 +646,25 @@ class ListDocumentsResponse(BaseModel):
626
646
 
627
647
  class DocumentResponse(BaseModel):
628
648
  """Response model for get document endpoint."""
629
- model_config = ConfigDict(json_schema_extra={
630
- "example": {
631
- "id": "session_1",
632
- "bank_id": "user123",
633
- "original_text": "Full document text here...",
634
- "content_hash": "abc123",
635
- "created_at": "2024-01-15T10:30:00Z",
636
- "updated_at": "2024-01-15T10:30:00Z",
637
- "memory_unit_count": 15
649
+
650
+ model_config = ConfigDict(
651
+ json_schema_extra={
652
+ "example": {
653
+ "id": "session_1",
654
+ "bank_id": "user123",
655
+ "original_text": "Full document text here...",
656
+ "content_hash": "abc123",
657
+ "created_at": "2024-01-15T10:30:00Z",
658
+ "updated_at": "2024-01-15T10:30:00Z",
659
+ "memory_unit_count": 15,
660
+ }
638
661
  }
639
- })
662
+ )
640
663
 
641
664
  id: str
642
665
  bank_id: str
643
666
  original_text: str
644
- content_hash: Optional[str]
667
+ content_hash: str | None
645
668
  created_at: str
646
669
  updated_at: str
647
670
  memory_unit_count: int
@@ -649,16 +672,19 @@ class DocumentResponse(BaseModel):
649
672
 
650
673
  class ChunkResponse(BaseModel):
651
674
  """Response model for get chunk endpoint."""
652
- model_config = ConfigDict(json_schema_extra={
653
- "example": {
654
- "chunk_id": "user123_session_1_0",
655
- "document_id": "session_1",
656
- "bank_id": "user123",
657
- "chunk_index": 0,
658
- "chunk_text": "This is the first chunk of the document...",
659
- "created_at": "2024-01-15T10:30:00Z"
675
+
676
+ model_config = ConfigDict(
677
+ json_schema_extra={
678
+ "example": {
679
+ "chunk_id": "user123_session_1_0",
680
+ "document_id": "session_1",
681
+ "bank_id": "user123",
682
+ "chunk_index": 0,
683
+ "chunk_text": "This is the first chunk of the document...",
684
+ "created_at": "2024-01-15T10:30:00Z",
685
+ }
660
686
  }
661
- })
687
+ )
662
688
 
663
689
  chunk_id: str
664
690
  document_id: str
@@ -670,17 +696,14 @@ class ChunkResponse(BaseModel):
670
696
 
671
697
  class DeleteResponse(BaseModel):
672
698
  """Response model for delete operations."""
673
- model_config = ConfigDict(json_schema_extra={
674
- "example": {
675
- "success": True,
676
- "message": "Deleted successfully",
677
- "deleted_count": 10
678
- }
679
- })
699
+
700
+ model_config = ConfigDict(
701
+ json_schema_extra={"example": {"success": True, "message": "Deleted successfully", "deleted_count": 10}}
702
+ )
680
703
 
681
704
  success: bool
682
- message: Optional[str] = None
683
- deleted_count: Optional[int] = None
705
+ message: str | None = None
706
+ deleted_count: int | None = None
684
707
 
685
708
 
686
709
  def create_app(memory: MemoryEngine, initialize_memory: bool = True) -> FastAPI:
@@ -700,6 +723,7 @@ def create_app(memory: MemoryEngine, initialize_memory: bool = True) -> FastAPI:
700
723
  In that case, you should call memory.initialize() manually before starting the server
701
724
  and memory.close() when shutting down.
702
725
  """
726
+
703
727
  @asynccontextmanager
704
728
  async def lifespan(app: FastAPI):
705
729
  """
@@ -708,10 +732,7 @@ def create_app(memory: MemoryEngine, initialize_memory: bool = True) -> FastAPI:
708
732
  """
709
733
  # Initialize OpenTelemetry metrics
710
734
  try:
711
- prometheus_reader = initialize_metrics(
712
- service_name="hindsight-api",
713
- service_version="1.0.0"
714
- )
735
+ prometheus_reader = initialize_metrics(service_name="hindsight-api", service_version="1.0.0")
715
736
  create_metrics_collector()
716
737
  app.state.prometheus_reader = prometheus_reader
717
738
  logging.info("Metrics initialized - available at /metrics endpoint")
@@ -725,8 +746,6 @@ def create_app(memory: MemoryEngine, initialize_memory: bool = True) -> FastAPI:
725
746
  await memory.initialize()
726
747
  logging.info("Memory system initialized")
727
748
 
728
-
729
-
730
749
  yield
731
750
 
732
751
  # Shutdown: Cleanup memory system
@@ -746,7 +765,7 @@ def create_app(memory: MemoryEngine, initialize_memory: bool = True) -> FastAPI:
746
765
  "name": "Apache 2.0",
747
766
  "url": "https://www.apache.org/licenses/LICENSE-2.0.html",
748
767
  },
749
- lifespan=lifespan
768
+ lifespan=lifespan,
750
769
  )
751
770
 
752
771
  # IMPORTANT: Set memory on app.state immediately, don't wait for lifespan
@@ -766,7 +785,7 @@ def _register_routes(app: FastAPI):
766
785
  "/health",
767
786
  summary="Health check endpoint",
768
787
  description="Checks the health of the API and database connection",
769
- tags=["Monitoring"]
788
+ tags=["Monitoring"],
770
789
  )
771
790
  async def health_endpoint():
772
791
  """
@@ -784,12 +803,12 @@ def _register_routes(app: FastAPI):
784
803
  "/metrics",
785
804
  summary="Prometheus metrics endpoint",
786
805
  description="Exports metrics in Prometheus format for scraping",
787
- tags=["Monitoring"]
806
+ tags=["Monitoring"],
788
807
  )
789
808
  async def metrics_endpoint():
790
809
  """Return Prometheus metrics."""
791
- from prometheus_client import generate_latest, CONTENT_TYPE_LATEST
792
810
  from fastapi.responses import Response
811
+ from prometheus_client import CONTENT_TYPE_LATEST, generate_latest
793
812
 
794
813
  metrics_data = generate_latest()
795
814
  return Response(content=metrics_data, media_type=CONTENT_TYPE_LATEST)
@@ -800,36 +819,29 @@ def _register_routes(app: FastAPI):
800
819
  summary="Get memory graph data",
801
820
  description="Retrieve graph data for visualization, optionally filtered by type (world/experience/opinion). Limited to 1000 most recent items.",
802
821
  operation_id="get_graph",
803
- tags=["Memory"]
822
+ tags=["Memory"],
804
823
  )
805
- async def api_graph(bank_id: str,
806
- type: Optional[str] = None
807
- ):
824
+ async def api_graph(bank_id: str, type: str | None = None):
808
825
  """Get graph data from database, filtered by bank_id and optionally by type."""
809
826
  try:
810
827
  data = await app.state.memory.get_graph_data(bank_id, type)
811
828
  return data
812
829
  except Exception as e:
813
830
  import traceback
831
+
814
832
  error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
815
833
  logger.error(f"Error in /v1/default/banks/{bank_id}/graph: {error_detail}")
816
834
  raise HTTPException(status_code=500, detail=str(e))
817
835
 
818
-
819
836
  @app.get(
820
837
  "/v1/default/banks/{bank_id}/memories/list",
821
838
  response_model=ListMemoryUnitsResponse,
822
839
  summary="List memory units",
823
840
  description="List memory units with pagination and optional full-text search. Supports filtering by type. Results are sorted by most recent first (mentioned_at DESC, then created_at DESC).",
824
841
  operation_id="list_memories",
825
- tags=["Memory"]
842
+ tags=["Memory"],
826
843
  )
827
- async def api_list(bank_id: str,
828
- type: Optional[str] = None,
829
- q: Optional[str] = None,
830
- limit: int = 100,
831
- offset: int = 0
832
- ):
844
+ async def api_list(bank_id: str, type: str | None = None, q: str | None = None, limit: int = 100, offset: int = 0):
833
845
  """
834
846
  List memory units for table view with optional full-text search.
835
847
 
@@ -845,20 +857,16 @@ def _register_routes(app: FastAPI):
845
857
  """
846
858
  try:
847
859
  data = await app.state.memory.list_memory_units(
848
- bank_id=bank_id,
849
- fact_type=type,
850
- search_query=q,
851
- limit=limit,
852
- offset=offset
860
+ bank_id=bank_id, fact_type=type, search_query=q, limit=limit, offset=offset
853
861
  )
854
862
  return data
855
863
  except Exception as e:
856
864
  import traceback
865
+
857
866
  error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
858
867
  logger.error(f"Error in /v1/default/banks/{bank_id}/memories/list: {error_detail}")
859
868
  raise HTTPException(status_code=500, detail=str(e))
860
869
 
861
-
862
870
  @app.post(
863
871
  "/v1/default/banks/{bank_id}/memories/recall",
864
872
  response_model=RecallResponse,
@@ -870,7 +878,7 @@ def _register_routes(app: FastAPI):
870
878
  "- `opinion`: The bank's formed beliefs, perspectives, and viewpoints\n\n"
871
879
  "Set `include_entities=true` to get entity observations alongside recall results.",
872
880
  operation_id="recall_memories",
873
- tags=["Memory"]
881
+ tags=["Memory"],
874
882
  )
875
883
  async def api_recall(bank_id: str, request: RecallRequest):
876
884
  """Run a recall and return results with trace."""
@@ -884,11 +892,11 @@ def _register_routes(app: FastAPI):
884
892
  question_date = None
885
893
  if request.query_timestamp:
886
894
  try:
887
- question_date = datetime.fromisoformat(request.query_timestamp.replace('Z', '+00:00'))
895
+ question_date = datetime.fromisoformat(request.query_timestamp.replace("Z", "+00:00"))
888
896
  except ValueError as e:
889
897
  raise HTTPException(
890
898
  status_code=400,
891
- detail=f"Invalid query_timestamp format. Expected ISO format (e.g., '2023-05-30T23:40:00'): {str(e)}"
899
+ detail=f"Invalid query_timestamp format. Expected ISO format (e.g., '2023-05-30T23:40:00'): {str(e)}",
892
900
  )
893
901
 
894
902
  # Determine entity inclusion settings
@@ -900,7 +908,9 @@ def _register_routes(app: FastAPI):
900
908
  max_chunk_tokens = request.include.chunks.max_tokens if include_chunks else 8192
901
909
 
902
910
  # Run recall with tracing (record metrics)
903
- with metrics.record_operation("recall", bank_id=bank_id, budget=request.budget.value, max_tokens=request.max_tokens):
911
+ with metrics.record_operation(
912
+ "recall", bank_id=bank_id, budget=request.budget.value, max_tokens=request.max_tokens
913
+ ):
904
914
  core_result = await app.state.memory.recall_async(
905
915
  bank_id=bank_id,
906
916
  query=request.query,
@@ -912,7 +922,7 @@ def _register_routes(app: FastAPI):
912
922
  include_entities=include_entities,
913
923
  max_entity_tokens=max_entity_tokens,
914
924
  include_chunks=include_chunks,
915
- max_chunk_tokens=max_chunk_tokens
925
+ max_chunk_tokens=max_chunk_tokens,
916
926
  )
917
927
 
918
928
  # Convert core MemoryFact objects to API RecallResult objects (excluding internal metrics)
@@ -927,7 +937,7 @@ def _register_routes(app: FastAPI):
927
937
  occurred_end=fact.occurred_end,
928
938
  mentioned_at=fact.mentioned_at,
929
939
  document_id=fact.document_id,
930
- chunk_id=fact.chunk_id
940
+ chunk_id=fact.chunk_id,
931
941
  )
932
942
  for fact in core_result.results
933
943
  ]
@@ -941,7 +951,7 @@ def _register_routes(app: FastAPI):
941
951
  id=chunk_id,
942
952
  text=chunk_info.chunk_text,
943
953
  chunk_index=chunk_info.chunk_index,
944
- truncated=chunk_info.truncated
954
+ truncated=chunk_info.truncated,
945
955
  )
946
956
 
947
957
  # Convert core EntityState objects to API EntityStateResponse objects
@@ -955,24 +965,21 @@ def _register_routes(app: FastAPI):
955
965
  observations=[
956
966
  EntityObservationResponse(text=obs.text, mentioned_at=obs.mentioned_at)
957
967
  for obs in state.observations
958
- ]
968
+ ],
959
969
  )
960
970
 
961
971
  return RecallResponse(
962
- results=recall_results,
963
- trace=core_result.trace,
964
- entities=entities_response,
965
- chunks=chunks_response
972
+ results=recall_results, trace=core_result.trace, entities=entities_response, chunks=chunks_response
966
973
  )
967
974
  except HTTPException:
968
975
  raise
969
976
  except Exception as e:
970
977
  import traceback
978
+
971
979
  error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
972
980
  logger.error(f"Error in /v1/default/banks/{bank_id}/memories/recall: {error_detail}")
973
981
  raise HTTPException(status_code=500, detail=str(e))
974
982
 
975
-
976
983
  @app.post(
977
984
  "/v1/default/banks/{bank_id}/reflect",
978
985
  response_model=ReflectResponse,
@@ -986,7 +993,7 @@ def _register_routes(app: FastAPI):
986
993
  "5. Extracts and stores any new opinions formed\n"
987
994
  "6. Returns plain text answer, the facts used, and new opinions",
988
995
  operation_id="reflect",
989
- tags=["Memory"]
996
+ tags=["Memory"],
990
997
  )
991
998
  async def api_reflect(bank_id: str, request: ReflectRequest):
992
999
  metrics = get_metrics_collector()
@@ -995,10 +1002,7 @@ def _register_routes(app: FastAPI):
995
1002
  # Use the memory system's reflect_async method (record metrics)
996
1003
  with metrics.record_operation("reflect", bank_id=bank_id, budget=request.budget.value):
997
1004
  core_result = await app.state.memory.reflect_async(
998
- bank_id=bank_id,
999
- query=request.query,
1000
- budget=request.budget,
1001
- context=request.context
1005
+ bank_id=bank_id, query=request.query, budget=request.budget, context=request.context
1002
1006
  )
1003
1007
 
1004
1008
  # Convert core MemoryFact objects to API ReflectFact objects if facts are requested
@@ -1006,14 +1010,16 @@ def _register_routes(app: FastAPI):
1006
1010
  if request.include.facts is not None:
1007
1011
  for fact_type, facts in core_result.based_on.items():
1008
1012
  for fact in facts:
1009
- based_on_facts.append(ReflectFact(
1010
- id=fact.id,
1011
- text=fact.text,
1012
- type=fact.fact_type,
1013
- context=fact.context,
1014
- occurred_start=fact.occurred_start,
1015
- occurred_end=fact.occurred_end
1016
- ))
1013
+ based_on_facts.append(
1014
+ ReflectFact(
1015
+ id=fact.id,
1016
+ text=fact.text,
1017
+ type=fact.fact_type,
1018
+ context=fact.context,
1019
+ occurred_start=fact.occurred_start,
1020
+ occurred_end=fact.occurred_end,
1021
+ )
1022
+ )
1017
1023
 
1018
1024
  return ReflectResponse(
1019
1025
  text=core_result.text,
@@ -1022,18 +1028,18 @@ def _register_routes(app: FastAPI):
1022
1028
 
1023
1029
  except Exception as e:
1024
1030
  import traceback
1031
+
1025
1032
  error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
1026
1033
  logger.error(f"Error in /v1/default/banks/{bank_id}/reflect: {error_detail}")
1027
1034
  raise HTTPException(status_code=500, detail=str(e))
1028
1035
 
1029
-
1030
1036
  @app.get(
1031
1037
  "/v1/default/banks",
1032
1038
  response_model=BankListResponse,
1033
1039
  summary="List all memory banks",
1034
1040
  description="Get a list of all agents with their profiles",
1035
1041
  operation_id="list_banks",
1036
- tags=["Banks"]
1042
+ tags=["Banks"],
1037
1043
  )
1038
1044
  async def api_list_banks():
1039
1045
  """Get list of all banks with their profiles."""
@@ -1042,6 +1048,7 @@ def _register_routes(app: FastAPI):
1042
1048
  return BankListResponse(banks=banks)
1043
1049
  except Exception as e:
1044
1050
  import traceback
1051
+
1045
1052
  error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
1046
1053
  logger.error(f"Error in /v1/default/banks: {error_detail}")
1047
1054
  raise HTTPException(status_code=500, detail=str(e))
@@ -1051,7 +1058,7 @@ def _register_routes(app: FastAPI):
1051
1058
  summary="Get statistics for memory bank",
1052
1059
  description="Get statistics about nodes and links for a specific agent",
1053
1060
  operation_id="get_agent_stats",
1054
- tags=["Banks"]
1061
+ tags=["Banks"],
1055
1062
  )
1056
1063
  async def api_stats(bank_id: str):
1057
1064
  """Get statistics about memory nodes and links for a memory bank."""
@@ -1066,7 +1073,7 @@ def _register_routes(app: FastAPI):
1066
1073
  WHERE bank_id = $1
1067
1074
  GROUP BY fact_type
1068
1075
  """,
1069
- bank_id
1076
+ bank_id,
1070
1077
  )
1071
1078
 
1072
1079
  # Get link counts by link_type
@@ -1078,7 +1085,7 @@ def _register_routes(app: FastAPI):
1078
1085
  WHERE mu.bank_id = $1
1079
1086
  GROUP BY ml.link_type
1080
1087
  """,
1081
- bank_id
1088
+ bank_id,
1082
1089
  )
1083
1090
 
1084
1091
  # Get link counts by fact_type (from nodes)
@@ -1090,7 +1097,7 @@ def _register_routes(app: FastAPI):
1090
1097
  WHERE mu.bank_id = $1
1091
1098
  GROUP BY mu.fact_type
1092
1099
  """,
1093
- bank_id
1100
+ bank_id,
1094
1101
  )
1095
1102
 
1096
1103
  # Get link counts by fact_type AND link_type
@@ -1102,7 +1109,7 @@ def _register_routes(app: FastAPI):
1102
1109
  WHERE mu.bank_id = $1
1103
1110
  GROUP BY mu.fact_type, ml.link_type
1104
1111
  """,
1105
- bank_id
1112
+ bank_id,
1106
1113
  )
1107
1114
 
1108
1115
  # Get pending and failed operations counts
@@ -1113,11 +1120,11 @@ def _register_routes(app: FastAPI):
1113
1120
  WHERE bank_id = $1
1114
1121
  GROUP BY status
1115
1122
  """,
1116
- bank_id
1123
+ bank_id,
1117
1124
  )
1118
- ops_by_status = {row['status']: row['count'] for row in ops_stats}
1119
- pending_operations = ops_by_status.get('pending', 0)
1120
- failed_operations = ops_by_status.get('failed', 0)
1125
+ ops_by_status = {row["status"]: row["count"] for row in ops_stats}
1126
+ pending_operations = ops_by_status.get("pending", 0)
1127
+ failed_operations = ops_by_status.get("failed", 0)
1121
1128
 
1122
1129
  # Get document count
1123
1130
  doc_count_result = await conn.fetchrow(
@@ -1126,21 +1133,21 @@ def _register_routes(app: FastAPI):
1126
1133
  FROM documents
1127
1134
  WHERE bank_id = $1
1128
1135
  """,
1129
- bank_id
1136
+ bank_id,
1130
1137
  )
1131
- total_documents = doc_count_result['count'] if doc_count_result else 0
1138
+ total_documents = doc_count_result["count"] if doc_count_result else 0
1132
1139
 
1133
1140
  # Format results
1134
- nodes_by_type = {row['fact_type']: row['count'] for row in node_stats}
1135
- links_by_type = {row['link_type']: row['count'] for row in link_stats}
1136
- links_by_fact_type = {row['fact_type']: row['count'] for row in link_fact_type_stats}
1141
+ nodes_by_type = {row["fact_type"]: row["count"] for row in node_stats}
1142
+ links_by_type = {row["link_type"]: row["count"] for row in link_stats}
1143
+ links_by_fact_type = {row["fact_type"]: row["count"] for row in link_fact_type_stats}
1137
1144
 
1138
1145
  # Build detailed breakdown: {fact_type: {link_type: count}}
1139
1146
  links_breakdown = {}
1140
1147
  for row in link_breakdown_stats:
1141
- fact_type = row['fact_type']
1142
- link_type = row['link_type']
1143
- count = row['count']
1148
+ fact_type = row["fact_type"]
1149
+ link_type = row["link_type"]
1150
+ count = row["count"]
1144
1151
  if fact_type not in links_breakdown:
1145
1152
  links_breakdown[fact_type] = {}
1146
1153
  links_breakdown[fact_type][link_type] = count
@@ -1158,11 +1165,12 @@ def _register_routes(app: FastAPI):
1158
1165
  "links_by_fact_type": links_by_fact_type,
1159
1166
  "links_breakdown": links_breakdown,
1160
1167
  "pending_operations": pending_operations,
1161
- "failed_operations": failed_operations
1168
+ "failed_operations": failed_operations,
1162
1169
  }
1163
1170
 
1164
1171
  except Exception as e:
1165
1172
  import traceback
1173
+
1166
1174
  error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
1167
1175
  logger.error(f"Error in /v1/default/banks/{bank_id}/stats: {error_detail}")
1168
1176
  raise HTTPException(status_code=500, detail=str(e))
@@ -1173,19 +1181,18 @@ def _register_routes(app: FastAPI):
1173
1181
  summary="List entities",
1174
1182
  description="List all entities (people, organizations, etc.) known by the bank, ordered by mention count.",
1175
1183
  operation_id="list_entities",
1176
- tags=["Entities"]
1184
+ tags=["Entities"],
1177
1185
  )
1178
- async def api_list_entities(bank_id: str,
1179
- limit: int = Query(default=100, description="Maximum number of entities to return")
1186
+ async def api_list_entities(
1187
+ bank_id: str, limit: int = Query(default=100, description="Maximum number of entities to return")
1180
1188
  ):
1181
1189
  """List entities for a memory bank."""
1182
1190
  try:
1183
1191
  entities = await app.state.memory.list_entities(bank_id, limit=limit)
1184
- return EntityListResponse(
1185
- items=[EntityListItem(**e) for e in entities]
1186
- )
1192
+ return EntityListResponse(items=[EntityListItem(**e) for e in entities])
1187
1193
  except Exception as e:
1188
1194
  import traceback
1195
+
1189
1196
  error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
1190
1197
  logger.error(f"Error in /v1/default/banks/{bank_id}/entities: {error_detail}")
1191
1198
  raise HTTPException(status_code=500, detail=str(e))
@@ -1196,7 +1203,7 @@ def _register_routes(app: FastAPI):
1196
1203
  summary="Get entity details",
1197
1204
  description="Get detailed information about an entity including observations (mental model).",
1198
1205
  operation_id="get_entity",
1199
- tags=["Entities"]
1206
+ tags=["Entities"],
1200
1207
  )
1201
1208
  async def api_get_entity(bank_id: str, entity_id: str):
1202
1209
  """Get entity details with observations."""
@@ -1210,33 +1217,32 @@ def _register_routes(app: FastAPI):
1210
1217
  FROM entities
1211
1218
  WHERE bank_id = $1 AND id = $2
1212
1219
  """,
1213
- bank_id, uuid.UUID(entity_id)
1220
+ bank_id,
1221
+ uuid.UUID(entity_id),
1214
1222
  )
1215
1223
 
1216
1224
  if not entity_row:
1217
1225
  raise HTTPException(status_code=404, detail=f"Entity {entity_id} not found")
1218
1226
 
1219
1227
  # Get observations for the entity
1220
- observations = await app.state.memory.get_entity_observations(
1221
- bank_id, entity_id, limit=20
1222
- )
1228
+ observations = await app.state.memory.get_entity_observations(bank_id, entity_id, limit=20)
1223
1229
 
1224
1230
  return EntityDetailResponse(
1225
- id=str(entity_row['id']),
1226
- canonical_name=entity_row['canonical_name'],
1227
- mention_count=entity_row['mention_count'],
1228
- first_seen=entity_row['first_seen'].isoformat() if entity_row['first_seen'] else None,
1229
- last_seen=entity_row['last_seen'].isoformat() if entity_row['last_seen'] else None,
1230
- metadata=_parse_metadata(entity_row['metadata']),
1231
+ id=str(entity_row["id"]),
1232
+ canonical_name=entity_row["canonical_name"],
1233
+ mention_count=entity_row["mention_count"],
1234
+ first_seen=entity_row["first_seen"].isoformat() if entity_row["first_seen"] else None,
1235
+ last_seen=entity_row["last_seen"].isoformat() if entity_row["last_seen"] else None,
1236
+ metadata=_parse_metadata(entity_row["metadata"]),
1231
1237
  observations=[
1232
- EntityObservationResponse(text=obs.text, mentioned_at=obs.mentioned_at)
1233
- for obs in observations
1234
- ]
1238
+ EntityObservationResponse(text=obs.text, mentioned_at=obs.mentioned_at) for obs in observations
1239
+ ],
1235
1240
  )
1236
1241
  except HTTPException:
1237
1242
  raise
1238
1243
  except Exception as e:
1239
1244
  import traceback
1245
+
1240
1246
  error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
1241
1247
  logger.error(f"Error in /v1/default/banks/{bank_id}/entities/{entity_id}: {error_detail}")
1242
1248
  raise HTTPException(status_code=500, detail=str(e))
@@ -1247,7 +1253,7 @@ def _register_routes(app: FastAPI):
1247
1253
  summary="Regenerate entity observations",
1248
1254
  description="Regenerate observations for an entity based on all facts mentioning it.",
1249
1255
  operation_id="regenerate_entity_observations",
1250
- tags=["Entities"]
1256
+ tags=["Entities"],
1251
1257
  )
1252
1258
  async def api_regenerate_entity_observations(bank_id: str, entity_id: str):
1253
1259
  """Regenerate observations for an entity."""
@@ -1261,7 +1267,8 @@ def _register_routes(app: FastAPI):
1261
1267
  FROM entities
1262
1268
  WHERE bank_id = $1 AND id = $2
1263
1269
  """,
1264
- bank_id, uuid.UUID(entity_id)
1270
+ bank_id,
1271
+ uuid.UUID(entity_id),
1265
1272
  )
1266
1273
 
1267
1274
  if not entity_row:
@@ -1269,32 +1276,28 @@ def _register_routes(app: FastAPI):
1269
1276
 
1270
1277
  # Regenerate observations
1271
1278
  await app.state.memory.regenerate_entity_observations(
1272
- bank_id=bank_id,
1273
- entity_id=entity_id,
1274
- entity_name=entity_row['canonical_name']
1279
+ bank_id=bank_id, entity_id=entity_id, entity_name=entity_row["canonical_name"]
1275
1280
  )
1276
1281
 
1277
1282
  # Get updated observations
1278
- observations = await app.state.memory.get_entity_observations(
1279
- bank_id, entity_id, limit=20
1280
- )
1283
+ observations = await app.state.memory.get_entity_observations(bank_id, entity_id, limit=20)
1281
1284
 
1282
1285
  return EntityDetailResponse(
1283
- id=str(entity_row['id']),
1284
- canonical_name=entity_row['canonical_name'],
1285
- mention_count=entity_row['mention_count'],
1286
- first_seen=entity_row['first_seen'].isoformat() if entity_row['first_seen'] else None,
1287
- last_seen=entity_row['last_seen'].isoformat() if entity_row['last_seen'] else None,
1288
- metadata=_parse_metadata(entity_row['metadata']),
1286
+ id=str(entity_row["id"]),
1287
+ canonical_name=entity_row["canonical_name"],
1288
+ mention_count=entity_row["mention_count"],
1289
+ first_seen=entity_row["first_seen"].isoformat() if entity_row["first_seen"] else None,
1290
+ last_seen=entity_row["last_seen"].isoformat() if entity_row["last_seen"] else None,
1291
+ metadata=_parse_metadata(entity_row["metadata"]),
1289
1292
  observations=[
1290
- EntityObservationResponse(text=obs.text, mentioned_at=obs.mentioned_at)
1291
- for obs in observations
1292
- ]
1293
+ EntityObservationResponse(text=obs.text, mentioned_at=obs.mentioned_at) for obs in observations
1294
+ ],
1293
1295
  )
1294
1296
  except HTTPException:
1295
1297
  raise
1296
1298
  except Exception as e:
1297
1299
  import traceback
1300
+
1298
1301
  error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
1299
1302
  logger.error(f"Error in /v1/default/banks/{bank_id}/entities/{entity_id}/regenerate: {error_detail}")
1300
1303
  raise HTTPException(status_code=500, detail=str(e))
@@ -1305,13 +1308,9 @@ def _register_routes(app: FastAPI):
1305
1308
  summary="List documents",
1306
1309
  description="List documents with pagination and optional search. Documents are the source content from which memory units are extracted.",
1307
1310
  operation_id="list_documents",
1308
- tags=["Documents"]
1311
+ tags=["Documents"],
1309
1312
  )
1310
- async def api_list_documents(bank_id: str,
1311
- q: Optional[str] = None,
1312
- limit: int = 100,
1313
- offset: int = 0
1314
- ):
1313
+ async def api_list_documents(bank_id: str, q: str | None = None, limit: int = 100, offset: int = 0):
1315
1314
  """
1316
1315
  List documents for a memory bank with optional search.
1317
1316
 
@@ -1322,31 +1321,24 @@ def _register_routes(app: FastAPI):
1322
1321
  offset: Offset for pagination (default: 0)
1323
1322
  """
1324
1323
  try:
1325
- data = await app.state.memory.list_documents(
1326
- bank_id=bank_id,
1327
- search_query=q,
1328
- limit=limit,
1329
- offset=offset
1330
- )
1324
+ data = await app.state.memory.list_documents(bank_id=bank_id, search_query=q, limit=limit, offset=offset)
1331
1325
  return data
1332
1326
  except Exception as e:
1333
1327
  import traceback
1328
+
1334
1329
  error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
1335
1330
  logger.error(f"Error in /v1/default/banks/{bank_id}/documents: {error_detail}")
1336
1331
  raise HTTPException(status_code=500, detail=str(e))
1337
1332
 
1338
-
1339
1333
  @app.get(
1340
1334
  "/v1/default/banks/{bank_id}/documents/{document_id}",
1341
1335
  response_model=DocumentResponse,
1342
1336
  summary="Get document details",
1343
1337
  description="Get a specific document including its original text",
1344
1338
  operation_id="get_document",
1345
- tags=["Documents"]
1339
+ tags=["Documents"],
1346
1340
  )
1347
- async def api_get_document(bank_id: str,
1348
- document_id: str
1349
- ):
1341
+ async def api_get_document(bank_id: str, document_id: str):
1350
1342
  """
1351
1343
  Get a specific document with its original text.
1352
1344
 
@@ -1363,18 +1355,18 @@ def _register_routes(app: FastAPI):
1363
1355
  raise
1364
1356
  except Exception as e:
1365
1357
  import traceback
1358
+
1366
1359
  error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
1367
1360
  logger.error(f"Error in /v1/default/banks/{bank_id}/documents/{document_id}: {error_detail}")
1368
1361
  raise HTTPException(status_code=500, detail=str(e))
1369
1362
 
1370
-
1371
1363
  @app.get(
1372
1364
  "/v1/default/chunks/{chunk_id}",
1373
1365
  response_model=ChunkResponse,
1374
1366
  summary="Get chunk details",
1375
1367
  description="Get a specific chunk by its ID",
1376
1368
  operation_id="get_chunk",
1377
- tags=["Documents"]
1369
+ tags=["Documents"],
1378
1370
  )
1379
1371
  async def api_get_chunk(chunk_id: str):
1380
1372
  """
@@ -1392,11 +1384,11 @@ def _register_routes(app: FastAPI):
1392
1384
  raise
1393
1385
  except Exception as e:
1394
1386
  import traceback
1387
+
1395
1388
  error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
1396
1389
  logger.error(f"Error in /v1/default/chunks/{chunk_id}: {error_detail}")
1397
1390
  raise HTTPException(status_code=500, detail=str(e))
1398
1391
 
1399
-
1400
1392
  @app.delete(
1401
1393
  "/v1/default/banks/{bank_id}/documents/{document_id}",
1402
1394
  summary="Delete a document",
@@ -1407,11 +1399,9 @@ def _register_routes(app: FastAPI):
1407
1399
  "- All links (temporal, semantic, entity) associated with those memory units\n\n"
1408
1400
  "This operation cannot be undone.",
1409
1401
  operation_id="delete_document",
1410
- tags=["Documents"]
1402
+ tags=["Documents"],
1411
1403
  )
1412
- async def api_delete_document(bank_id: str,
1413
- document_id: str
1414
- ):
1404
+ async def api_delete_document(bank_id: str, document_id: str):
1415
1405
  """
1416
1406
  Delete a document and all its associated memory units and links.
1417
1407
 
@@ -1429,23 +1419,23 @@ def _register_routes(app: FastAPI):
1429
1419
  "success": True,
1430
1420
  "message": f"Document '{document_id}' and {result['memory_units_deleted']} associated memory units deleted successfully",
1431
1421
  "document_id": document_id,
1432
- "memory_units_deleted": result["memory_units_deleted"]
1422
+ "memory_units_deleted": result["memory_units_deleted"],
1433
1423
  }
1434
1424
  except HTTPException:
1435
1425
  raise
1436
1426
  except Exception as e:
1437
1427
  import traceback
1428
+
1438
1429
  error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
1439
1430
  logger.error(f"Error in /v1/default/banks/{bank_id}/documents/{document_id}: {error_detail}")
1440
1431
  raise HTTPException(status_code=500, detail=str(e))
1441
1432
 
1442
-
1443
1433
  @app.get(
1444
1434
  "/v1/default/banks/{bank_id}/operations",
1445
1435
  summary="List async operations",
1446
1436
  description="Get a list of all async operations (pending and failed) for a specific agent, including error messages for failed operations",
1447
1437
  operation_id="list_operations",
1448
- tags=["Operations"]
1438
+ tags=["Operations"],
1449
1439
  )
1450
1440
  async def api_list_operations(bank_id: str):
1451
1441
  """List all async operations (pending and failed) for a memory bank."""
@@ -1459,38 +1449,42 @@ def _register_routes(app: FastAPI):
1459
1449
  WHERE bank_id = $1
1460
1450
  ORDER BY created_at DESC
1461
1451
  """,
1462
- bank_id
1452
+ bank_id,
1463
1453
  )
1464
1454
 
1465
1455
  return {
1466
1456
  "bank_id": bank_id,
1467
1457
  "operations": [
1468
1458
  {
1469
- "id": str(row['operation_id']),
1470
- "task_type": row['operation_type'],
1471
- "items_count": row['result_metadata'].get('items_count', 0) if row['result_metadata'] else 0,
1472
- "document_id": row['result_metadata'].get('document_id') if row['result_metadata'] else None,
1473
- "created_at": row['created_at'].isoformat(),
1474
- "status": row['status'],
1475
- "error_message": row['error_message']
1459
+ "id": str(row["operation_id"]),
1460
+ "task_type": row["operation_type"],
1461
+ "items_count": row["result_metadata"].get("items_count", 0)
1462
+ if row["result_metadata"]
1463
+ else 0,
1464
+ "document_id": row["result_metadata"].get("document_id")
1465
+ if row["result_metadata"]
1466
+ else None,
1467
+ "created_at": row["created_at"].isoformat(),
1468
+ "status": row["status"],
1469
+ "error_message": row["error_message"],
1476
1470
  }
1477
1471
  for row in operations
1478
- ]
1472
+ ],
1479
1473
  }
1480
1474
 
1481
1475
  except Exception as e:
1482
1476
  import traceback
1477
+
1483
1478
  error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
1484
1479
  logger.error(f"Error in /v1/default/banks/{bank_id}/operations: {error_detail}")
1485
1480
  raise HTTPException(status_code=500, detail=str(e))
1486
1481
 
1487
-
1488
1482
  @app.delete(
1489
1483
  "/v1/default/banks/{bank_id}/operations/{operation_id}",
1490
1484
  summary="Cancel a pending async operation",
1491
1485
  description="Cancel a pending async operation by removing it from the queue",
1492
1486
  operation_id="cancel_operation",
1493
- tags=["Operations"]
1487
+ tags=["Operations"],
1494
1488
  )
1495
1489
  async def api_cancel_operation(bank_id: str, operation_id: str):
1496
1490
  """Cancel a pending async operation."""
@@ -1505,115 +1499,111 @@ def _register_routes(app: FastAPI):
1505
1499
  async with acquire_with_retry(pool) as conn:
1506
1500
  # Check if operation exists and belongs to this memory bank
1507
1501
  result = await conn.fetchrow(
1508
- "SELECT bank_id FROM async_operations WHERE id = $1 AND bank_id = $2",
1509
- op_uuid,
1510
- bank_id
1502
+ "SELECT bank_id FROM async_operations WHERE id = $1 AND bank_id = $2", op_uuid, bank_id
1511
1503
  )
1512
1504
 
1513
1505
  if not result:
1514
- raise HTTPException(status_code=404, detail=f"Operation {operation_id} not found for memory bank {bank_id}")
1506
+ raise HTTPException(
1507
+ status_code=404, detail=f"Operation {operation_id} not found for memory bank {bank_id}"
1508
+ )
1515
1509
 
1516
1510
  # Delete the operation
1517
- await conn.execute(
1518
- "DELETE FROM async_operations WHERE id = $1",
1519
- op_uuid
1520
- )
1511
+ await conn.execute("DELETE FROM async_operations WHERE id = $1", op_uuid)
1521
1512
 
1522
1513
  return {
1523
1514
  "success": True,
1524
1515
  "message": f"Operation {operation_id} cancelled",
1525
1516
  "operation_id": operation_id,
1526
- "bank_id": bank_id
1517
+ "bank_id": bank_id,
1527
1518
  }
1528
1519
 
1529
1520
  except HTTPException:
1530
1521
  raise
1531
1522
  except Exception as e:
1532
1523
  import traceback
1524
+
1533
1525
  error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
1534
1526
  logger.error(f"Error in /v1/default/banks/{bank_id}/operations/{operation_id}: {error_detail}")
1535
1527
  raise HTTPException(status_code=500, detail=str(e))
1536
1528
 
1537
-
1538
1529
  @app.get(
1539
1530
  "/v1/default/banks/{bank_id}/profile",
1540
1531
  response_model=BankProfileResponse,
1541
1532
  summary="Get memory bank profile",
1542
1533
  description="Get disposition traits and background for a memory bank. Auto-creates agent with defaults if not exists.",
1543
1534
  operation_id="get_bank_profile",
1544
- tags=["Banks"]
1535
+ tags=["Banks"],
1545
1536
  )
1546
1537
  async def api_get_bank_profile(bank_id: str):
1547
1538
  """Get memory bank profile (disposition + background)."""
1548
1539
  try:
1549
1540
  profile = await app.state.memory.get_bank_profile(bank_id)
1550
1541
  # Convert DispositionTraits object to dict for Pydantic
1551
- disposition_dict = profile["disposition"].model_dump() if hasattr(profile["disposition"], 'model_dump') else dict(profile["disposition"])
1542
+ disposition_dict = (
1543
+ profile["disposition"].model_dump()
1544
+ if hasattr(profile["disposition"], "model_dump")
1545
+ else dict(profile["disposition"])
1546
+ )
1552
1547
  return BankProfileResponse(
1553
1548
  bank_id=bank_id,
1554
1549
  name=profile["name"],
1555
1550
  disposition=DispositionTraits(**disposition_dict),
1556
- background=profile["background"]
1551
+ background=profile["background"],
1557
1552
  )
1558
1553
  except Exception as e:
1559
1554
  import traceback
1555
+
1560
1556
  error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
1561
1557
  logger.error(f"Error in /v1/default/banks/{bank_id}/profile: {error_detail}")
1562
1558
  raise HTTPException(status_code=500, detail=str(e))
1563
1559
 
1564
-
1565
1560
  @app.put(
1566
1561
  "/v1/default/banks/{bank_id}/profile",
1567
1562
  response_model=BankProfileResponse,
1568
1563
  summary="Update memory bank disposition",
1569
1564
  description="Update bank's disposition traits (skepticism, literalism, empathy)",
1570
1565
  operation_id="update_bank_disposition",
1571
- tags=["Banks"]
1566
+ tags=["Banks"],
1572
1567
  )
1573
- async def api_update_bank_disposition(bank_id: str,
1574
- request: UpdateDispositionRequest
1575
- ):
1568
+ async def api_update_bank_disposition(bank_id: str, request: UpdateDispositionRequest):
1576
1569
  """Update bank disposition traits."""
1577
1570
  try:
1578
1571
  # Update disposition
1579
- await app.state.memory.update_bank_disposition(
1580
- bank_id,
1581
- request.disposition.model_dump()
1582
- )
1572
+ await app.state.memory.update_bank_disposition(bank_id, request.disposition.model_dump())
1583
1573
 
1584
1574
  # Get updated profile
1585
1575
  profile = await app.state.memory.get_bank_profile(bank_id)
1586
- disposition_dict = profile["disposition"].model_dump() if hasattr(profile["disposition"], 'model_dump') else dict(profile["disposition"])
1576
+ disposition_dict = (
1577
+ profile["disposition"].model_dump()
1578
+ if hasattr(profile["disposition"], "model_dump")
1579
+ else dict(profile["disposition"])
1580
+ )
1587
1581
  return BankProfileResponse(
1588
1582
  bank_id=bank_id,
1589
1583
  name=profile["name"],
1590
1584
  disposition=DispositionTraits(**disposition_dict),
1591
- background=profile["background"]
1585
+ background=profile["background"],
1592
1586
  )
1593
1587
  except Exception as e:
1594
1588
  import traceback
1589
+
1595
1590
  error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
1596
1591
  logger.error(f"Error in /v1/default/banks/{bank_id}/profile: {error_detail}")
1597
1592
  raise HTTPException(status_code=500, detail=str(e))
1598
1593
 
1599
-
1600
1594
  @app.post(
1601
1595
  "/v1/default/banks/{bank_id}/background",
1602
1596
  response_model=BackgroundResponse,
1603
1597
  summary="Add/merge memory bank background",
1604
1598
  description="Add new background information or merge with existing. LLM intelligently resolves conflicts, normalizes to first person, and optionally infers disposition traits.",
1605
1599
  operation_id="add_bank_background",
1606
- tags=["Banks"]
1600
+ tags=["Banks"],
1607
1601
  )
1608
- async def api_add_bank_background(bank_id: str,
1609
- request: AddBackgroundRequest
1610
- ):
1602
+ async def api_add_bank_background(bank_id: str, request: AddBackgroundRequest):
1611
1603
  """Add or merge bank background information. Optionally infer disposition traits."""
1612
1604
  try:
1613
1605
  result = await app.state.memory.merge_bank_background(
1614
- bank_id,
1615
- request.content,
1616
- update_disposition=request.update_disposition
1606
+ bank_id, request.content, update_disposition=request.update_disposition
1617
1607
  )
1618
1608
 
1619
1609
  response = BackgroundResponse(background=result["background"])
@@ -1623,22 +1613,20 @@ def _register_routes(app: FastAPI):
1623
1613
  return response
1624
1614
  except Exception as e:
1625
1615
  import traceback
1616
+
1626
1617
  error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
1627
1618
  logger.error(f"Error in /v1/default/banks/{bank_id}/background: {error_detail}")
1628
1619
  raise HTTPException(status_code=500, detail=str(e))
1629
1620
 
1630
-
1631
1621
  @app.put(
1632
1622
  "/v1/default/banks/{bank_id}",
1633
1623
  response_model=BankProfileResponse,
1634
1624
  summary="Create or update memory bank",
1635
1625
  description="Create a new agent or update existing agent with disposition and background. Auto-fills missing fields with defaults.",
1636
1626
  operation_id="create_or_update_bank",
1637
- tags=["Banks"]
1627
+ tags=["Banks"],
1638
1628
  )
1639
- async def api_create_or_update_bank(bank_id: str,
1640
- request: CreateBankRequest
1641
- ):
1629
+ async def api_create_or_update_bank(bank_id: str, request: CreateBankRequest):
1642
1630
  """Create or update an agent with disposition and background."""
1643
1631
  try:
1644
1632
  # Get existing profile or create with defaults
@@ -1656,16 +1644,13 @@ def _register_routes(app: FastAPI):
1656
1644
  WHERE bank_id = $1
1657
1645
  """,
1658
1646
  bank_id,
1659
- request.name
1647
+ request.name,
1660
1648
  )
1661
1649
  profile["name"] = request.name
1662
1650
 
1663
1651
  # Update disposition if provided
1664
1652
  if request.disposition is not None:
1665
- await app.state.memory.update_bank_disposition(
1666
- bank_id,
1667
- request.disposition.model_dump()
1668
- )
1653
+ await app.state.memory.update_bank_disposition(bank_id, request.disposition.model_dump())
1669
1654
  profile["disposition"] = request.disposition.model_dump()
1670
1655
 
1671
1656
  # Update background if provided (replace, not merge)
@@ -1680,26 +1665,30 @@ def _register_routes(app: FastAPI):
1680
1665
  WHERE bank_id = $1
1681
1666
  """,
1682
1667
  bank_id,
1683
- request.background
1668
+ request.background,
1684
1669
  )
1685
1670
  profile["background"] = request.background
1686
1671
 
1687
1672
  # Get final profile
1688
1673
  final_profile = await app.state.memory.get_bank_profile(bank_id)
1689
- disposition_dict = final_profile["disposition"].model_dump() if hasattr(final_profile["disposition"], 'model_dump') else dict(final_profile["disposition"])
1674
+ disposition_dict = (
1675
+ final_profile["disposition"].model_dump()
1676
+ if hasattr(final_profile["disposition"], "model_dump")
1677
+ else dict(final_profile["disposition"])
1678
+ )
1690
1679
  return BankProfileResponse(
1691
1680
  bank_id=bank_id,
1692
1681
  name=final_profile["name"],
1693
1682
  disposition=DispositionTraits(**disposition_dict),
1694
- background=final_profile["background"]
1683
+ background=final_profile["background"],
1695
1684
  )
1696
1685
  except Exception as e:
1697
1686
  import traceback
1687
+
1698
1688
  error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
1699
1689
  logger.error(f"Error in /v1/default/banks/{bank_id}: {error_detail}")
1700
1690
  raise HTTPException(status_code=500, detail=str(e))
1701
1691
 
1702
-
1703
1692
  @app.delete(
1704
1693
  "/v1/default/banks/{bank_id}",
1705
1694
  response_model=DeleteResponse,
@@ -1707,7 +1696,7 @@ def _register_routes(app: FastAPI):
1707
1696
  description="Delete an entire memory bank including all memories, entities, documents, and the bank profile itself. "
1708
1697
  "This is a destructive operation that cannot be undone.",
1709
1698
  operation_id="delete_bank",
1710
- tags=["Banks"]
1699
+ tags=["Banks"],
1711
1700
  )
1712
1701
  async def api_delete_bank(bank_id: str):
1713
1702
  """Delete an entire memory bank and all its data."""
@@ -1716,15 +1705,17 @@ def _register_routes(app: FastAPI):
1716
1705
  return DeleteResponse(
1717
1706
  success=True,
1718
1707
  message=f"Bank '{bank_id}' and all associated data deleted successfully",
1719
- deleted_count=result.get("memory_units_deleted", 0) + result.get("entities_deleted", 0) + result.get("documents_deleted", 0)
1708
+ deleted_count=result.get("memory_units_deleted", 0)
1709
+ + result.get("entities_deleted", 0)
1710
+ + result.get("documents_deleted", 0),
1720
1711
  )
1721
1712
  except Exception as e:
1722
1713
  import traceback
1714
+
1723
1715
  error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
1724
1716
  logger.error(f"Error in DELETE /v1/default/banks/{bank_id}: {error_detail}")
1725
1717
  raise HTTPException(status_code=500, detail=str(e))
1726
1718
 
1727
-
1728
1719
  @app.post(
1729
1720
  "/v1/default/banks/{bank_id}/memories",
1730
1721
  response_model=RetainResponse,
@@ -1748,7 +1739,7 @@ def _register_routes(app: FastAPI):
1748
1739
  "**When `async=false` (default):** Waits for processing to complete.\n\n"
1749
1740
  "**Note:** If a memory item has a `document_id` that already exists, the old document and its memory units will be deleted before creating new ones (upsert behavior).",
1750
1741
  operation_id="retain_memories",
1751
- tags=["Memory"]
1742
+ tags=["Memory"],
1752
1743
  )
1753
1744
  async def api_retain(bank_id: str, request: RetainRequest):
1754
1745
  """Retain memories with optional async processing."""
@@ -1783,67 +1774,58 @@ def _register_routes(app: FastAPI):
1783
1774
  """,
1784
1775
  operation_id,
1785
1776
  bank_id,
1786
- 'retain',
1787
- len(contents)
1777
+ "retain",
1778
+ len(contents),
1788
1779
  )
1789
1780
 
1790
1781
  # Submit task to background queue
1791
- await app.state.memory._task_backend.submit_task({
1792
- 'type': 'batch_retain',
1793
- 'operation_id': str(operation_id),
1794
- 'bank_id': bank_id,
1795
- 'contents': contents
1796
- })
1797
-
1798
- logging.info(f"Retain task queued for bank_id={bank_id}, {len(contents)} items, operation_id={operation_id}")
1782
+ await app.state.memory._task_backend.submit_task(
1783
+ {
1784
+ "type": "batch_retain",
1785
+ "operation_id": str(operation_id),
1786
+ "bank_id": bank_id,
1787
+ "contents": contents,
1788
+ }
1789
+ )
1799
1790
 
1800
- return RetainResponse(
1801
- success=True,
1802
- bank_id=bank_id,
1803
- items_count=len(contents),
1804
- async_=True
1791
+ logging.info(
1792
+ f"Retain task queued for bank_id={bank_id}, {len(contents)} items, operation_id={operation_id}"
1805
1793
  )
1794
+
1795
+ return RetainResponse(success=True, bank_id=bank_id, items_count=len(contents), async_=True)
1806
1796
  else:
1807
1797
  # Synchronous processing: wait for completion (record metrics)
1808
1798
  with metrics.record_operation("retain", bank_id=bank_id):
1809
- result = await app.state.memory.retain_batch_async(
1810
- bank_id=bank_id,
1811
- contents=contents
1812
- )
1799
+ result = await app.state.memory.retain_batch_async(bank_id=bank_id, contents=contents)
1813
1800
 
1814
- return RetainResponse(
1815
- success=True,
1816
- bank_id=bank_id,
1817
- items_count=len(contents),
1818
- async_=False
1819
- )
1801
+ return RetainResponse(success=True, bank_id=bank_id, items_count=len(contents), async_=False)
1820
1802
  except Exception as e:
1821
1803
  import traceback
1804
+
1822
1805
  error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
1823
1806
  logger.error(f"Error in /v1/default/banks/{bank_id}/memories (retain): {error_detail}")
1824
1807
  raise HTTPException(status_code=500, detail=str(e))
1825
1808
 
1826
-
1827
1809
  @app.delete(
1828
1810
  "/v1/default/banks/{bank_id}/memories",
1829
1811
  response_model=DeleteResponse,
1830
1812
  summary="Clear memory bank memories",
1831
1813
  description="Delete memory units for a memory bank. Optionally filter by type (world, experience, opinion) to delete only specific types. This is a destructive operation that cannot be undone. The bank profile (disposition and background) will be preserved.",
1832
1814
  operation_id="clear_bank_memories",
1833
- tags=["Memory"]
1815
+ tags=["Memory"],
1834
1816
  )
1835
- async def api_clear_bank_memories(bank_id: str,
1836
- type: Optional[str] = Query(None, description="Optional fact type filter (world, experience, opinion)")
1817
+ async def api_clear_bank_memories(
1818
+ bank_id: str,
1819
+ type: str | None = Query(None, description="Optional fact type filter (world, experience, opinion)"),
1837
1820
  ):
1838
1821
  """Clear memories for a memory bank, optionally filtered by type."""
1839
1822
  try:
1840
1823
  await app.state.memory.delete_bank(bank_id, fact_type=type)
1841
1824
 
1842
- return DeleteResponse(
1843
- success=True
1844
- )
1825
+ return DeleteResponse(success=True)
1845
1826
  except Exception as e:
1846
1827
  import traceback
1828
+
1847
1829
  error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
1848
1830
  logger.error(f"Error in /v1/default/banks/{bank_id}/memories: {error_detail}")
1849
1831
  raise HTTPException(status_code=500, detail=str(e))