hindsight-api 0.1.11__py3-none-any.whl → 0.1.13__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 +2 -0
- hindsight_api/alembic/env.py +24 -1
- hindsight_api/alembic/versions/d9f6a3b4c5e2_rename_bank_to_interactions.py +14 -4
- hindsight_api/alembic/versions/e0a1b2c3d4e5_disposition_to_3_traits.py +54 -13
- hindsight_api/alembic/versions/rename_personality_to_disposition.py +18 -7
- hindsight_api/api/http.py +253 -230
- hindsight_api/api/mcp.py +14 -3
- hindsight_api/config.py +11 -0
- hindsight_api/daemon.py +204 -0
- hindsight_api/engine/__init__.py +12 -1
- hindsight_api/engine/entity_resolver.py +38 -37
- hindsight_api/engine/interface.py +592 -0
- hindsight_api/engine/llm_wrapper.py +176 -6
- hindsight_api/engine/memory_engine.py +1092 -293
- hindsight_api/engine/retain/bank_utils.py +13 -12
- hindsight_api/engine/retain/chunk_storage.py +3 -2
- hindsight_api/engine/retain/fact_storage.py +10 -7
- hindsight_api/engine/retain/link_utils.py +17 -16
- hindsight_api/engine/retain/observation_regeneration.py +17 -16
- hindsight_api/engine/retain/orchestrator.py +2 -3
- hindsight_api/engine/retain/types.py +25 -8
- hindsight_api/engine/search/graph_retrieval.py +6 -5
- hindsight_api/engine/search/mpfp_retrieval.py +8 -7
- hindsight_api/engine/search/reranking.py +17 -0
- hindsight_api/engine/search/retrieval.py +12 -11
- hindsight_api/engine/search/think_utils.py +1 -1
- hindsight_api/engine/search/tracer.py +1 -1
- hindsight_api/engine/task_backend.py +32 -0
- hindsight_api/extensions/__init__.py +66 -0
- hindsight_api/extensions/base.py +81 -0
- hindsight_api/extensions/builtin/__init__.py +18 -0
- hindsight_api/extensions/builtin/tenant.py +33 -0
- hindsight_api/extensions/context.py +110 -0
- hindsight_api/extensions/http.py +89 -0
- hindsight_api/extensions/loader.py +125 -0
- hindsight_api/extensions/operation_validator.py +325 -0
- hindsight_api/extensions/tenant.py +63 -0
- hindsight_api/main.py +97 -17
- hindsight_api/mcp_local.py +7 -1
- hindsight_api/migrations.py +54 -10
- hindsight_api/models.py +15 -0
- hindsight_api/pg0.py +1 -1
- {hindsight_api-0.1.11.dist-info → hindsight_api-0.1.13.dist-info}/METADATA +1 -1
- hindsight_api-0.1.13.dist-info/RECORD +75 -0
- hindsight_api-0.1.11.dist-info/RECORD +0 -64
- {hindsight_api-0.1.11.dist-info → hindsight_api-0.1.13.dist-info}/WHEEL +0 -0
- {hindsight_api-0.1.11.dist-info → hindsight_api-0.1.13.dist-info}/entry_points.txt +0 -0
hindsight_api/api/http.py
CHANGED
|
@@ -12,7 +12,7 @@ from contextlib import asynccontextmanager
|
|
|
12
12
|
from datetime import datetime
|
|
13
13
|
from typing import Any
|
|
14
14
|
|
|
15
|
-
from fastapi import FastAPI, HTTPException, Query
|
|
15
|
+
from fastapi import Depends, FastAPI, Header, HTTPException, Query
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def _parse_metadata(metadata: Any) -> dict[str, Any]:
|
|
@@ -29,13 +29,15 @@ def _parse_metadata(metadata: Any) -> dict[str, Any]:
|
|
|
29
29
|
return {}
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
from pydantic import BaseModel, ConfigDict, Field
|
|
32
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
33
33
|
|
|
34
34
|
from hindsight_api import MemoryEngine
|
|
35
35
|
from hindsight_api.engine.db_utils import acquire_with_retry
|
|
36
|
-
from hindsight_api.engine.memory_engine import Budget
|
|
36
|
+
from hindsight_api.engine.memory_engine import Budget, fq_table
|
|
37
37
|
from hindsight_api.engine.response_models import VALID_RECALL_FACT_TYPES
|
|
38
|
+
from hindsight_api.extensions import HttpExtension, load_extension
|
|
38
39
|
from hindsight_api.metrics import create_metrics_collector, get_metrics_collector, initialize_metrics
|
|
40
|
+
from hindsight_api.models import RequestContext
|
|
39
41
|
|
|
40
42
|
logger = logging.getLogger(__name__)
|
|
41
43
|
|
|
@@ -289,7 +291,7 @@ class MemoryItem(BaseModel):
|
|
|
289
291
|
"metadata": {"source": "slack", "channel": "engineering"},
|
|
290
292
|
"document_id": "meeting_notes_2024_01_15",
|
|
291
293
|
}
|
|
292
|
-
}
|
|
294
|
+
},
|
|
293
295
|
)
|
|
294
296
|
|
|
295
297
|
content: str
|
|
@@ -298,6 +300,23 @@ class MemoryItem(BaseModel):
|
|
|
298
300
|
metadata: dict[str, str] | None = None
|
|
299
301
|
document_id: str | None = Field(default=None, description="Optional document ID for this memory item.")
|
|
300
302
|
|
|
303
|
+
@field_validator("timestamp", mode="before")
|
|
304
|
+
@classmethod
|
|
305
|
+
def validate_timestamp(cls, v):
|
|
306
|
+
if v is None or v == "":
|
|
307
|
+
return None
|
|
308
|
+
if isinstance(v, datetime):
|
|
309
|
+
return v
|
|
310
|
+
if isinstance(v, str):
|
|
311
|
+
try:
|
|
312
|
+
# Try parsing as ISO format
|
|
313
|
+
return datetime.fromisoformat(v.replace("Z", "+00:00"))
|
|
314
|
+
except ValueError as e:
|
|
315
|
+
raise ValueError(
|
|
316
|
+
f"Invalid timestamp/event_date format: '{v}'. Expected ISO format like '2024-01-15T10:30:00' or '2024-01-15T10:30:00Z'"
|
|
317
|
+
) from e
|
|
318
|
+
raise ValueError(f"timestamp must be a string or datetime, got {type(v).__name__}")
|
|
319
|
+
|
|
301
320
|
|
|
302
321
|
class RetainRequest(BaseModel):
|
|
303
322
|
"""Request model for retain endpoint."""
|
|
@@ -337,7 +356,7 @@ class RetainResponse(BaseModel):
|
|
|
337
356
|
success: bool
|
|
338
357
|
bank_id: str
|
|
339
358
|
items_count: int
|
|
340
|
-
|
|
359
|
+
is_async: bool = Field(
|
|
341
360
|
alias="async", serialization_alias="async", description="Whether the operation was processed asynchronously"
|
|
342
361
|
)
|
|
343
362
|
|
|
@@ -706,7 +725,11 @@ class DeleteResponse(BaseModel):
|
|
|
706
725
|
deleted_count: int | None = None
|
|
707
726
|
|
|
708
727
|
|
|
709
|
-
def create_app(
|
|
728
|
+
def create_app(
|
|
729
|
+
memory: MemoryEngine,
|
|
730
|
+
initialize_memory: bool = True,
|
|
731
|
+
http_extension: HttpExtension | None = None,
|
|
732
|
+
) -> FastAPI:
|
|
710
733
|
"""
|
|
711
734
|
Create and configure the FastAPI application.
|
|
712
735
|
|
|
@@ -714,6 +737,8 @@ def create_app(memory: MemoryEngine, initialize_memory: bool = True) -> FastAPI:
|
|
|
714
737
|
memory: MemoryEngine instance (already initialized with required parameters).
|
|
715
738
|
Migrations are controlled by the MemoryEngine's run_migrations parameter.
|
|
716
739
|
initialize_memory: Whether to initialize memory system on startup (default: True)
|
|
740
|
+
http_extension: Optional HTTP extension to mount custom endpoints under /extension/.
|
|
741
|
+
If None, attempts to load from HINDSIGHT_API_HTTP_EXTENSION env var.
|
|
717
742
|
|
|
718
743
|
Returns:
|
|
719
744
|
Configured FastAPI application
|
|
@@ -723,6 +748,11 @@ def create_app(memory: MemoryEngine, initialize_memory: bool = True) -> FastAPI:
|
|
|
723
748
|
In that case, you should call memory.initialize() manually before starting the server
|
|
724
749
|
and memory.close() when shutting down.
|
|
725
750
|
"""
|
|
751
|
+
# Load HTTP extension from environment if not provided
|
|
752
|
+
if http_extension is None:
|
|
753
|
+
http_extension = load_extension("HTTP", HttpExtension)
|
|
754
|
+
if http_extension:
|
|
755
|
+
logging.info(f"Loaded HTTP extension: {http_extension.__class__.__name__}")
|
|
726
756
|
|
|
727
757
|
@asynccontextmanager
|
|
728
758
|
async def lifespan(app: FastAPI):
|
|
@@ -746,8 +776,18 @@ def create_app(memory: MemoryEngine, initialize_memory: bool = True) -> FastAPI:
|
|
|
746
776
|
await memory.initialize()
|
|
747
777
|
logging.info("Memory system initialized")
|
|
748
778
|
|
|
779
|
+
# Call HTTP extension startup hook
|
|
780
|
+
if http_extension:
|
|
781
|
+
await http_extension.on_startup()
|
|
782
|
+
logging.info("HTTP extension started")
|
|
783
|
+
|
|
749
784
|
yield
|
|
750
785
|
|
|
786
|
+
# Call HTTP extension shutdown hook
|
|
787
|
+
if http_extension:
|
|
788
|
+
await http_extension.on_shutdown()
|
|
789
|
+
logging.info("HTTP extension stopped")
|
|
790
|
+
|
|
751
791
|
# Shutdown: Cleanup memory system
|
|
752
792
|
await memory.close()
|
|
753
793
|
logging.info("Memory system closed")
|
|
@@ -775,12 +815,36 @@ def create_app(memory: MemoryEngine, initialize_memory: bool = True) -> FastAPI:
|
|
|
775
815
|
# Register all routes
|
|
776
816
|
_register_routes(app)
|
|
777
817
|
|
|
818
|
+
# Mount HTTP extension router if available
|
|
819
|
+
if http_extension:
|
|
820
|
+
extension_router = http_extension.get_router(memory)
|
|
821
|
+
app.include_router(extension_router, prefix="/ext", tags=["Extension"])
|
|
822
|
+
logging.info("HTTP extension router mounted at /ext/")
|
|
823
|
+
|
|
778
824
|
return app
|
|
779
825
|
|
|
780
826
|
|
|
781
827
|
def _register_routes(app: FastAPI):
|
|
782
828
|
"""Register all API routes on the given app instance."""
|
|
783
829
|
|
|
830
|
+
def get_request_context(authorization: str | None = Header(default=None)) -> RequestContext:
|
|
831
|
+
"""
|
|
832
|
+
Extract request context from Authorization header.
|
|
833
|
+
|
|
834
|
+
Supports:
|
|
835
|
+
- Bearer token: "Bearer <api_key>"
|
|
836
|
+
- Direct API key: "<api_key>"
|
|
837
|
+
|
|
838
|
+
Returns RequestContext with extracted API key (may be None if no auth header).
|
|
839
|
+
"""
|
|
840
|
+
api_key = None
|
|
841
|
+
if authorization:
|
|
842
|
+
if authorization.lower().startswith("bearer "):
|
|
843
|
+
api_key = authorization[7:].strip()
|
|
844
|
+
else:
|
|
845
|
+
api_key = authorization.strip()
|
|
846
|
+
return RequestContext(api_key=api_key)
|
|
847
|
+
|
|
784
848
|
@app.get(
|
|
785
849
|
"/health",
|
|
786
850
|
summary="Health check endpoint",
|
|
@@ -821,10 +885,12 @@ def _register_routes(app: FastAPI):
|
|
|
821
885
|
operation_id="get_graph",
|
|
822
886
|
tags=["Memory"],
|
|
823
887
|
)
|
|
824
|
-
async def api_graph(
|
|
888
|
+
async def api_graph(
|
|
889
|
+
bank_id: str, type: str | None = None, request_context: RequestContext = Depends(get_request_context)
|
|
890
|
+
):
|
|
825
891
|
"""Get graph data from database, filtered by bank_id and optionally by type."""
|
|
826
892
|
try:
|
|
827
|
-
data = await app.state.memory.get_graph_data(bank_id, type)
|
|
893
|
+
data = await app.state.memory.get_graph_data(bank_id, type, request_context=request_context)
|
|
828
894
|
return data
|
|
829
895
|
except Exception as e:
|
|
830
896
|
import traceback
|
|
@@ -841,7 +907,14 @@ def _register_routes(app: FastAPI):
|
|
|
841
907
|
operation_id="list_memories",
|
|
842
908
|
tags=["Memory"],
|
|
843
909
|
)
|
|
844
|
-
async def api_list(
|
|
910
|
+
async def api_list(
|
|
911
|
+
bank_id: str,
|
|
912
|
+
type: str | None = None,
|
|
913
|
+
q: str | None = None,
|
|
914
|
+
limit: int = 100,
|
|
915
|
+
offset: int = 0,
|
|
916
|
+
request_context: RequestContext = Depends(get_request_context),
|
|
917
|
+
):
|
|
845
918
|
"""
|
|
846
919
|
List memory units for table view with optional full-text search.
|
|
847
920
|
|
|
@@ -857,7 +930,12 @@ def _register_routes(app: FastAPI):
|
|
|
857
930
|
"""
|
|
858
931
|
try:
|
|
859
932
|
data = await app.state.memory.list_memory_units(
|
|
860
|
-
bank_id=bank_id,
|
|
933
|
+
bank_id=bank_id,
|
|
934
|
+
fact_type=type,
|
|
935
|
+
search_query=q,
|
|
936
|
+
limit=limit,
|
|
937
|
+
offset=offset,
|
|
938
|
+
request_context=request_context,
|
|
861
939
|
)
|
|
862
940
|
return data
|
|
863
941
|
except Exception as e:
|
|
@@ -880,7 +958,9 @@ def _register_routes(app: FastAPI):
|
|
|
880
958
|
operation_id="recall_memories",
|
|
881
959
|
tags=["Memory"],
|
|
882
960
|
)
|
|
883
|
-
async def api_recall(
|
|
961
|
+
async def api_recall(
|
|
962
|
+
bank_id: str, request: RecallRequest, request_context: RequestContext = Depends(get_request_context)
|
|
963
|
+
):
|
|
884
964
|
"""Run a recall and return results with trace."""
|
|
885
965
|
metrics = get_metrics_collector()
|
|
886
966
|
|
|
@@ -923,6 +1003,7 @@ def _register_routes(app: FastAPI):
|
|
|
923
1003
|
max_entity_tokens=max_entity_tokens,
|
|
924
1004
|
include_chunks=include_chunks,
|
|
925
1005
|
max_chunk_tokens=max_chunk_tokens,
|
|
1006
|
+
request_context=request_context,
|
|
926
1007
|
)
|
|
927
1008
|
|
|
928
1009
|
# Convert core MemoryFact objects to API RecallResult objects (excluding internal metrics)
|
|
@@ -995,14 +1076,20 @@ def _register_routes(app: FastAPI):
|
|
|
995
1076
|
operation_id="reflect",
|
|
996
1077
|
tags=["Memory"],
|
|
997
1078
|
)
|
|
998
|
-
async def api_reflect(
|
|
1079
|
+
async def api_reflect(
|
|
1080
|
+
bank_id: str, request: ReflectRequest, request_context: RequestContext = Depends(get_request_context)
|
|
1081
|
+
):
|
|
999
1082
|
metrics = get_metrics_collector()
|
|
1000
1083
|
|
|
1001
1084
|
try:
|
|
1002
1085
|
# Use the memory system's reflect_async method (record metrics)
|
|
1003
1086
|
with metrics.record_operation("reflect", bank_id=bank_id, budget=request.budget.value):
|
|
1004
1087
|
core_result = await app.state.memory.reflect_async(
|
|
1005
|
-
bank_id=bank_id,
|
|
1088
|
+
bank_id=bank_id,
|
|
1089
|
+
query=request.query,
|
|
1090
|
+
budget=request.budget,
|
|
1091
|
+
context=request.context,
|
|
1092
|
+
request_context=request_context,
|
|
1006
1093
|
)
|
|
1007
1094
|
|
|
1008
1095
|
# Convert core MemoryFact objects to API ReflectFact objects if facts are requested
|
|
@@ -1041,10 +1128,10 @@ def _register_routes(app: FastAPI):
|
|
|
1041
1128
|
operation_id="list_banks",
|
|
1042
1129
|
tags=["Banks"],
|
|
1043
1130
|
)
|
|
1044
|
-
async def api_list_banks():
|
|
1131
|
+
async def api_list_banks(request_context: RequestContext = Depends(get_request_context)):
|
|
1045
1132
|
"""Get list of all banks with their profiles."""
|
|
1046
1133
|
try:
|
|
1047
|
-
banks = await app.state.memory.list_banks()
|
|
1134
|
+
banks = await app.state.memory.list_banks(request_context=request_context)
|
|
1048
1135
|
return BankListResponse(banks=banks)
|
|
1049
1136
|
except Exception as e:
|
|
1050
1137
|
import traceback
|
|
@@ -1067,9 +1154,9 @@ def _register_routes(app: FastAPI):
|
|
|
1067
1154
|
async with acquire_with_retry(pool) as conn:
|
|
1068
1155
|
# Get node counts by fact_type
|
|
1069
1156
|
node_stats = await conn.fetch(
|
|
1070
|
-
"""
|
|
1157
|
+
f"""
|
|
1071
1158
|
SELECT fact_type, COUNT(*) as count
|
|
1072
|
-
FROM memory_units
|
|
1159
|
+
FROM {fq_table("memory_units")}
|
|
1073
1160
|
WHERE bank_id = $1
|
|
1074
1161
|
GROUP BY fact_type
|
|
1075
1162
|
""",
|
|
@@ -1078,10 +1165,10 @@ def _register_routes(app: FastAPI):
|
|
|
1078
1165
|
|
|
1079
1166
|
# Get link counts by link_type
|
|
1080
1167
|
link_stats = await conn.fetch(
|
|
1081
|
-
"""
|
|
1168
|
+
f"""
|
|
1082
1169
|
SELECT ml.link_type, COUNT(*) as count
|
|
1083
|
-
FROM memory_links ml
|
|
1084
|
-
JOIN memory_units mu ON ml.from_unit_id = mu.id
|
|
1170
|
+
FROM {fq_table("memory_links")} ml
|
|
1171
|
+
JOIN {fq_table("memory_units")} mu ON ml.from_unit_id = mu.id
|
|
1085
1172
|
WHERE mu.bank_id = $1
|
|
1086
1173
|
GROUP BY ml.link_type
|
|
1087
1174
|
""",
|
|
@@ -1090,10 +1177,10 @@ def _register_routes(app: FastAPI):
|
|
|
1090
1177
|
|
|
1091
1178
|
# Get link counts by fact_type (from nodes)
|
|
1092
1179
|
link_fact_type_stats = await conn.fetch(
|
|
1093
|
-
"""
|
|
1180
|
+
f"""
|
|
1094
1181
|
SELECT mu.fact_type, COUNT(*) as count
|
|
1095
|
-
FROM memory_links ml
|
|
1096
|
-
JOIN memory_units mu ON ml.from_unit_id = mu.id
|
|
1182
|
+
FROM {fq_table("memory_links")} ml
|
|
1183
|
+
JOIN {fq_table("memory_units")} mu ON ml.from_unit_id = mu.id
|
|
1097
1184
|
WHERE mu.bank_id = $1
|
|
1098
1185
|
GROUP BY mu.fact_type
|
|
1099
1186
|
""",
|
|
@@ -1102,10 +1189,10 @@ def _register_routes(app: FastAPI):
|
|
|
1102
1189
|
|
|
1103
1190
|
# Get link counts by fact_type AND link_type
|
|
1104
1191
|
link_breakdown_stats = await conn.fetch(
|
|
1105
|
-
"""
|
|
1192
|
+
f"""
|
|
1106
1193
|
SELECT mu.fact_type, ml.link_type, COUNT(*) as count
|
|
1107
|
-
FROM memory_links ml
|
|
1108
|
-
JOIN memory_units mu ON ml.from_unit_id = mu.id
|
|
1194
|
+
FROM {fq_table("memory_links")} ml
|
|
1195
|
+
JOIN {fq_table("memory_units")} mu ON ml.from_unit_id = mu.id
|
|
1109
1196
|
WHERE mu.bank_id = $1
|
|
1110
1197
|
GROUP BY mu.fact_type, ml.link_type
|
|
1111
1198
|
""",
|
|
@@ -1114,9 +1201,9 @@ def _register_routes(app: FastAPI):
|
|
|
1114
1201
|
|
|
1115
1202
|
# Get pending and failed operations counts
|
|
1116
1203
|
ops_stats = await conn.fetch(
|
|
1117
|
-
"""
|
|
1204
|
+
f"""
|
|
1118
1205
|
SELECT status, COUNT(*) as count
|
|
1119
|
-
FROM async_operations
|
|
1206
|
+
FROM {fq_table("async_operations")}
|
|
1120
1207
|
WHERE bank_id = $1
|
|
1121
1208
|
GROUP BY status
|
|
1122
1209
|
""",
|
|
@@ -1128,9 +1215,9 @@ def _register_routes(app: FastAPI):
|
|
|
1128
1215
|
|
|
1129
1216
|
# Get document count
|
|
1130
1217
|
doc_count_result = await conn.fetchrow(
|
|
1131
|
-
"""
|
|
1218
|
+
f"""
|
|
1132
1219
|
SELECT COUNT(*) as count
|
|
1133
|
-
FROM documents
|
|
1220
|
+
FROM {fq_table("documents")}
|
|
1134
1221
|
WHERE bank_id = $1
|
|
1135
1222
|
""",
|
|
1136
1223
|
bank_id,
|
|
@@ -1184,11 +1271,13 @@ def _register_routes(app: FastAPI):
|
|
|
1184
1271
|
tags=["Entities"],
|
|
1185
1272
|
)
|
|
1186
1273
|
async def api_list_entities(
|
|
1187
|
-
bank_id: str,
|
|
1274
|
+
bank_id: str,
|
|
1275
|
+
limit: int = Query(default=100, description="Maximum number of entities to return"),
|
|
1276
|
+
request_context: RequestContext = Depends(get_request_context),
|
|
1188
1277
|
):
|
|
1189
1278
|
"""List entities for a memory bank."""
|
|
1190
1279
|
try:
|
|
1191
|
-
entities = await app.state.memory.list_entities(bank_id, limit=limit)
|
|
1280
|
+
entities = await app.state.memory.list_entities(bank_id, limit=limit, request_context=request_context)
|
|
1192
1281
|
return EntityListResponse(items=[EntityListItem(**e) for e in entities])
|
|
1193
1282
|
except Exception as e:
|
|
1194
1283
|
import traceback
|
|
@@ -1205,37 +1294,26 @@ def _register_routes(app: FastAPI):
|
|
|
1205
1294
|
operation_id="get_entity",
|
|
1206
1295
|
tags=["Entities"],
|
|
1207
1296
|
)
|
|
1208
|
-
async def api_get_entity(
|
|
1297
|
+
async def api_get_entity(
|
|
1298
|
+
bank_id: str, entity_id: str, request_context: RequestContext = Depends(get_request_context)
|
|
1299
|
+
):
|
|
1209
1300
|
"""Get entity details with observations."""
|
|
1210
1301
|
try:
|
|
1211
|
-
|
|
1212
|
-
pool = await app.state.memory._get_pool()
|
|
1213
|
-
async with acquire_with_retry(pool) as conn:
|
|
1214
|
-
entity_row = await conn.fetchrow(
|
|
1215
|
-
"""
|
|
1216
|
-
SELECT id, canonical_name, mention_count, first_seen, last_seen, metadata
|
|
1217
|
-
FROM entities
|
|
1218
|
-
WHERE bank_id = $1 AND id = $2
|
|
1219
|
-
""",
|
|
1220
|
-
bank_id,
|
|
1221
|
-
uuid.UUID(entity_id),
|
|
1222
|
-
)
|
|
1302
|
+
entity = await app.state.memory.get_entity(bank_id, entity_id, request_context=request_context)
|
|
1223
1303
|
|
|
1224
|
-
if
|
|
1304
|
+
if entity is None:
|
|
1225
1305
|
raise HTTPException(status_code=404, detail=f"Entity {entity_id} not found")
|
|
1226
1306
|
|
|
1227
|
-
# Get observations for the entity
|
|
1228
|
-
observations = await app.state.memory.get_entity_observations(bank_id, entity_id, limit=20)
|
|
1229
|
-
|
|
1230
1307
|
return EntityDetailResponse(
|
|
1231
|
-
id=
|
|
1232
|
-
canonical_name=
|
|
1233
|
-
mention_count=
|
|
1234
|
-
first_seen=
|
|
1235
|
-
last_seen=
|
|
1236
|
-
metadata=_parse_metadata(
|
|
1308
|
+
id=entity["id"],
|
|
1309
|
+
canonical_name=entity["canonical_name"],
|
|
1310
|
+
mention_count=entity["mention_count"],
|
|
1311
|
+
first_seen=entity["first_seen"],
|
|
1312
|
+
last_seen=entity["last_seen"],
|
|
1313
|
+
metadata=_parse_metadata(entity["metadata"]),
|
|
1237
1314
|
observations=[
|
|
1238
|
-
EntityObservationResponse(text=obs.text, mentioned_at=obs.mentioned_at)
|
|
1315
|
+
EntityObservationResponse(text=obs.text, mentioned_at=obs.mentioned_at)
|
|
1316
|
+
for obs in entity["observations"]
|
|
1239
1317
|
],
|
|
1240
1318
|
)
|
|
1241
1319
|
except HTTPException:
|
|
@@ -1255,42 +1333,40 @@ def _register_routes(app: FastAPI):
|
|
|
1255
1333
|
operation_id="regenerate_entity_observations",
|
|
1256
1334
|
tags=["Entities"],
|
|
1257
1335
|
)
|
|
1258
|
-
async def api_regenerate_entity_observations(
|
|
1336
|
+
async def api_regenerate_entity_observations(
|
|
1337
|
+
bank_id: str,
|
|
1338
|
+
entity_id: str,
|
|
1339
|
+
request_context: RequestContext = Depends(get_request_context),
|
|
1340
|
+
):
|
|
1259
1341
|
"""Regenerate observations for an entity."""
|
|
1260
1342
|
try:
|
|
1261
|
-
#
|
|
1262
|
-
|
|
1263
|
-
async with acquire_with_retry(pool) as conn:
|
|
1264
|
-
entity_row = await conn.fetchrow(
|
|
1265
|
-
"""
|
|
1266
|
-
SELECT id, canonical_name, mention_count, first_seen, last_seen, metadata
|
|
1267
|
-
FROM entities
|
|
1268
|
-
WHERE bank_id = $1 AND id = $2
|
|
1269
|
-
""",
|
|
1270
|
-
bank_id,
|
|
1271
|
-
uuid.UUID(entity_id),
|
|
1272
|
-
)
|
|
1343
|
+
# Get the entity to verify it exists and get canonical_name
|
|
1344
|
+
entity = await app.state.memory.get_entity(bank_id, entity_id, request_context=request_context)
|
|
1273
1345
|
|
|
1274
|
-
if
|
|
1346
|
+
if entity is None:
|
|
1275
1347
|
raise HTTPException(status_code=404, detail=f"Entity {entity_id} not found")
|
|
1276
1348
|
|
|
1277
1349
|
# Regenerate observations
|
|
1278
1350
|
await app.state.memory.regenerate_entity_observations(
|
|
1279
|
-
bank_id=bank_id,
|
|
1351
|
+
bank_id=bank_id,
|
|
1352
|
+
entity_id=entity_id,
|
|
1353
|
+
entity_name=entity["canonical_name"],
|
|
1354
|
+
request_context=request_context,
|
|
1280
1355
|
)
|
|
1281
1356
|
|
|
1282
|
-
# Get updated observations
|
|
1283
|
-
|
|
1357
|
+
# Get updated entity with new observations
|
|
1358
|
+
entity = await app.state.memory.get_entity(bank_id, entity_id, request_context=request_context)
|
|
1284
1359
|
|
|
1285
1360
|
return EntityDetailResponse(
|
|
1286
|
-
id=
|
|
1287
|
-
canonical_name=
|
|
1288
|
-
mention_count=
|
|
1289
|
-
first_seen=
|
|
1290
|
-
last_seen=
|
|
1291
|
-
metadata=_parse_metadata(
|
|
1361
|
+
id=entity["id"],
|
|
1362
|
+
canonical_name=entity["canonical_name"],
|
|
1363
|
+
mention_count=entity["mention_count"],
|
|
1364
|
+
first_seen=entity["first_seen"],
|
|
1365
|
+
last_seen=entity["last_seen"],
|
|
1366
|
+
metadata=_parse_metadata(entity["metadata"]),
|
|
1292
1367
|
observations=[
|
|
1293
|
-
EntityObservationResponse(text=obs.text, mentioned_at=obs.mentioned_at)
|
|
1368
|
+
EntityObservationResponse(text=obs.text, mentioned_at=obs.mentioned_at)
|
|
1369
|
+
for obs in entity["observations"]
|
|
1294
1370
|
],
|
|
1295
1371
|
)
|
|
1296
1372
|
except HTTPException:
|
|
@@ -1310,7 +1386,13 @@ def _register_routes(app: FastAPI):
|
|
|
1310
1386
|
operation_id="list_documents",
|
|
1311
1387
|
tags=["Documents"],
|
|
1312
1388
|
)
|
|
1313
|
-
async def api_list_documents(
|
|
1389
|
+
async def api_list_documents(
|
|
1390
|
+
bank_id: str,
|
|
1391
|
+
q: str | None = None,
|
|
1392
|
+
limit: int = 100,
|
|
1393
|
+
offset: int = 0,
|
|
1394
|
+
request_context: RequestContext = Depends(get_request_context),
|
|
1395
|
+
):
|
|
1314
1396
|
"""
|
|
1315
1397
|
List documents for a memory bank with optional search.
|
|
1316
1398
|
|
|
@@ -1321,7 +1403,9 @@ def _register_routes(app: FastAPI):
|
|
|
1321
1403
|
offset: Offset for pagination (default: 0)
|
|
1322
1404
|
"""
|
|
1323
1405
|
try:
|
|
1324
|
-
data = await app.state.memory.list_documents(
|
|
1406
|
+
data = await app.state.memory.list_documents(
|
|
1407
|
+
bank_id=bank_id, search_query=q, limit=limit, offset=offset, request_context=request_context
|
|
1408
|
+
)
|
|
1325
1409
|
return data
|
|
1326
1410
|
except Exception as e:
|
|
1327
1411
|
import traceback
|
|
@@ -1338,7 +1422,9 @@ def _register_routes(app: FastAPI):
|
|
|
1338
1422
|
operation_id="get_document",
|
|
1339
1423
|
tags=["Documents"],
|
|
1340
1424
|
)
|
|
1341
|
-
async def api_get_document(
|
|
1425
|
+
async def api_get_document(
|
|
1426
|
+
bank_id: str, document_id: str, request_context: RequestContext = Depends(get_request_context)
|
|
1427
|
+
):
|
|
1342
1428
|
"""
|
|
1343
1429
|
Get a specific document with its original text.
|
|
1344
1430
|
|
|
@@ -1347,7 +1433,7 @@ def _register_routes(app: FastAPI):
|
|
|
1347
1433
|
document_id: Document ID (from path)
|
|
1348
1434
|
"""
|
|
1349
1435
|
try:
|
|
1350
|
-
document = await app.state.memory.get_document(document_id, bank_id)
|
|
1436
|
+
document = await app.state.memory.get_document(document_id, bank_id, request_context=request_context)
|
|
1351
1437
|
if not document:
|
|
1352
1438
|
raise HTTPException(status_code=404, detail="Document not found")
|
|
1353
1439
|
return document
|
|
@@ -1368,7 +1454,7 @@ def _register_routes(app: FastAPI):
|
|
|
1368
1454
|
operation_id="get_chunk",
|
|
1369
1455
|
tags=["Documents"],
|
|
1370
1456
|
)
|
|
1371
|
-
async def api_get_chunk(chunk_id: str):
|
|
1457
|
+
async def api_get_chunk(chunk_id: str, request_context: RequestContext = Depends(get_request_context)):
|
|
1372
1458
|
"""
|
|
1373
1459
|
Get a specific chunk with its text.
|
|
1374
1460
|
|
|
@@ -1376,7 +1462,7 @@ def _register_routes(app: FastAPI):
|
|
|
1376
1462
|
chunk_id: Chunk ID (from path, format: bank_id_document_id_chunk_index)
|
|
1377
1463
|
"""
|
|
1378
1464
|
try:
|
|
1379
|
-
chunk = await app.state.memory.get_chunk(chunk_id)
|
|
1465
|
+
chunk = await app.state.memory.get_chunk(chunk_id, request_context=request_context)
|
|
1380
1466
|
if not chunk:
|
|
1381
1467
|
raise HTTPException(status_code=404, detail="Chunk not found")
|
|
1382
1468
|
return chunk
|
|
@@ -1401,7 +1487,9 @@ def _register_routes(app: FastAPI):
|
|
|
1401
1487
|
operation_id="delete_document",
|
|
1402
1488
|
tags=["Documents"],
|
|
1403
1489
|
)
|
|
1404
|
-
async def api_delete_document(
|
|
1490
|
+
async def api_delete_document(
|
|
1491
|
+
bank_id: str, document_id: str, request_context: RequestContext = Depends(get_request_context)
|
|
1492
|
+
):
|
|
1405
1493
|
"""
|
|
1406
1494
|
Delete a document and all its associated memory units and links.
|
|
1407
1495
|
|
|
@@ -1410,7 +1498,7 @@ def _register_routes(app: FastAPI):
|
|
|
1410
1498
|
document_id: Document ID to delete (from path)
|
|
1411
1499
|
"""
|
|
1412
1500
|
try:
|
|
1413
|
-
result = await app.state.memory.delete_document(document_id, bank_id)
|
|
1501
|
+
result = await app.state.memory.delete_document(document_id, bank_id, request_context=request_context)
|
|
1414
1502
|
|
|
1415
1503
|
if result["document_deleted"] == 0:
|
|
1416
1504
|
raise HTTPException(status_code=404, detail="Document not found")
|
|
@@ -1437,45 +1525,14 @@ def _register_routes(app: FastAPI):
|
|
|
1437
1525
|
operation_id="list_operations",
|
|
1438
1526
|
tags=["Operations"],
|
|
1439
1527
|
)
|
|
1440
|
-
async def api_list_operations(bank_id: str):
|
|
1528
|
+
async def api_list_operations(bank_id: str, request_context: RequestContext = Depends(get_request_context)):
|
|
1441
1529
|
"""List all async operations (pending and failed) for a memory bank."""
|
|
1442
1530
|
try:
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
FROM async_operations
|
|
1449
|
-
WHERE bank_id = $1
|
|
1450
|
-
ORDER BY created_at DESC
|
|
1451
|
-
""",
|
|
1452
|
-
bank_id,
|
|
1453
|
-
)
|
|
1454
|
-
|
|
1455
|
-
def parse_metadata(metadata):
|
|
1456
|
-
"""Parse result_metadata which may be a string or dict."""
|
|
1457
|
-
if metadata is None:
|
|
1458
|
-
return {}
|
|
1459
|
-
if isinstance(metadata, str):
|
|
1460
|
-
return json.loads(metadata)
|
|
1461
|
-
return metadata
|
|
1462
|
-
|
|
1463
|
-
return {
|
|
1464
|
-
"bank_id": bank_id,
|
|
1465
|
-
"operations": [
|
|
1466
|
-
{
|
|
1467
|
-
"id": str(row["operation_id"]),
|
|
1468
|
-
"task_type": row["operation_type"],
|
|
1469
|
-
"items_count": parse_metadata(row["result_metadata"]).get("items_count", 0),
|
|
1470
|
-
"document_id": parse_metadata(row["result_metadata"]).get("document_id"),
|
|
1471
|
-
"created_at": row["created_at"].isoformat(),
|
|
1472
|
-
"status": row["status"],
|
|
1473
|
-
"error_message": row["error_message"],
|
|
1474
|
-
}
|
|
1475
|
-
for row in operations
|
|
1476
|
-
],
|
|
1477
|
-
}
|
|
1478
|
-
|
|
1531
|
+
operations = await app.state.memory.list_operations(bank_id, request_context=request_context)
|
|
1532
|
+
return {
|
|
1533
|
+
"bank_id": bank_id,
|
|
1534
|
+
"operations": operations,
|
|
1535
|
+
}
|
|
1479
1536
|
except Exception as e:
|
|
1480
1537
|
import traceback
|
|
1481
1538
|
|
|
@@ -1490,39 +1547,21 @@ def _register_routes(app: FastAPI):
|
|
|
1490
1547
|
operation_id="cancel_operation",
|
|
1491
1548
|
tags=["Operations"],
|
|
1492
1549
|
)
|
|
1493
|
-
async def api_cancel_operation(
|
|
1550
|
+
async def api_cancel_operation(
|
|
1551
|
+
bank_id: str, operation_id: str, request_context: RequestContext = Depends(get_request_context)
|
|
1552
|
+
):
|
|
1494
1553
|
"""Cancel a pending async operation."""
|
|
1495
1554
|
try:
|
|
1496
1555
|
# Validate UUID format
|
|
1497
1556
|
try:
|
|
1498
|
-
|
|
1557
|
+
uuid.UUID(operation_id)
|
|
1499
1558
|
except ValueError:
|
|
1500
1559
|
raise HTTPException(status_code=400, detail=f"Invalid operation_id format: {operation_id}")
|
|
1501
1560
|
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
"SELECT bank_id FROM async_operations WHERE operation_id = $1 AND bank_id = $2", op_uuid, bank_id
|
|
1507
|
-
)
|
|
1508
|
-
|
|
1509
|
-
if not result:
|
|
1510
|
-
raise HTTPException(
|
|
1511
|
-
status_code=404, detail=f"Operation {operation_id} not found for memory bank {bank_id}"
|
|
1512
|
-
)
|
|
1513
|
-
|
|
1514
|
-
# Delete the operation
|
|
1515
|
-
await conn.execute("DELETE FROM async_operations WHERE operation_id = $1", op_uuid)
|
|
1516
|
-
|
|
1517
|
-
return {
|
|
1518
|
-
"success": True,
|
|
1519
|
-
"message": f"Operation {operation_id} cancelled",
|
|
1520
|
-
"operation_id": operation_id,
|
|
1521
|
-
"bank_id": bank_id,
|
|
1522
|
-
}
|
|
1523
|
-
|
|
1524
|
-
except HTTPException:
|
|
1525
|
-
raise
|
|
1561
|
+
result = await app.state.memory.cancel_operation(bank_id, operation_id, request_context=request_context)
|
|
1562
|
+
return result
|
|
1563
|
+
except ValueError as e:
|
|
1564
|
+
raise HTTPException(status_code=404, detail=str(e))
|
|
1526
1565
|
except Exception as e:
|
|
1527
1566
|
import traceback
|
|
1528
1567
|
|
|
@@ -1538,10 +1577,10 @@ def _register_routes(app: FastAPI):
|
|
|
1538
1577
|
operation_id="get_bank_profile",
|
|
1539
1578
|
tags=["Banks"],
|
|
1540
1579
|
)
|
|
1541
|
-
async def api_get_bank_profile(bank_id: str):
|
|
1580
|
+
async def api_get_bank_profile(bank_id: str, request_context: RequestContext = Depends(get_request_context)):
|
|
1542
1581
|
"""Get memory bank profile (disposition + background)."""
|
|
1543
1582
|
try:
|
|
1544
|
-
profile = await app.state.memory.get_bank_profile(bank_id)
|
|
1583
|
+
profile = await app.state.memory.get_bank_profile(bank_id, request_context=request_context)
|
|
1545
1584
|
# Convert DispositionTraits object to dict for Pydantic
|
|
1546
1585
|
disposition_dict = (
|
|
1547
1586
|
profile["disposition"].model_dump()
|
|
@@ -1569,14 +1608,18 @@ def _register_routes(app: FastAPI):
|
|
|
1569
1608
|
operation_id="update_bank_disposition",
|
|
1570
1609
|
tags=["Banks"],
|
|
1571
1610
|
)
|
|
1572
|
-
async def api_update_bank_disposition(
|
|
1611
|
+
async def api_update_bank_disposition(
|
|
1612
|
+
bank_id: str, request: UpdateDispositionRequest, request_context: RequestContext = Depends(get_request_context)
|
|
1613
|
+
):
|
|
1573
1614
|
"""Update bank disposition traits."""
|
|
1574
1615
|
try:
|
|
1575
1616
|
# Update disposition
|
|
1576
|
-
await app.state.memory.update_bank_disposition(
|
|
1617
|
+
await app.state.memory.update_bank_disposition(
|
|
1618
|
+
bank_id, request.disposition.model_dump(), request_context=request_context
|
|
1619
|
+
)
|
|
1577
1620
|
|
|
1578
1621
|
# Get updated profile
|
|
1579
|
-
profile = await app.state.memory.get_bank_profile(bank_id)
|
|
1622
|
+
profile = await app.state.memory.get_bank_profile(bank_id, request_context=request_context)
|
|
1580
1623
|
disposition_dict = (
|
|
1581
1624
|
profile["disposition"].model_dump()
|
|
1582
1625
|
if hasattr(profile["disposition"], "model_dump")
|
|
@@ -1603,11 +1646,13 @@ def _register_routes(app: FastAPI):
|
|
|
1603
1646
|
operation_id="add_bank_background",
|
|
1604
1647
|
tags=["Banks"],
|
|
1605
1648
|
)
|
|
1606
|
-
async def api_add_bank_background(
|
|
1649
|
+
async def api_add_bank_background(
|
|
1650
|
+
bank_id: str, request: AddBackgroundRequest, request_context: RequestContext = Depends(get_request_context)
|
|
1651
|
+
):
|
|
1607
1652
|
"""Add or merge bank background information. Optionally infer disposition traits."""
|
|
1608
1653
|
try:
|
|
1609
1654
|
result = await app.state.memory.merge_bank_background(
|
|
1610
|
-
bank_id, request.content, update_disposition=request.update_disposition
|
|
1655
|
+
bank_id, request.content, update_disposition=request.update_disposition, request_context=request_context
|
|
1611
1656
|
)
|
|
1612
1657
|
|
|
1613
1658
|
response = BackgroundResponse(background=result["background"])
|
|
@@ -1630,51 +1675,31 @@ def _register_routes(app: FastAPI):
|
|
|
1630
1675
|
operation_id="create_or_update_bank",
|
|
1631
1676
|
tags=["Banks"],
|
|
1632
1677
|
)
|
|
1633
|
-
async def api_create_or_update_bank(
|
|
1678
|
+
async def api_create_or_update_bank(
|
|
1679
|
+
bank_id: str, request: CreateBankRequest, request_context: RequestContext = Depends(get_request_context)
|
|
1680
|
+
):
|
|
1634
1681
|
"""Create or update an agent with disposition and background."""
|
|
1635
1682
|
try:
|
|
1636
|
-
#
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
# Update name if provided
|
|
1640
|
-
if request.name is not None:
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
updated_at = NOW()
|
|
1648
|
-
WHERE bank_id = $1
|
|
1649
|
-
""",
|
|
1650
|
-
bank_id,
|
|
1651
|
-
request.name,
|
|
1652
|
-
)
|
|
1653
|
-
profile["name"] = request.name
|
|
1683
|
+
# Ensure bank exists by getting profile (auto-creates with defaults)
|
|
1684
|
+
await app.state.memory.get_bank_profile(bank_id, request_context=request_context)
|
|
1685
|
+
|
|
1686
|
+
# Update name and/or background if provided
|
|
1687
|
+
if request.name is not None or request.background is not None:
|
|
1688
|
+
await app.state.memory.update_bank(
|
|
1689
|
+
bank_id,
|
|
1690
|
+
name=request.name,
|
|
1691
|
+
background=request.background,
|
|
1692
|
+
request_context=request_context,
|
|
1693
|
+
)
|
|
1654
1694
|
|
|
1655
1695
|
# Update disposition if provided
|
|
1656
1696
|
if request.disposition is not None:
|
|
1657
|
-
await app.state.memory.update_bank_disposition(
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
# Update background if provided (replace, not merge)
|
|
1661
|
-
if request.background is not None:
|
|
1662
|
-
pool = await app.state.memory._get_pool()
|
|
1663
|
-
async with acquire_with_retry(pool) as conn:
|
|
1664
|
-
await conn.execute(
|
|
1665
|
-
"""
|
|
1666
|
-
UPDATE banks
|
|
1667
|
-
SET background = $2,
|
|
1668
|
-
updated_at = NOW()
|
|
1669
|
-
WHERE bank_id = $1
|
|
1670
|
-
""",
|
|
1671
|
-
bank_id,
|
|
1672
|
-
request.background,
|
|
1673
|
-
)
|
|
1674
|
-
profile["background"] = request.background
|
|
1697
|
+
await app.state.memory.update_bank_disposition(
|
|
1698
|
+
bank_id, request.disposition.model_dump(), request_context=request_context
|
|
1699
|
+
)
|
|
1675
1700
|
|
|
1676
1701
|
# Get final profile
|
|
1677
|
-
final_profile = await app.state.memory.get_bank_profile(bank_id)
|
|
1702
|
+
final_profile = await app.state.memory.get_bank_profile(bank_id, request_context=request_context)
|
|
1678
1703
|
disposition_dict = (
|
|
1679
1704
|
final_profile["disposition"].model_dump()
|
|
1680
1705
|
if hasattr(final_profile["disposition"], "model_dump")
|
|
@@ -1702,10 +1727,10 @@ def _register_routes(app: FastAPI):
|
|
|
1702
1727
|
operation_id="delete_bank",
|
|
1703
1728
|
tags=["Banks"],
|
|
1704
1729
|
)
|
|
1705
|
-
async def api_delete_bank(bank_id: str):
|
|
1730
|
+
async def api_delete_bank(bank_id: str, request_context: RequestContext = Depends(get_request_context)):
|
|
1706
1731
|
"""Delete an entire memory bank and all its data."""
|
|
1707
1732
|
try:
|
|
1708
|
-
result = await app.state.memory.delete_bank(bank_id)
|
|
1733
|
+
result = await app.state.memory.delete_bank(bank_id, request_context=request_context)
|
|
1709
1734
|
return DeleteResponse(
|
|
1710
1735
|
success=True,
|
|
1711
1736
|
message=f"Bank '{bank_id}' and all associated data deleted successfully",
|
|
@@ -1745,7 +1770,9 @@ def _register_routes(app: FastAPI):
|
|
|
1745
1770
|
operation_id="retain_memories",
|
|
1746
1771
|
tags=["Memory"],
|
|
1747
1772
|
)
|
|
1748
|
-
async def api_retain(
|
|
1773
|
+
async def api_retain(
|
|
1774
|
+
bank_id: str, request: RetainRequest, request_context: RequestContext = Depends(get_request_context)
|
|
1775
|
+
):
|
|
1749
1776
|
"""Retain memories with optional async processing."""
|
|
1750
1777
|
metrics = get_metrics_collector()
|
|
1751
1778
|
|
|
@@ -1766,47 +1793,42 @@ def _register_routes(app: FastAPI):
|
|
|
1766
1793
|
|
|
1767
1794
|
if request.async_:
|
|
1768
1795
|
# Async processing: queue task and return immediately
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
# Insert operation record into database
|
|
1772
|
-
pool = await app.state.memory._get_pool()
|
|
1773
|
-
async with acquire_with_retry(pool) as conn:
|
|
1774
|
-
await conn.execute(
|
|
1775
|
-
"""
|
|
1776
|
-
INSERT INTO async_operations (operation_id, bank_id, operation_type, result_metadata)
|
|
1777
|
-
VALUES ($1, $2, $3, $4)
|
|
1778
|
-
""",
|
|
1779
|
-
operation_id,
|
|
1780
|
-
bank_id,
|
|
1781
|
-
"retain",
|
|
1782
|
-
json.dumps({"items_count": len(contents)}),
|
|
1783
|
-
)
|
|
1784
|
-
|
|
1785
|
-
# Submit task to background queue
|
|
1786
|
-
await app.state.memory._task_backend.submit_task(
|
|
1796
|
+
result = await app.state.memory.submit_async_retain(bank_id, contents, request_context=request_context)
|
|
1797
|
+
return RetainResponse.model_validate(
|
|
1787
1798
|
{
|
|
1788
|
-
"
|
|
1789
|
-
"operation_id": str(operation_id),
|
|
1799
|
+
"success": True,
|
|
1790
1800
|
"bank_id": bank_id,
|
|
1791
|
-
"
|
|
1801
|
+
"items_count": result["items_count"],
|
|
1802
|
+
"async": True,
|
|
1792
1803
|
}
|
|
1793
1804
|
)
|
|
1794
|
-
|
|
1795
|
-
logging.info(
|
|
1796
|
-
f"Retain task queued for bank_id={bank_id}, {len(contents)} items, operation_id={operation_id}"
|
|
1797
|
-
)
|
|
1798
|
-
|
|
1799
|
-
return RetainResponse(success=True, bank_id=bank_id, items_count=len(contents), async_=True)
|
|
1800
1805
|
else:
|
|
1801
1806
|
# Synchronous processing: wait for completion (record metrics)
|
|
1802
1807
|
with metrics.record_operation("retain", bank_id=bank_id):
|
|
1803
|
-
result = await app.state.memory.retain_batch_async(
|
|
1808
|
+
result = await app.state.memory.retain_batch_async(
|
|
1809
|
+
bank_id=bank_id, contents=contents, request_context=request_context
|
|
1810
|
+
)
|
|
1804
1811
|
|
|
1805
|
-
return RetainResponse(
|
|
1812
|
+
return RetainResponse.model_validate(
|
|
1813
|
+
{"success": True, "bank_id": bank_id, "items_count": len(contents), "async": False}
|
|
1814
|
+
)
|
|
1806
1815
|
except Exception as e:
|
|
1807
1816
|
import traceback
|
|
1808
1817
|
|
|
1809
|
-
|
|
1818
|
+
# Create a summary of the input for debugging
|
|
1819
|
+
input_summary = []
|
|
1820
|
+
for i, item in enumerate(request.items):
|
|
1821
|
+
content_preview = item.content[:100] + "..." if len(item.content) > 100 else item.content
|
|
1822
|
+
input_summary.append(
|
|
1823
|
+
f" [{i}] content={content_preview!r}, context={item.context}, timestamp={item.timestamp}"
|
|
1824
|
+
)
|
|
1825
|
+
input_debug = "\n".join(input_summary)
|
|
1826
|
+
|
|
1827
|
+
error_detail = (
|
|
1828
|
+
f"{str(e)}\n\n"
|
|
1829
|
+
f"Input ({len(request.items)} items):\n{input_debug}\n\n"
|
|
1830
|
+
f"Traceback:\n{traceback.format_exc()}"
|
|
1831
|
+
)
|
|
1810
1832
|
logger.error(f"Error in /v1/default/banks/{bank_id}/memories (retain): {error_detail}")
|
|
1811
1833
|
raise HTTPException(status_code=500, detail=str(e))
|
|
1812
1834
|
|
|
@@ -1821,10 +1843,11 @@ def _register_routes(app: FastAPI):
|
|
|
1821
1843
|
async def api_clear_bank_memories(
|
|
1822
1844
|
bank_id: str,
|
|
1823
1845
|
type: str | None = Query(None, description="Optional fact type filter (world, experience, opinion)"),
|
|
1846
|
+
request_context: RequestContext = Depends(get_request_context),
|
|
1824
1847
|
):
|
|
1825
1848
|
"""Clear memories for a memory bank, optionally filtered by type."""
|
|
1826
1849
|
try:
|
|
1827
|
-
await app.state.memory.delete_bank(bank_id, fact_type=type)
|
|
1850
|
+
await app.state.memory.delete_bank(bank_id, fact_type=type, request_context=request_context)
|
|
1828
1851
|
|
|
1829
1852
|
return DeleteResponse(success=True)
|
|
1830
1853
|
except Exception as e:
|