hindsight-api 0.0.21__tar.gz → 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/PKG-INFO +2 -3
  2. hindsight_api-0.1.0/alembic/versions/e0a1b2c3d4e5_disposition_to_3_traits.py +62 -0
  3. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/api/__init__.py +2 -4
  4. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/api/http.py +28 -78
  5. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/api/mcp.py +2 -1
  6. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/cli.py +0 -1
  7. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/engine/cross_encoder.py +6 -1
  8. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/engine/embeddings.py +6 -1
  9. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/engine/entity_resolver.py +56 -29
  10. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/engine/llm_wrapper.py +97 -5
  11. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/engine/memory_engine.py +264 -139
  12. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/engine/response_models.py +15 -17
  13. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/engine/retain/bank_utils.py +23 -33
  14. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/engine/retain/entity_processing.py +5 -5
  15. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/engine/retain/fact_extraction.py +85 -23
  16. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/engine/retain/fact_storage.py +1 -1
  17. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/engine/retain/link_creation.py +12 -6
  18. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/engine/retain/link_utils.py +50 -56
  19. hindsight_api-0.1.0/hindsight_api/engine/retain/observation_regeneration.py +264 -0
  20. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/engine/retain/orchestrator.py +31 -44
  21. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/engine/retain/types.py +14 -0
  22. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/engine/search/retrieval.py +2 -2
  23. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/engine/search/think_utils.py +59 -30
  24. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/migrations.py +54 -32
  25. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/models.py +1 -2
  26. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/pg0.py +17 -36
  27. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/pyproject.toml +5 -7
  28. hindsight_api-0.1.0/tests/conftest.py +164 -0
  29. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/tests/test_agents_api.py +45 -60
  30. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/tests/test_chunking.py +1 -5
  31. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/tests/test_fact_extraction_quality.py +194 -312
  32. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/tests/test_fact_ordering.py +10 -13
  33. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/tests/test_http_api_integration.py +16 -49
  34. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/tests/test_mcp_api_integration.py +2 -3
  35. hindsight_api-0.1.0/tests/test_observations.py +497 -0
  36. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/tests/test_temporal_ranges.py +15 -26
  37. hindsight_api-0.0.21/tests/RETAIN_TEST_COVERAGE_PLAN.md +0 -302
  38. hindsight_api-0.0.21/tests/conftest.py +0 -130
  39. hindsight_api-0.0.21/tests/test_observations.py +0 -339
  40. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/.gitignore +0 -0
  41. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/README.md +0 -0
  42. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/alembic/README +0 -0
  43. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/alembic/env.py +0 -0
  44. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/alembic/script.py.mako +0 -0
  45. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/alembic/versions/5a366d414dce_initial_schema.py +0 -0
  46. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/alembic/versions/b7c4d8e9f1a2_add_chunks_table.py +0 -0
  47. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/alembic/versions/c8e5f2a3b4d1_add_retain_params_to_documents.py +0 -0
  48. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/alembic/versions/d9f6a3b4c5e2_rename_bank_to_interactions.py +0 -0
  49. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/alembic/versions/rename_personality_to_disposition.py +0 -0
  50. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/__init__.py +0 -0
  51. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/engine/__init__.py +0 -0
  52. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/engine/db_utils.py +0 -0
  53. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/engine/query_analyzer.py +0 -0
  54. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/engine/retain/__init__.py +0 -0
  55. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/engine/retain/chunk_storage.py +0 -0
  56. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/engine/retain/deduplication.py +0 -0
  57. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/engine/retain/embedding_processing.py +0 -0
  58. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/engine/retain/embedding_utils.py +0 -0
  59. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/engine/search/__init__.py +0 -0
  60. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/engine/search/fusion.py +0 -0
  61. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/engine/search/observation_utils.py +0 -0
  62. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/engine/search/reranking.py +0 -0
  63. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/engine/search/scoring.py +0 -0
  64. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/engine/search/temporal_extraction.py +0 -0
  65. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/engine/search/trace.py +0 -0
  66. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/engine/search/tracer.py +0 -0
  67. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/engine/search/types.py +0 -0
  68. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/engine/task_backend.py +0 -0
  69. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/engine/utils.py +0 -0
  70. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/metrics.py +0 -0
  71. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/web/__init__.py +0 -0
  72. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/hindsight_api/web/server.py +0 -0
  73. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/test_chunks_debug.py +0 -0
  74. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/test_mentioned_at.py +0 -0
  75. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/tests/__init__.py +0 -0
  76. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/tests/fixtures/README.md +0 -0
  77. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/tests/fixtures/locomo_conversation_sample.json +0 -0
  78. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/tests/test_batch_chunking.py +0 -0
  79. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/tests/test_document_tracking.py +0 -0
  80. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/tests/test_link_utils.py +0 -0
  81. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/tests/test_mcp_routing.py +0 -0
  82. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/tests/test_query_analyzer.py +0 -0
  83. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/tests/test_retain.py +0 -0
  84. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/tests/test_search_trace.py +0 -0
  85. {hindsight_api-0.0.21 → hindsight_api-0.1.0}/tests/test_think.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hindsight-api
3
- Version: 0.0.21
3
+ Version: 0.1.0
4
4
  Summary: Temporal + Semantic + Entity Memory System for AI agents using PostgreSQL
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: alembic>=1.17.1
@@ -23,7 +23,7 @@ Requires-Dist: pydantic>=2.0.0
23
23
  Requires-Dist: python-dateutil>=2.8.0
24
24
  Requires-Dist: python-dotenv>=1.0.0
25
25
  Requires-Dist: rich>=13.0.0
26
- Requires-Dist: sentence-transformers>=2.2.0
26
+ Requires-Dist: sentence-transformers>=3.0.0
27
27
  Requires-Dist: sqlalchemy>=2.0.44
28
28
  Requires-Dist: tiktoken>=0.12.0
29
29
  Requires-Dist: torch>=2.0.0
@@ -36,7 +36,6 @@ Requires-Dist: pytest-asyncio>=0.21.0; extra == 'test'
36
36
  Requires-Dist: pytest-timeout>=2.4.0; extra == 'test'
37
37
  Requires-Dist: pytest-xdist>=3.0.0; extra == 'test'
38
38
  Requires-Dist: pytest>=7.0.0; extra == 'test'
39
- Requires-Dist: testcontainers[postgres]>=4.0.0; extra == 'test'
40
39
  Description-Content-Type: text/markdown
41
40
 
42
41
  # Memory
@@ -0,0 +1,62 @@
1
+ """disposition_to_3_traits
2
+
3
+ Revision ID: e0a1b2c3d4e5
4
+ Revises: rename_personality
5
+ Create Date: 2024-12-08
6
+
7
+ Migrate disposition traits from Big Five (openness, conscientiousness, extraversion,
8
+ agreeableness, neuroticism, bias_strength with 0-1 float values) to the new 3-trait
9
+ system (skepticism, literalism, empathy with 1-5 integer values).
10
+ """
11
+ from typing import Sequence, Union
12
+
13
+ from alembic import op
14
+ import sqlalchemy as sa
15
+
16
+
17
+ # revision identifiers, used by Alembic.
18
+ revision: str = 'e0a1b2c3d4e5'
19
+ down_revision: Union[str, Sequence[str], None] = 'rename_personality'
20
+ branch_labels: Union[str, Sequence[str], None] = None
21
+ depends_on: Union[str, Sequence[str], None] = None
22
+
23
+
24
+ def upgrade() -> None:
25
+ """Convert Big Five disposition to 3-trait disposition."""
26
+ conn = op.get_bind()
27
+
28
+ # Update all existing banks to use the new disposition format
29
+ # Convert from old format to new format with reasonable mappings:
30
+ # - skepticism: derived from inverse of agreeableness (skeptical people are less agreeable)
31
+ # - literalism: derived from conscientiousness (detail-oriented people are more literal)
32
+ # - empathy: derived from agreeableness + inverse of neuroticism
33
+ # Default all to 3 (neutral) for simplicity
34
+ conn.execute(sa.text("""
35
+ UPDATE banks
36
+ SET disposition = '{"skepticism": 3, "literalism": 3, "empathy": 3}'::jsonb
37
+ WHERE disposition IS NOT NULL
38
+ """))
39
+
40
+ # Update the default for new banks
41
+ conn.execute(sa.text("""
42
+ ALTER TABLE banks
43
+ ALTER COLUMN disposition SET DEFAULT '{"skepticism": 3, "literalism": 3, "empathy": 3}'::jsonb
44
+ """))
45
+
46
+
47
+ def downgrade() -> None:
48
+ """Convert back to Big Five disposition."""
49
+ conn = op.get_bind()
50
+
51
+ # Revert to Big Five format with default values
52
+ conn.execute(sa.text("""
53
+ UPDATE banks
54
+ SET disposition = '{"openness": 0.5, "conscientiousness": 0.5, "extraversion": 0.5, "agreeableness": 0.5, "neuroticism": 0.5, "bias_strength": 0.5}'::jsonb
55
+ WHERE disposition IS NOT NULL
56
+ """))
57
+
58
+ # Update the default for new banks
59
+ conn.execute(sa.text("""
60
+ ALTER TABLE banks
61
+ ALTER COLUMN disposition SET DEFAULT '{"openness": 0.5, "conscientiousness": 0.5, "extraversion": 0.5, "agreeableness": 0.5, "neuroticism": 0.5, "bias_strength": 0.5}'::jsonb
62
+ """))
@@ -17,18 +17,17 @@ def create_app(
17
17
  http_api_enabled: bool = True,
18
18
  mcp_api_enabled: bool = False,
19
19
  mcp_mount_path: str = "/mcp",
20
- run_migrations: bool = True,
21
20
  initialize_memory: bool = True
22
21
  ) -> FastAPI:
23
22
  """
24
23
  Create and configure the unified Hindsight API application.
25
24
 
26
25
  Args:
27
- memory: MemoryEngine instance (already initialized with required parameters)
26
+ memory: MemoryEngine instance (already initialized with required parameters).
27
+ Migrations are controlled by the MemoryEngine's run_migrations parameter.
28
28
  http_api_enabled: Whether to enable HTTP REST API endpoints (default: True)
29
29
  mcp_api_enabled: Whether to enable MCP server (default: False)
30
30
  mcp_mount_path: Path to mount MCP server (default: /mcp)
31
- run_migrations: Whether to run database migrations on startup (default: True)
32
31
  initialize_memory: Whether to initialize memory system on startup (default: True)
33
32
 
34
33
  Returns:
@@ -50,7 +49,6 @@ def create_app(
50
49
  from .http import create_app as create_http_app
51
50
  app = create_http_app(
52
51
  memory=memory,
53
- run_migrations=run_migrations,
54
52
  initialize_memory=initialize_memory
55
53
  )
56
54
  logger.info("HTTP REST API enabled")
@@ -36,27 +36,13 @@ from pydantic import BaseModel, Field, ConfigDict
36
36
  from hindsight_api import MemoryEngine
37
37
  from hindsight_api.engine.memory_engine import Budget
38
38
  from hindsight_api.engine.db_utils import acquire_with_retry
39
+ from hindsight_api.engine.response_models import VALID_RECALL_FACT_TYPES
39
40
  from hindsight_api.metrics import get_metrics_collector, initialize_metrics, create_metrics_collector
40
41
 
41
42
 
42
43
  logger = logging.getLogger(__name__)
43
44
 
44
45
 
45
- class MetadataFilter(BaseModel):
46
- """Filter for metadata fields. Matches records where (key=value) OR (key not set) when match_unset=True."""
47
- model_config = ConfigDict(json_schema_extra={
48
- "example": {
49
- "key": "source",
50
- "value": "slack",
51
- "match_unset": True
52
- }
53
- })
54
-
55
- key: str = Field(description="Metadata key to filter on")
56
- value: Optional[str] = Field(default=None, description="Value to match. If None with match_unset=True, matches any record where key is not set.")
57
- match_unset: bool = Field(default=True, description="If True, also match records where this metadata key is not set")
58
-
59
-
60
46
  class EntityIncludeOptions(BaseModel):
61
47
  """Options for including entity observations in recall results."""
62
48
  max_tokens: int = Field(default=500, description="Maximum tokens for entity observations")
@@ -89,7 +75,6 @@ class RecallRequest(BaseModel):
89
75
  "max_tokens": 4096,
90
76
  "trace": True,
91
77
  "query_timestamp": "2023-05-30T23:40:00",
92
- "filters": [{"key": "source", "value": "slack", "match_unset": True}],
93
78
  "include": {
94
79
  "entities": {
95
80
  "max_tokens": 500
@@ -104,7 +89,6 @@ class RecallRequest(BaseModel):
104
89
  max_tokens: int = 4096
105
90
  trace: bool = False
106
91
  query_timestamp: Optional[str] = Field(default=None, description="ISO format date string (e.g., '2023-05-30T23:40:00')")
107
- filters: Optional[List[MetadataFilter]] = Field(default=None, description="Filter by metadata. Multiple filters are ANDed together.")
108
92
  include: IncludeOptions = Field(default_factory=IncludeOptions, description="Options for including additional data (entities are included by default)")
109
93
 
110
94
 
@@ -362,7 +346,6 @@ class ReflectRequest(BaseModel):
362
346
  "query": "What do you think about artificial intelligence?",
363
347
  "budget": "low",
364
348
  "context": "This is for a research paper on AI ethics",
365
- "filters": [{"key": "source", "value": "slack", "match_unset": True}],
366
349
  "include": {
367
350
  "facts": {}
368
351
  }
@@ -372,7 +355,6 @@ class ReflectRequest(BaseModel):
372
355
  query: str
373
356
  budget: Budget = Budget.LOW
374
357
  context: Optional[str] = None
375
- filters: Optional[List[MetadataFilter]] = Field(default=None, description="Filter by metadata. Multiple filters are ANDed together.")
376
358
  include: ReflectIncludeOptions = Field(default_factory=ReflectIncludeOptions, description="Options for including additional data (disabled by default)")
377
359
 
378
360
 
@@ -439,24 +421,18 @@ class BanksResponse(BaseModel):
439
421
 
440
422
 
441
423
  class DispositionTraits(BaseModel):
442
- """Disposition traits based on Big Five model."""
424
+ """Disposition traits that influence how memories are formed and interpreted."""
443
425
  model_config = ConfigDict(json_schema_extra={
444
426
  "example": {
445
- "openness": 0.8,
446
- "conscientiousness": 0.6,
447
- "extraversion": 0.5,
448
- "agreeableness": 0.7,
449
- "neuroticism": 0.3,
450
- "bias_strength": 0.7
427
+ "skepticism": 3,
428
+ "literalism": 3,
429
+ "empathy": 3
451
430
  }
452
431
  })
453
432
 
454
- openness: float = Field(ge=0.0, le=1.0, description="Openness to experience (0-1)")
455
- conscientiousness: float = Field(ge=0.0, le=1.0, description="Conscientiousness (0-1)")
456
- extraversion: float = Field(ge=0.0, le=1.0, description="Extraversion (0-1)")
457
- agreeableness: float = Field(ge=0.0, le=1.0, description="Agreeableness (0-1)")
458
- neuroticism: float = Field(ge=0.0, le=1.0, description="Neuroticism (0-1)")
459
- bias_strength: float = Field(ge=0.0, le=1.0, description="How strongly disposition influences opinions (0-1)")
433
+ skepticism: int = Field(ge=1, le=5, description="How skeptical vs trusting (1=trusting, 5=skeptical)")
434
+ literalism: int = Field(ge=1, le=5, description="How literally to interpret information (1=flexible, 5=literal)")
435
+ empathy: int = Field(ge=1, le=5, description="How much to consider emotional context (1=detached, 5=empathetic)")
460
436
 
461
437
 
462
438
  class BankProfileResponse(BaseModel):
@@ -466,12 +442,9 @@ class BankProfileResponse(BaseModel):
466
442
  "bank_id": "user123",
467
443
  "name": "Alice",
468
444
  "disposition": {
469
- "openness": 0.8,
470
- "conscientiousness": 0.6,
471
- "extraversion": 0.5,
472
- "agreeableness": 0.7,
473
- "neuroticism": 0.3,
474
- "bias_strength": 0.7
445
+ "skepticism": 3,
446
+ "literalism": 3,
447
+ "empathy": 3
475
448
  },
476
449
  "background": "I am a software engineer with 10 years of experience in startups"
477
450
  }
@@ -500,7 +473,7 @@ class AddBackgroundRequest(BaseModel):
500
473
  content: str = Field(description="New background information to add or merge")
501
474
  update_disposition: bool = Field(
502
475
  default=True,
503
- description="If true, infer Big Five disposition traits from the merged background (default: true)"
476
+ description="If true, infer disposition traits from the merged background (default: true)"
504
477
  )
505
478
 
506
479
 
@@ -510,12 +483,9 @@ class BackgroundResponse(BaseModel):
510
483
  "example": {
511
484
  "background": "I was born in Texas. I am a software engineer with 10 years of experience.",
512
485
  "disposition": {
513
- "openness": 0.7,
514
- "conscientiousness": 0.6,
515
- "extraversion": 0.5,
516
- "agreeableness": 0.8,
517
- "neuroticism": 0.4,
518
- "bias_strength": 0.6
486
+ "skepticism": 3,
487
+ "literalism": 3,
488
+ "empathy": 3
519
489
  }
520
490
  }
521
491
  })
@@ -543,12 +513,9 @@ class BankListResponse(BaseModel):
543
513
  "bank_id": "user123",
544
514
  "name": "Alice",
545
515
  "disposition": {
546
- "openness": 0.5,
547
- "conscientiousness": 0.5,
548
- "extraversion": 0.5,
549
- "agreeableness": 0.5,
550
- "neuroticism": 0.5,
551
- "bias_strength": 0.5
516
+ "skepticism": 3,
517
+ "literalism": 3,
518
+ "empathy": 3
552
519
  },
553
520
  "background": "I am a software engineer",
554
521
  "created_at": "2024-01-15T10:30:00Z",
@@ -567,12 +534,9 @@ class CreateBankRequest(BaseModel):
567
534
  "example": {
568
535
  "name": "Alice",
569
536
  "disposition": {
570
- "openness": 0.8,
571
- "conscientiousness": 0.6,
572
- "extraversion": 0.5,
573
- "agreeableness": 0.7,
574
- "neuroticism": 0.3,
575
- "bias_strength": 0.7
537
+ "skepticism": 3,
538
+ "literalism": 3,
539
+ "empathy": 3
576
540
  },
577
541
  "background": "I am a creative software engineer with 10 years of experience"
578
542
  }
@@ -715,13 +679,13 @@ class DeleteResponse(BaseModel):
715
679
  success: bool
716
680
 
717
681
 
718
- def create_app(memory: MemoryEngine, run_migrations: bool = True, initialize_memory: bool = True) -> FastAPI:
682
+ def create_app(memory: MemoryEngine, initialize_memory: bool = True) -> FastAPI:
719
683
  """
720
684
  Create and configure the FastAPI application.
721
685
 
722
686
  Args:
723
- memory: MemoryEngine instance (already initialized with required parameters)
724
- run_migrations: Whether to run database migrations on startup (default: True)
687
+ memory: MemoryEngine instance (already initialized with required parameters).
688
+ Migrations are controlled by the MemoryEngine's run_migrations parameter.
725
689
  initialize_memory: Whether to initialize memory system on startup (default: True)
726
690
 
727
691
  Returns:
@@ -752,16 +716,11 @@ def create_app(memory: MemoryEngine, run_migrations: bool = True, initialize_mem
752
716
  app.state.prometheus_reader = None
753
717
  # Metrics collector is already initialized as no-op by default
754
718
 
755
- # Startup: Initialize database and memory system
719
+ # Startup: Initialize database and memory system (migrations run inside initialize if enabled)
756
720
  if initialize_memory:
757
721
  await memory.initialize()
758
722
  logging.info("Memory system initialized")
759
723
 
760
- if run_migrations:
761
- from hindsight_api.migrations import run_migrations as do_migrations
762
- do_migrations(memory.db_url)
763
- logging.info("Database migrations applied")
764
-
765
724
 
766
725
 
767
726
  yield
@@ -913,17 +872,8 @@ def _register_routes(app: FastAPI):
913
872
  metrics = get_metrics_collector()
914
873
 
915
874
  try:
916
- # Validate types
917
- valid_fact_types = ["world", "experience", "opinion"]
918
-
919
875
  # Default to world, experience, opinion if not specified (exclude observation by default)
920
- fact_types = request.types if request.types else ["world", "experience", "opinion"]
921
- for ft in fact_types:
922
- if ft not in valid_fact_types:
923
- raise HTTPException(
924
- status_code=400,
925
- detail=f"Invalid type '{ft}'. Must be one of: {', '.join(valid_fact_types)}"
926
- )
876
+ fact_types = request.types if request.types else list(VALID_RECALL_FACT_TYPES)
927
877
 
928
878
  # Parse query_timestamp if provided
929
879
  question_date = None
@@ -1605,7 +1555,7 @@ This operation cannot be undone.
1605
1555
  "/v1/default/banks/{bank_id}/profile",
1606
1556
  response_model=BankProfileResponse,
1607
1557
  summary="Update memory bank disposition",
1608
- description="Update bank's Big Five disposition traits and bias strength",
1558
+ description="Update bank's disposition traits (skepticism, literalism, empathy)",
1609
1559
  operation_id="update_bank_disposition"
1610
1560
  )
1611
1561
  async def api_update_bank_disposition(bank_id: str,
@@ -1852,7 +1802,7 @@ This operation cannot be undone.
1852
1802
  "/v1/default/banks/{bank_id}/memories",
1853
1803
  response_model=DeleteResponse,
1854
1804
  summary="Clear memory bank memories",
1855
- 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 (personality and background) will be preserved.",
1805
+ 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.",
1856
1806
  operation_id="clear_bank_memories"
1857
1807
  )
1858
1808
  async def api_clear_bank_memories(bank_id: str,
@@ -8,6 +8,7 @@ from typing import Optional
8
8
 
9
9
  from fastmcp import FastMCP
10
10
  from hindsight_api import MemoryEngine
11
+ from hindsight_api.engine.response_models import VALID_RECALL_FACT_TYPES
11
12
 
12
13
  # Configure logging from HINDSIGHT_API_LOG_LEVEL environment variable
13
14
  _log_level_str = os.environ.get("HINDSIGHT_API_LOG_LEVEL", "info").lower()
@@ -90,7 +91,7 @@ def create_mcp_server(memory: MemoryEngine) -> FastMCP:
90
91
  search_result = await memory.recall_async(
91
92
  bank_id=bank_id,
92
93
  query=query,
93
- fact_type=["world", "experience", "opinion"],
94
+ fact_type=list(VALID_RECALL_FACT_TYPES),
94
95
  budget=Budget.LOW
95
96
  )
96
97
 
@@ -102,7 +102,6 @@ def main():
102
102
  http_api_enabled=True,
103
103
  mcp_api_enabled=True,
104
104
  mcp_mount_path="/mcp",
105
- run_migrations=True,
106
105
  initialize_memory=True,
107
106
  )
108
107
 
@@ -78,7 +78,12 @@ class SentenceTransformersCrossEncoder(CrossEncoderModel):
78
78
  )
79
79
 
80
80
  logger.info(f"Loading cross-encoder model: {self.model_name}...")
81
- self._model = CrossEncoder(self.model_name)
81
+ # Disable lazy loading (meta tensors) which causes issues with newer transformers/accelerate
82
+ # Setting low_cpu_mem_usage=False and device_map=None ensures tensors are fully materialized
83
+ self._model = CrossEncoder(
84
+ self.model_name,
85
+ model_kwargs={"low_cpu_mem_usage": False, "device_map": None},
86
+ )
82
87
  logger.info("Cross-encoder model loaded")
83
88
 
84
89
  def predict(self, pairs: List[Tuple[str, str]]) -> List[float]:
@@ -84,7 +84,12 @@ class SentenceTransformersEmbeddings(Embeddings):
84
84
  )
85
85
 
86
86
  logger.info(f"Loading embedding model: {self.model_name}...")
87
- self._model = SentenceTransformer(self.model_name)
87
+ # Disable lazy loading (meta tensors) which causes issues with newer transformers/accelerate
88
+ # Setting low_cpu_mem_usage=False and device_map=None ensures tensors are fully materialized
89
+ self._model = SentenceTransformer(
90
+ self.model_name,
91
+ model_kwargs={"low_cpu_mem_usage": False, "device_map": None},
92
+ )
88
93
 
89
94
  # Validate dimension matches database schema
90
95
  model_dim = self._model.get_sentence_embedding_dimension()
@@ -126,18 +126,20 @@ class EntityResolver:
126
126
 
127
127
  # Resolve each entity using pre-fetched candidates
128
128
  entity_ids = [None] * len(entities_data)
129
- entities_to_update = [] # (entity_id, unit_event_date)
130
- entities_to_create = [] # (idx, entity_data)
129
+ entities_to_update = [] # (entity_id, event_date)
130
+ entities_to_create = [] # (idx, entity_data, event_date)
131
131
 
132
132
  for idx, entity_data in enumerate(entities_data):
133
133
  entity_text = entity_data['text']
134
134
  nearby_entities = entity_data.get('nearby_entities', [])
135
+ # Use per-entity date if available, otherwise fall back to batch-level date
136
+ entity_event_date = entity_data.get('event_date', unit_event_date)
135
137
 
136
138
  candidates = all_candidates.get(entity_text, [])
137
139
 
138
140
  if not candidates:
139
141
  # Will create new entity
140
- entities_to_create.append((idx, entity_data))
142
+ entities_to_create.append((idx, entity_data, entity_event_date))
141
143
  continue
142
144
 
143
145
  # Score candidates
@@ -165,9 +167,9 @@ class EntityResolver:
165
167
  score += co_entity_score * 0.3
166
168
 
167
169
  # 3. Temporal proximity (0-0.2)
168
- if last_seen:
170
+ if last_seen and entity_event_date:
169
171
  # Normalize timezone awareness for comparison
170
- event_date_utc = unit_event_date if unit_event_date.tzinfo else unit_event_date.replace(tzinfo=timezone.utc)
172
+ event_date_utc = entity_event_date if entity_event_date.tzinfo else entity_event_date.replace(tzinfo=timezone.utc)
171
173
  last_seen_utc = last_seen if last_seen.tzinfo else last_seen.replace(tzinfo=timezone.utc)
172
174
  days_diff = abs((event_date_utc - last_seen_utc).total_seconds() / 86400)
173
175
  if days_diff < 7:
@@ -183,9 +185,9 @@ class EntityResolver:
183
185
 
184
186
  if best_score > threshold:
185
187
  entity_ids[idx] = best_candidate
186
- entities_to_update.append((best_candidate, unit_event_date))
188
+ entities_to_update.append((best_candidate, entity_event_date))
187
189
  else:
188
- entities_to_create.append((idx, entity_data))
190
+ entities_to_create.append((idx, entity_data, entity_event_date))
189
191
 
190
192
  # Batch update existing entities
191
193
  if entities_to_update:
@@ -199,29 +201,54 @@ class EntityResolver:
199
201
  entities_to_update
200
202
  )
201
203
 
202
- # Create new entities using INSERT ... ON CONFLICT to handle race conditions
203
- # This ensures that if two concurrent transactions try to create the same entity,
204
- # only one succeeds and the other gets the existing ID
204
+ # Batch create new entities using COPY + INSERT for maximum speed
205
+ # This handles duplicates via ON CONFLICT and returns all IDs
205
206
  if entities_to_create:
206
- for idx, entity_data in entities_to_create:
207
- # Use INSERT ... ON CONFLICT to atomically get-or-create
208
- # The unique index is on (bank_id, LOWER(canonical_name))
209
- row = await conn.fetchrow(
210
- """
211
- INSERT INTO entities (bank_id, canonical_name, first_seen, last_seen, mention_count)
212
- VALUES ($1, $2, $3, $4, 1)
213
- ON CONFLICT (bank_id, LOWER(canonical_name))
214
- DO UPDATE SET
215
- mention_count = entities.mention_count + 1,
216
- last_seen = EXCLUDED.last_seen
217
- RETURNING id
218
- """,
219
- bank_id,
220
- entity_data['text'],
221
- unit_event_date,
222
- unit_event_date
223
- )
224
- entity_ids[idx] = row['id']
207
+ # Group entities by canonical name (lowercase) to handle duplicates within batch
208
+ # For duplicates, we only insert once and reuse the ID
209
+ unique_entities = {} # lowercase_name -> (entity_data, event_date, [indices])
210
+ for idx, entity_data, event_date in entities_to_create:
211
+ name_lower = entity_data['text'].lower()
212
+ if name_lower not in unique_entities:
213
+ unique_entities[name_lower] = (entity_data, event_date, [idx])
214
+ else:
215
+ # Same entity appears multiple times - add index to list
216
+ unique_entities[name_lower][2].append(idx)
217
+
218
+ # Batch insert unique entities and get their IDs
219
+ # Use a single query with unnest for speed
220
+ entity_names = []
221
+ entity_dates = []
222
+ indices_map = [] # Maps result index -> list of original indices
223
+
224
+ for name_lower, (entity_data, event_date, indices) in unique_entities.items():
225
+ entity_names.append(entity_data['text'])
226
+ entity_dates.append(event_date)
227
+ indices_map.append(indices)
228
+
229
+ # Batch INSERT ... ON CONFLICT with RETURNING
230
+ # This is much faster than individual inserts
231
+ rows = await conn.fetch(
232
+ """
233
+ INSERT INTO entities (bank_id, canonical_name, first_seen, last_seen, mention_count)
234
+ SELECT $1, name, event_date, event_date, 1
235
+ FROM unnest($2::text[], $3::timestamptz[]) AS t(name, event_date)
236
+ ON CONFLICT (bank_id, LOWER(canonical_name))
237
+ DO UPDATE SET
238
+ mention_count = entities.mention_count + 1,
239
+ last_seen = EXCLUDED.last_seen
240
+ RETURNING id
241
+ """,
242
+ bank_id,
243
+ entity_names,
244
+ entity_dates
245
+ )
246
+
247
+ # Map returned IDs back to original indices
248
+ for result_idx, row in enumerate(rows):
249
+ entity_id = row['id']
250
+ for original_idx in indices_map[result_idx]:
251
+ entity_ids[original_idx] = entity_id
225
252
 
226
253
  return entity_ids
227
254