hindsight-api 0.1.16__py3-none-any.whl → 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,6 +5,7 @@ Provides both HTTP REST API and MCP (Model Context Protocol) server.
5
5
  """
6
6
 
7
7
  import logging
8
+ from contextlib import asynccontextmanager
8
9
  from typing import Optional
9
10
 
10
11
  from fastapi import FastAPI
@@ -45,6 +46,18 @@ def create_app(
45
46
  # Both HTTP and MCP
46
47
  app = create_app(memory, mcp_api_enabled=True)
47
48
  """
49
+ mcp_app = None
50
+
51
+ # Create MCP app first if enabled (we need its lifespan for chaining)
52
+ if mcp_api_enabled:
53
+ try:
54
+ from .mcp import create_mcp_app
55
+
56
+ mcp_app = create_mcp_app(memory=memory)
57
+ except ImportError as e:
58
+ logger.error(f"MCP server requested but dependencies not available: {e}")
59
+ logger.error("Install with: pip install hindsight-api[mcp]")
60
+ raise
48
61
 
49
62
  # Import and create HTTP API if enabled
50
63
  if http_api_enabled:
@@ -57,20 +70,31 @@ def create_app(
57
70
  app = FastAPI(title="Hindsight API", version="0.0.7")
58
71
  logger.info("HTTP REST API disabled")
59
72
 
60
- # Mount MCP server if enabled
61
- if mcp_api_enabled:
62
- try:
63
- from .mcp import create_mcp_app
64
-
65
- # Create MCP app with dynamic bank_id support
66
- # Supports: /mcp/{bank_id}/sse (bank-specific SSE endpoint)
67
- mcp_app = create_mcp_app(memory=memory)
68
- app.mount(mcp_mount_path, mcp_app)
69
- logger.info(f"MCP server enabled at {mcp_mount_path}/{{bank_id}}/sse")
70
- except ImportError as e:
71
- logger.error(f"MCP server requested but dependencies not available: {e}")
72
- logger.error("Install with: pip install hindsight-api[mcp]")
73
- raise
73
+ # Mount MCP server and chain its lifespan if enabled
74
+ if mcp_app is not None:
75
+ # Get the MCP app's underlying Starlette app for lifespan access
76
+ mcp_starlette_app = mcp_app.mcp_app
77
+
78
+ # Store the original lifespan
79
+ original_lifespan = app.router.lifespan_context
80
+
81
+ @asynccontextmanager
82
+ async def chained_lifespan(app_instance: FastAPI):
83
+ """Chain the MCP lifespan with the main app lifespan."""
84
+ # Start MCP lifespan first
85
+ async with mcp_starlette_app.router.lifespan_context(mcp_starlette_app):
86
+ logger.info("MCP lifespan started")
87
+ # Then start the original app lifespan
88
+ async with original_lifespan(app_instance):
89
+ yield
90
+ logger.info("MCP lifespan stopped")
91
+
92
+ # Replace the app's lifespan with the chained version
93
+ app.router.lifespan_context = chained_lifespan
94
+
95
+ # Mount the MCP middleware
96
+ app.mount(mcp_mount_path, mcp_app)
97
+ logger.info(f"MCP server enabled at {mcp_mount_path}/")
74
98
 
75
99
  return app
76
100
 
hindsight_api/api/http.py CHANGED
@@ -14,6 +14,8 @@ from typing import Any
14
14
 
15
15
  from fastapi import Depends, FastAPI, Header, HTTPException, Query
16
16
 
17
+ from hindsight_api.extensions import AuthenticationError
18
+
17
19
 
18
20
  def _parse_metadata(metadata: Any) -> dict[str, Any]:
19
21
  """Parse metadata that may be a dict, JSON string, or None."""
@@ -35,7 +37,7 @@ from hindsight_api import MemoryEngine
35
37
  from hindsight_api.engine.db_utils import acquire_with_retry
36
38
  from hindsight_api.engine.memory_engine import Budget, fq_table
37
39
  from hindsight_api.engine.response_models import VALID_RECALL_FACT_TYPES
38
- from hindsight_api.extensions import HttpExtension, load_extension
40
+ from hindsight_api.extensions import HttpExtension, OperationValidationError, load_extension
39
41
  from hindsight_api.metrics import create_metrics_collector, get_metrics_collector, initialize_metrics
40
42
  from hindsight_api.models import RequestContext
41
43
 
@@ -279,6 +281,13 @@ class RecallResponse(BaseModel):
279
281
  chunks: dict[str, ChunkData] | None = Field(default=None, description="Chunks for facts, keyed by chunk_id")
280
282
 
281
283
 
284
+ class EntityInput(BaseModel):
285
+ """Entity to associate with retained content."""
286
+
287
+ text: str = Field(description="The entity name/text")
288
+ type: str | None = Field(default=None, description="Optional entity type (e.g., 'PERSON', 'ORG', 'CONCEPT')")
289
+
290
+
282
291
  class MemoryItem(BaseModel):
283
292
  """Single memory item for retain."""
284
293
 
@@ -290,6 +299,7 @@ class MemoryItem(BaseModel):
290
299
  "context": "team meeting",
291
300
  "metadata": {"source": "slack", "channel": "engineering"},
292
301
  "document_id": "meeting_notes_2024_01_15",
302
+ "entities": [{"text": "Alice"}, {"text": "ML model", "type": "CONCEPT"}],
293
303
  }
294
304
  },
295
305
  )
@@ -299,6 +309,10 @@ class MemoryItem(BaseModel):
299
309
  context: str | None = None
300
310
  metadata: dict[str, str] | None = None
301
311
  document_id: str | None = Field(default=None, description="Optional document ID for this memory item.")
312
+ entities: list[EntityInput] | None = Field(
313
+ default=None,
314
+ description="Optional entities to combine with auto-extracted entities.",
315
+ )
302
316
 
303
317
  @field_validator("timestamp", mode="before")
304
318
  @classmethod
@@ -385,7 +399,16 @@ class ReflectRequest(BaseModel):
385
399
  "query": "What do you think about artificial intelligence?",
386
400
  "budget": "low",
387
401
  "context": "This is for a research paper on AI ethics",
402
+ "max_tokens": 4096,
388
403
  "include": {"facts": {}},
404
+ "response_schema": {
405
+ "type": "object",
406
+ "properties": {
407
+ "summary": {"type": "string"},
408
+ "key_points": {"type": "array", "items": {"type": "string"}},
409
+ },
410
+ "required": ["summary", "key_points"],
411
+ },
389
412
  }
390
413
  }
391
414
  )
@@ -393,9 +416,14 @@ class ReflectRequest(BaseModel):
393
416
  query: str
394
417
  budget: Budget = Budget.LOW
395
418
  context: str | None = None
419
+ max_tokens: int = Field(default=4096, description="Maximum tokens for the response")
396
420
  include: ReflectIncludeOptions = Field(
397
421
  default_factory=ReflectIncludeOptions, description="Options for including additional data (disabled by default)"
398
422
  )
423
+ response_schema: dict | None = Field(
424
+ default=None,
425
+ description="Optional JSON Schema for structured output. When provided, the response will include a 'structured_output' field with the LLM response parsed according to this schema.",
426
+ )
399
427
 
400
428
 
401
429
  class OpinionItem(BaseModel):
@@ -440,12 +468,20 @@ class ReflectResponse(BaseModel):
440
468
  {"id": "123", "text": "AI is used in healthcare", "type": "world"},
441
469
  {"id": "456", "text": "I discussed AI applications last week", "type": "experience"},
442
470
  ],
471
+ "structured_output": {
472
+ "summary": "AI is transformative",
473
+ "key_points": ["Used in healthcare", "Discussed recently"],
474
+ },
443
475
  }
444
476
  }
445
477
  )
446
478
 
447
479
  text: str
448
480
  based_on: list[ReflectFact] = [] # Facts used to generate the response
481
+ structured_output: dict | None = Field(
482
+ default=None,
483
+ description="Structured output parsed according to the request's response_schema. Only present when response_schema was provided in the request.",
484
+ )
449
485
 
450
486
 
451
487
  class BanksResponse(BaseModel):
@@ -967,6 +1003,16 @@ def _register_routes(app: FastAPI):
967
1003
  api_key = authorization.strip()
968
1004
  return RequestContext(api_key=api_key)
969
1005
 
1006
+ # Global exception handler for authentication errors
1007
+ @app.exception_handler(AuthenticationError)
1008
+ async def authentication_error_handler(request, exc: AuthenticationError):
1009
+ from fastapi.responses import JSONResponse
1010
+
1011
+ return JSONResponse(
1012
+ status_code=401,
1013
+ content={"detail": str(exc)},
1014
+ )
1015
+
970
1016
  @app.get(
971
1017
  "/health",
972
1018
  summary="Health check endpoint",
@@ -1014,6 +1060,8 @@ def _register_routes(app: FastAPI):
1014
1060
  try:
1015
1061
  data = await app.state.memory.get_graph_data(bank_id, type, request_context=request_context)
1016
1062
  return data
1063
+ except (AuthenticationError, HTTPException):
1064
+ raise
1017
1065
  except Exception as e:
1018
1066
  import traceback
1019
1067
 
@@ -1060,6 +1108,8 @@ def _register_routes(app: FastAPI):
1060
1108
  request_context=request_context,
1061
1109
  )
1062
1110
  return data
1111
+ except (AuthenticationError, HTTPException):
1112
+ raise
1063
1113
  except Exception as e:
1064
1114
  import traceback
1065
1115
 
@@ -1176,6 +1226,10 @@ def _register_routes(app: FastAPI):
1176
1226
  )
1177
1227
  except HTTPException:
1178
1228
  raise
1229
+ except OperationValidationError as e:
1230
+ raise HTTPException(status_code=e.status_code, detail=e.reason)
1231
+ except (AuthenticationError, HTTPException):
1232
+ raise
1179
1233
  except Exception as e:
1180
1234
  import traceback
1181
1235
 
@@ -1211,6 +1265,8 @@ def _register_routes(app: FastAPI):
1211
1265
  query=request.query,
1212
1266
  budget=request.budget,
1213
1267
  context=request.context,
1268
+ max_tokens=request.max_tokens,
1269
+ response_schema=request.response_schema,
1214
1270
  request_context=request_context,
1215
1271
  )
1216
1272
 
@@ -1233,8 +1289,13 @@ def _register_routes(app: FastAPI):
1233
1289
  return ReflectResponse(
1234
1290
  text=core_result.text,
1235
1291
  based_on=based_on_facts,
1292
+ structured_output=core_result.structured_output,
1236
1293
  )
1237
1294
 
1295
+ except OperationValidationError as e:
1296
+ raise HTTPException(status_code=e.status_code, detail=e.reason)
1297
+ except (AuthenticationError, HTTPException):
1298
+ raise
1238
1299
  except Exception as e:
1239
1300
  import traceback
1240
1301
 
@@ -1255,6 +1316,8 @@ def _register_routes(app: FastAPI):
1255
1316
  try:
1256
1317
  banks = await app.state.memory.list_banks(request_context=request_context)
1257
1318
  return BankListResponse(banks=banks)
1319
+ except (AuthenticationError, HTTPException):
1320
+ raise
1258
1321
  except Exception as e:
1259
1322
  import traceback
1260
1323
 
@@ -1378,6 +1441,8 @@ def _register_routes(app: FastAPI):
1378
1441
  failed_operations=failed_operations,
1379
1442
  )
1380
1443
 
1444
+ except (AuthenticationError, HTTPException):
1445
+ raise
1381
1446
  except Exception as e:
1382
1447
  import traceback
1383
1448
 
@@ -1402,6 +1467,8 @@ def _register_routes(app: FastAPI):
1402
1467
  try:
1403
1468
  entities = await app.state.memory.list_entities(bank_id, limit=limit, request_context=request_context)
1404
1469
  return EntityListResponse(items=[EntityListItem(**e) for e in entities])
1470
+ except (AuthenticationError, HTTPException):
1471
+ raise
1405
1472
  except Exception as e:
1406
1473
  import traceback
1407
1474
 
@@ -1439,7 +1506,7 @@ def _register_routes(app: FastAPI):
1439
1506
  for obs in entity["observations"]
1440
1507
  ],
1441
1508
  )
1442
- except HTTPException:
1509
+ except (AuthenticationError, HTTPException):
1443
1510
  raise
1444
1511
  except Exception as e:
1445
1512
  import traceback
@@ -1492,7 +1559,7 @@ def _register_routes(app: FastAPI):
1492
1559
  for obs in entity["observations"]
1493
1560
  ],
1494
1561
  )
1495
- except HTTPException:
1562
+ except (AuthenticationError, HTTPException):
1496
1563
  raise
1497
1564
  except Exception as e:
1498
1565
  import traceback
@@ -1530,6 +1597,8 @@ def _register_routes(app: FastAPI):
1530
1597
  bank_id=bank_id, search_query=q, limit=limit, offset=offset, request_context=request_context
1531
1598
  )
1532
1599
  return data
1600
+ except (AuthenticationError, HTTPException):
1601
+ raise
1533
1602
  except Exception as e:
1534
1603
  import traceback
1535
1604
 
@@ -1538,7 +1607,7 @@ def _register_routes(app: FastAPI):
1538
1607
  raise HTTPException(status_code=500, detail=str(e))
1539
1608
 
1540
1609
  @app.get(
1541
- "/v1/default/banks/{bank_id}/documents/{document_id}",
1610
+ "/v1/default/banks/{bank_id}/documents/{document_id:path}",
1542
1611
  response_model=DocumentResponse,
1543
1612
  summary="Get document details",
1544
1613
  description="Get a specific document including its original text",
@@ -1560,7 +1629,7 @@ def _register_routes(app: FastAPI):
1560
1629
  if not document:
1561
1630
  raise HTTPException(status_code=404, detail="Document not found")
1562
1631
  return document
1563
- except HTTPException:
1632
+ except (AuthenticationError, HTTPException):
1564
1633
  raise
1565
1634
  except Exception as e:
1566
1635
  import traceback
@@ -1570,7 +1639,7 @@ def _register_routes(app: FastAPI):
1570
1639
  raise HTTPException(status_code=500, detail=str(e))
1571
1640
 
1572
1641
  @app.get(
1573
- "/v1/default/chunks/{chunk_id}",
1642
+ "/v1/default/chunks/{chunk_id:path}",
1574
1643
  response_model=ChunkResponse,
1575
1644
  summary="Get chunk details",
1576
1645
  description="Get a specific chunk by its ID",
@@ -1589,7 +1658,7 @@ def _register_routes(app: FastAPI):
1589
1658
  if not chunk:
1590
1659
  raise HTTPException(status_code=404, detail="Chunk not found")
1591
1660
  return chunk
1592
- except HTTPException:
1661
+ except (AuthenticationError, HTTPException):
1593
1662
  raise
1594
1663
  except Exception as e:
1595
1664
  import traceback
@@ -1599,7 +1668,7 @@ def _register_routes(app: FastAPI):
1599
1668
  raise HTTPException(status_code=500, detail=str(e))
1600
1669
 
1601
1670
  @app.delete(
1602
- "/v1/default/banks/{bank_id}/documents/{document_id}",
1671
+ "/v1/default/banks/{bank_id}/documents/{document_id:path}",
1603
1672
  response_model=DeleteDocumentResponse,
1604
1673
  summary="Delete a document",
1605
1674
  description="Delete a document and all its associated memory units and links.\n\n"
@@ -1633,7 +1702,7 @@ def _register_routes(app: FastAPI):
1633
1702
  document_id=document_id,
1634
1703
  memory_units_deleted=result["memory_units_deleted"],
1635
1704
  )
1636
- except HTTPException:
1705
+ except (AuthenticationError, HTTPException):
1637
1706
  raise
1638
1707
  except Exception as e:
1639
1708
  import traceback
@@ -1658,6 +1727,8 @@ def _register_routes(app: FastAPI):
1658
1727
  bank_id=bank_id,
1659
1728
  operations=[OperationResponse(**op) for op in operations],
1660
1729
  )
1730
+ except (AuthenticationError, HTTPException):
1731
+ raise
1661
1732
  except Exception as e:
1662
1733
  import traceback
1663
1734
 
@@ -1688,6 +1759,8 @@ def _register_routes(app: FastAPI):
1688
1759
  return CancelOperationResponse(**result)
1689
1760
  except ValueError as e:
1690
1761
  raise HTTPException(status_code=404, detail=str(e))
1762
+ except (AuthenticationError, HTTPException):
1763
+ raise
1691
1764
  except Exception as e:
1692
1765
  import traceback
1693
1766
 
@@ -1719,6 +1792,8 @@ def _register_routes(app: FastAPI):
1719
1792
  disposition=DispositionTraits(**disposition_dict),
1720
1793
  background=profile["background"],
1721
1794
  )
1795
+ except (AuthenticationError, HTTPException):
1796
+ raise
1722
1797
  except Exception as e:
1723
1798
  import traceback
1724
1799
 
@@ -1757,6 +1832,8 @@ def _register_routes(app: FastAPI):
1757
1832
  disposition=DispositionTraits(**disposition_dict),
1758
1833
  background=profile["background"],
1759
1834
  )
1835
+ except (AuthenticationError, HTTPException):
1836
+ raise
1760
1837
  except Exception as e:
1761
1838
  import traceback
1762
1839
 
@@ -1786,6 +1863,8 @@ def _register_routes(app: FastAPI):
1786
1863
  response.disposition = DispositionTraits(**result["disposition"])
1787
1864
 
1788
1865
  return response
1866
+ except (AuthenticationError, HTTPException):
1867
+ raise
1789
1868
  except Exception as e:
1790
1869
  import traceback
1791
1870
 
@@ -1837,6 +1916,8 @@ def _register_routes(app: FastAPI):
1837
1916
  disposition=DispositionTraits(**disposition_dict),
1838
1917
  background=final_profile["background"],
1839
1918
  )
1919
+ except (AuthenticationError, HTTPException):
1920
+ raise
1840
1921
  except Exception as e:
1841
1922
  import traceback
1842
1923
 
@@ -1864,6 +1945,8 @@ def _register_routes(app: FastAPI):
1864
1945
  + result.get("entities_deleted", 0)
1865
1946
  + result.get("documents_deleted", 0),
1866
1947
  )
1948
+ except (AuthenticationError, HTTPException):
1949
+ raise
1867
1950
  except Exception as e:
1868
1951
  import traceback
1869
1952
 
@@ -1915,6 +1998,8 @@ def _register_routes(app: FastAPI):
1915
1998
  content_dict["metadata"] = item.metadata
1916
1999
  if item.document_id:
1917
2000
  content_dict["document_id"] = item.document_id
2001
+ if item.entities:
2002
+ content_dict["entities"] = [{"text": e.text, "type": e.type or "CONCEPT"} for e in item.entities]
1918
2003
  contents.append(content_dict)
1919
2004
 
1920
2005
  if request.async_:
@@ -1938,6 +2023,10 @@ def _register_routes(app: FastAPI):
1938
2023
  return RetainResponse.model_validate(
1939
2024
  {"success": True, "bank_id": bank_id, "items_count": len(contents), "async": False}
1940
2025
  )
2026
+ except OperationValidationError as e:
2027
+ raise HTTPException(status_code=e.status_code, detail=e.reason)
2028
+ except (AuthenticationError, HTTPException):
2029
+ raise
1941
2030
  except Exception as e:
1942
2031
  import traceback
1943
2032
 
@@ -1976,6 +2065,8 @@ def _register_routes(app: FastAPI):
1976
2065
  await app.state.memory.delete_bank(bank_id, fact_type=type, request_context=request_context)
1977
2066
 
1978
2067
  return DeleteResponse(success=True)
2068
+ except (AuthenticationError, HTTPException):
2069
+ raise
1979
2070
  except Exception as e:
1980
2071
  import traceback
1981
2072