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.
- hindsight_api/__init__.py +10 -9
- hindsight_api/alembic/env.py +5 -8
- hindsight_api/alembic/versions/5a366d414dce_initial_schema.py +266 -180
- hindsight_api/alembic/versions/b7c4d8e9f1a2_add_chunks_table.py +32 -32
- hindsight_api/alembic/versions/c8e5f2a3b4d1_add_retain_params_to_documents.py +11 -11
- hindsight_api/alembic/versions/d9f6a3b4c5e2_rename_bank_to_interactions.py +7 -12
- hindsight_api/alembic/versions/e0a1b2c3d4e5_disposition_to_3_traits.py +23 -15
- hindsight_api/alembic/versions/rename_personality_to_disposition.py +30 -21
- hindsight_api/api/__init__.py +10 -10
- hindsight_api/api/http.py +575 -593
- hindsight_api/api/mcp.py +31 -33
- hindsight_api/banner.py +13 -6
- hindsight_api/config.py +17 -12
- hindsight_api/engine/__init__.py +9 -9
- hindsight_api/engine/cross_encoder.py +23 -27
- hindsight_api/engine/db_utils.py +5 -4
- hindsight_api/engine/embeddings.py +22 -21
- hindsight_api/engine/entity_resolver.py +81 -75
- hindsight_api/engine/llm_wrapper.py +74 -88
- hindsight_api/engine/memory_engine.py +663 -673
- hindsight_api/engine/query_analyzer.py +100 -97
- hindsight_api/engine/response_models.py +105 -106
- hindsight_api/engine/retain/__init__.py +9 -16
- hindsight_api/engine/retain/bank_utils.py +34 -58
- hindsight_api/engine/retain/chunk_storage.py +4 -12
- hindsight_api/engine/retain/deduplication.py +9 -28
- hindsight_api/engine/retain/embedding_processing.py +4 -11
- hindsight_api/engine/retain/embedding_utils.py +3 -4
- hindsight_api/engine/retain/entity_processing.py +7 -17
- hindsight_api/engine/retain/fact_extraction.py +155 -165
- hindsight_api/engine/retain/fact_storage.py +11 -23
- hindsight_api/engine/retain/link_creation.py +11 -39
- hindsight_api/engine/retain/link_utils.py +166 -95
- hindsight_api/engine/retain/observation_regeneration.py +39 -52
- hindsight_api/engine/retain/orchestrator.py +72 -62
- hindsight_api/engine/retain/types.py +49 -43
- hindsight_api/engine/search/__init__.py +15 -1
- hindsight_api/engine/search/fusion.py +6 -15
- hindsight_api/engine/search/graph_retrieval.py +234 -0
- hindsight_api/engine/search/mpfp_retrieval.py +438 -0
- hindsight_api/engine/search/observation_utils.py +9 -16
- hindsight_api/engine/search/reranking.py +4 -7
- hindsight_api/engine/search/retrieval.py +388 -193
- hindsight_api/engine/search/scoring.py +5 -7
- hindsight_api/engine/search/temporal_extraction.py +8 -11
- hindsight_api/engine/search/think_utils.py +115 -39
- hindsight_api/engine/search/trace.py +68 -38
- hindsight_api/engine/search/tracer.py +49 -35
- hindsight_api/engine/search/types.py +22 -16
- hindsight_api/engine/task_backend.py +21 -26
- hindsight_api/engine/utils.py +25 -10
- hindsight_api/main.py +21 -40
- hindsight_api/mcp_local.py +190 -0
- hindsight_api/metrics.py +44 -30
- hindsight_api/migrations.py +10 -8
- hindsight_api/models.py +60 -72
- hindsight_api/pg0.py +64 -337
- hindsight_api/server.py +3 -6
- {hindsight_api-0.1.4.dist-info → hindsight_api-0.1.6.dist-info}/METADATA +6 -5
- hindsight_api-0.1.6.dist-info/RECORD +64 -0
- {hindsight_api-0.1.4.dist-info → hindsight_api-0.1.6.dist-info}/entry_points.txt +1 -0
- hindsight_api-0.1.4.dist-info/RECORD +0 -61
- {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) ->
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
"
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
"
|
|
80
|
-
|
|
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:
|
|
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:
|
|
92
|
-
|
|
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:
|
|
119
|
-
entities:
|
|
120
|
-
context:
|
|
121
|
-
occurred_start:
|
|
122
|
-
occurred_end:
|
|
123
|
-
mentioned_at:
|
|
124
|
-
document_id:
|
|
125
|
-
metadata:
|
|
126
|
-
chunk_id:
|
|
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:
|
|
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:
|
|
147
|
+
observations: list[EntityObservationResponse]
|
|
140
148
|
|
|
141
149
|
|
|
142
150
|
class EntityListItem(BaseModel):
|
|
143
151
|
"""Entity list item with summary."""
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
"
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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:
|
|
158
|
-
last_seen:
|
|
159
|
-
metadata:
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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:
|
|
192
|
+
items: list[EntityListItem]
|
|
179
193
|
|
|
180
194
|
|
|
181
195
|
class EntityDetailResponse(BaseModel):
|
|
182
196
|
"""Response model for entity detail endpoint."""
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
"
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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:
|
|
200
|
-
last_seen:
|
|
201
|
-
metadata:
|
|
202
|
-
observations:
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
"
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
"
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
"
|
|
247
|
-
|
|
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:
|
|
254
|
-
trace:
|
|
255
|
-
entities:
|
|
256
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
"
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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:
|
|
273
|
-
context:
|
|
274
|
-
metadata:
|
|
275
|
-
document_id:
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
"context": "work",
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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:
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
"
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
"
|
|
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:
|
|
358
|
-
include: ReflectIncludeOptions = Field(
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
"
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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:
|
|
405
|
+
id: str | None = None
|
|
381
406
|
text: str
|
|
382
|
-
type:
|
|
383
|
-
context:
|
|
384
|
-
occurred_start:
|
|
385
|
-
occurred_end:
|
|
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
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
"
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
"text": "AI is used in healthcare",
|
|
397
|
-
"type": "
|
|
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:
|
|
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:
|
|
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
|
-
|
|
426
|
-
|
|
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
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
"
|
|
444
|
-
|
|
445
|
-
"
|
|
446
|
-
"literalism": 3,
|
|
447
|
-
"
|
|
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
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
"
|
|
486
|
-
"
|
|
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:
|
|
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:
|
|
504
|
-
updated_at:
|
|
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
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
"
|
|
517
|
-
"literalism": 3,
|
|
518
|
-
"
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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:
|
|
536
|
+
banks: list[BankListItem]
|
|
529
537
|
|
|
530
538
|
|
|
531
539
|
class CreateBankRequest(BaseModel):
|
|
532
540
|
"""Request model for creating/updating a bank."""
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
"
|
|
537
|
-
"
|
|
538
|
-
"literalism": 3,
|
|
539
|
-
"
|
|
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:
|
|
546
|
-
disposition:
|
|
547
|
-
background:
|
|
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
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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:
|
|
569
|
-
edges:
|
|
570
|
-
table_rows:
|
|
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
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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:
|
|
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
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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:
|
|
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
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
"
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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:
|
|
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
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
"
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
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:
|
|
683
|
-
deleted_count:
|
|
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(
|
|
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(
|
|
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(
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
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[
|
|
1119
|
-
pending_operations = ops_by_status.get(
|
|
1120
|
-
failed_operations = ops_by_status.get(
|
|
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[
|
|
1138
|
+
total_documents = doc_count_result["count"] if doc_count_result else 0
|
|
1132
1139
|
|
|
1133
1140
|
# Format results
|
|
1134
|
-
nodes_by_type = {row[
|
|
1135
|
-
links_by_type = {row[
|
|
1136
|
-
links_by_fact_type = {row[
|
|
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[
|
|
1142
|
-
link_type = row[
|
|
1143
|
-
count = row[
|
|
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(
|
|
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,
|
|
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[
|
|
1226
|
-
canonical_name=entity_row[
|
|
1227
|
-
mention_count=entity_row[
|
|
1228
|
-
first_seen=entity_row[
|
|
1229
|
-
last_seen=entity_row[
|
|
1230
|
-
metadata=_parse_metadata(entity_row[
|
|
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
|
-
|
|
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,
|
|
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[
|
|
1284
|
-
canonical_name=entity_row[
|
|
1285
|
-
mention_count=entity_row[
|
|
1286
|
-
first_seen=entity_row[
|
|
1287
|
-
last_seen=entity_row[
|
|
1288
|
-
metadata=_parse_metadata(entity_row[
|
|
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
|
-
|
|
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[
|
|
1470
|
-
"task_type": row[
|
|
1471
|
-
"items_count": row[
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
"
|
|
1475
|
-
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
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
|
-
|
|
1801
|
-
|
|
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(
|
|
1836
|
-
|
|
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))
|