hindsight-api 0.4.6__py3-none-any.whl → 0.4.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. hindsight_api/__init__.py +1 -1
  2. hindsight_api/alembic/versions/5a366d414dce_initial_schema.py +16 -2
  3. hindsight_api/api/http.py +83 -1
  4. hindsight_api/banner.py +3 -0
  5. hindsight_api/config.py +44 -6
  6. hindsight_api/daemon.py +18 -112
  7. hindsight_api/engine/llm_interface.py +146 -0
  8. hindsight_api/engine/llm_wrapper.py +304 -1327
  9. hindsight_api/engine/memory_engine.py +125 -41
  10. hindsight_api/engine/providers/__init__.py +14 -0
  11. hindsight_api/engine/providers/anthropic_llm.py +434 -0
  12. hindsight_api/engine/providers/claude_code_llm.py +352 -0
  13. hindsight_api/engine/providers/codex_llm.py +527 -0
  14. hindsight_api/engine/providers/gemini_llm.py +502 -0
  15. hindsight_api/engine/providers/mock_llm.py +234 -0
  16. hindsight_api/engine/providers/openai_compatible_llm.py +745 -0
  17. hindsight_api/engine/retain/fact_extraction.py +13 -9
  18. hindsight_api/engine/retain/fact_storage.py +5 -3
  19. hindsight_api/extensions/__init__.py +10 -0
  20. hindsight_api/extensions/builtin/tenant.py +36 -0
  21. hindsight_api/extensions/operation_validator.py +129 -0
  22. hindsight_api/main.py +6 -21
  23. hindsight_api/migrations.py +75 -0
  24. hindsight_api/worker/main.py +41 -11
  25. hindsight_api/worker/poller.py +26 -14
  26. {hindsight_api-0.4.6.dist-info → hindsight_api-0.4.8.dist-info}/METADATA +2 -1
  27. {hindsight_api-0.4.6.dist-info → hindsight_api-0.4.8.dist-info}/RECORD +29 -21
  28. {hindsight_api-0.4.6.dist-info → hindsight_api-0.4.8.dist-info}/WHEEL +0 -0
  29. {hindsight_api-0.4.6.dist-info → hindsight_api-0.4.8.dist-info}/entry_points.txt +0 -0
@@ -303,8 +303,10 @@ class MemoryEngine(MemoryEngineInterface):
303
303
  db_url = db_url or config.database_url
304
304
  memory_llm_provider = memory_llm_provider or config.llm_provider
305
305
  memory_llm_api_key = memory_llm_api_key or config.llm_api_key
306
- # Ollama and mock don't require an API key
307
- if not memory_llm_api_key and memory_llm_provider not in ("ollama", "mock"):
306
+ # Ollama, openai-codex, claude-code, and mock don't require an API key
307
+ # openai-codex uses OAuth tokens from ~/.codex/auth.json
308
+ # claude-code uses OAuth tokens from macOS Keychain
309
+ if not memory_llm_api_key and memory_llm_provider not in ("ollama", "openai-codex", "claude-code", "mock"):
308
310
  raise ValueError("LLM API key is required. Set HINDSIGHT_API_LLM_API_KEY environment variable.")
309
311
  memory_llm_model = memory_llm_model or config.llm_model
310
312
  memory_llm_base_url = memory_llm_base_url or config.get_llm_base_url() or None
@@ -457,7 +459,11 @@ class MemoryEngine(MemoryEngineInterface):
457
459
  # Store operation validator extension (optional)
458
460
  self._operation_validator = operation_validator
459
461
 
460
- # Store tenant extension (optional)
462
+ # Store tenant extension (always set, use default if none provided)
463
+ if tenant_extension is None:
464
+ from ..extensions.builtin.tenant import DefaultTenantExtension
465
+
466
+ tenant_extension = DefaultTenantExtension(config={})
461
467
  self._tenant_extension = tenant_extension
462
468
 
463
469
  async def _validate_operation(self, validation_coro) -> None:
@@ -495,22 +501,18 @@ class MemoryEngine(MemoryEngineInterface):
495
501
  Raises:
496
502
  AuthenticationError: If authentication fails or request_context is missing when required.
497
503
  """
498
- if self._tenant_extension is None:
499
- _current_schema.set("public")
500
- return "public"
501
-
502
504
  from hindsight_api.extensions import AuthenticationError
503
505
 
504
506
  if request_context is None:
505
- raise AuthenticationError("RequestContext is required when tenant extension is configured")
507
+ raise AuthenticationError("RequestContext is required")
506
508
 
507
509
  # For internal/background operations (e.g., worker tasks), skip extension authentication.
508
510
  # The task was already authenticated at submission time, and execute_task sets _current_schema
509
- # from the task's _schema field. For public schema tasks, _current_schema keeps its default "public".
511
+ # from the task's _schema field.
510
512
  if request_context.internal:
511
513
  return _current_schema.get()
512
514
 
513
- # Let AuthenticationError propagate - HTTP layer will convert to 401
515
+ # Authenticate through tenant extension (always set, may be default no-auth extension)
514
516
  tenant_context = await self._tenant_extension.authenticate(request_context)
515
517
 
516
518
  _current_schema.set(tenant_context.schema_name)
@@ -536,10 +538,15 @@ class MemoryEngine(MemoryEngineInterface):
536
538
  f"[BATCH_RETAIN_TASK] Starting background batch retain for bank_id={bank_id}, {len(contents)} items"
537
539
  )
538
540
 
539
- # Use internal request context for background tasks (skips tenant auth when schema is pre-set)
541
+ # Restore tenant_id/api_key_id from task payload so downstream operations
542
+ # (e.g., consolidation and mental model refreshes) can attribute usage.
540
543
  from hindsight_api.models import RequestContext
541
544
 
542
- internal_context = RequestContext(internal=True)
545
+ internal_context = RequestContext(
546
+ internal=True,
547
+ tenant_id=task_dict.get("_tenant_id"),
548
+ api_key_id=task_dict.get("_api_key_id"),
549
+ )
543
550
  await self.retain_batch_async(bank_id=bank_id, contents=contents, request_context=internal_context)
544
551
 
545
552
  logger.info(f"[BATCH_RETAIN_TASK] Completed background batch retain for bank_id={bank_id}")
@@ -565,7 +572,13 @@ class MemoryEngine(MemoryEngineInterface):
565
572
 
566
573
  from .consolidation import run_consolidation_job
567
574
 
568
- internal_context = RequestContext(internal=True)
575
+ # Restore tenant_id/api_key_id from task payload so downstream operations
576
+ # (e.g., mental model refreshes) can attribute usage to the correct org.
577
+ internal_context = RequestContext(
578
+ internal=True,
579
+ tenant_id=task_dict.get("_tenant_id"),
580
+ api_key_id=task_dict.get("_api_key_id"),
581
+ )
569
582
  result = await run_consolidation_job(
570
583
  memory_engine=self,
571
584
  bank_id=bank_id,
@@ -597,7 +610,13 @@ class MemoryEngine(MemoryEngineInterface):
597
610
 
598
611
  from hindsight_api.models import RequestContext
599
612
 
600
- internal_context = RequestContext(internal=True)
613
+ # Restore tenant_id/api_key_id from task payload so extensions can
614
+ # attribute the mental_model_refresh operation to the correct org.
615
+ internal_context = RequestContext(
616
+ internal=True,
617
+ tenant_id=task_dict.get("_tenant_id"),
618
+ api_key_id=task_dict.get("_api_key_id"),
619
+ )
601
620
 
602
621
  # Get the current mental model to get source_query
603
622
  mental_model = await self.get_mental_model(bank_id, mental_model_id, request_context=internal_context)
@@ -641,6 +660,42 @@ class MemoryEngine(MemoryEngineInterface):
641
660
  request_context=internal_context,
642
661
  )
643
662
 
663
+ # Call post-operation hook if validator is configured
664
+ if self._operation_validator:
665
+ from hindsight_api.extensions.operation_validator import MentalModelRefreshResult
666
+
667
+ # Count facts and mental models from based_on
668
+ facts_used = 0
669
+ mental_models_used = 0
670
+ if reflect_result.based_on:
671
+ for fact_type, facts in reflect_result.based_on.items():
672
+ if facts:
673
+ if fact_type == "mental_models":
674
+ mental_models_used += len(facts)
675
+ else:
676
+ facts_used += len(facts)
677
+
678
+ # Estimate tokens
679
+ query_tokens = len(source_query) // 4 if source_query else 0
680
+ output_tokens = len(generated_content) // 4 if generated_content else 0
681
+ context_tokens = 0 # refresh doesn't use additional context
682
+
683
+ result_ctx = MentalModelRefreshResult(
684
+ bank_id=bank_id,
685
+ mental_model_id=mental_model_id,
686
+ request_context=internal_context,
687
+ query_tokens=query_tokens,
688
+ output_tokens=output_tokens,
689
+ context_tokens=context_tokens,
690
+ facts_used=facts_used,
691
+ mental_models_used=mental_models_used,
692
+ success=True,
693
+ )
694
+ try:
695
+ await self._operation_validator.on_mental_model_refresh_complete(result_ctx)
696
+ except Exception as hook_err:
697
+ logger.warning(f"Post-mental-model-refresh hook error (non-fatal): {hook_err}")
698
+
644
699
  logger.info(f"[REFRESH_MENTAL_MODEL_TASK] Completed for bank_id={bank_id}, mental_model_id={mental_model_id}")
645
700
 
646
701
  async def execute_task(self, task_dict: dict[str, Any]):
@@ -884,30 +939,34 @@ class MemoryEngine(MemoryEngineInterface):
884
939
 
885
940
  if not self.db_url:
886
941
  raise ValueError("Database URL is required for migrations")
887
- logger.info("Running database migrations...")
888
- # Use configured database schema for migrations (defaults to "public")
889
- run_migrations(self.db_url, schema=get_config().database_schema)
890
942
 
891
- # Migrate all existing tenant schemas (if multi-tenant)
892
- if self._tenant_extension is not None:
893
- try:
894
- tenants = await self._tenant_extension.list_tenants()
895
- if tenants:
896
- logger.info(f"Running migrations on {len(tenants)} tenant schemas...")
897
- for tenant in tenants:
898
- schema = tenant.schema
899
- if schema and schema != "public":
900
- try:
901
- run_migrations(self.db_url, schema=schema)
902
- except Exception as e:
903
- logger.warning(f"Failed to migrate tenant schema {schema}: {e}")
904
- logger.info("Tenant schema migrations completed")
905
- except Exception as e:
906
- logger.warning(f"Failed to run tenant schema migrations: {e}")
907
-
908
- # Ensure embedding column dimension matches the model's dimension
909
- # This is done after migrations and after embeddings.initialize()
910
- ensure_embedding_dimension(self.db_url, self.embeddings.dimension, schema=get_config().database_schema)
943
+ # Migrate all schemas from the tenant extension
944
+ # The tenant extension is the single source of truth for which schemas exist
945
+ logger.info("Running database migrations...")
946
+ try:
947
+ tenants = await self._tenant_extension.list_tenants()
948
+ if tenants:
949
+ logger.info(f"Running migrations on {len(tenants)} schema(s)...")
950
+ for tenant in tenants:
951
+ schema = tenant.schema
952
+ if schema:
953
+ try:
954
+ run_migrations(self.db_url, schema=schema)
955
+ except Exception as e:
956
+ logger.warning(f"Failed to migrate schema {schema}: {e}")
957
+ logger.info("Schema migrations completed")
958
+
959
+ # Ensure embedding column dimension matches the model's dimension
960
+ # This is done after migrations and after embeddings.initialize()
961
+ for tenant in tenants:
962
+ schema = tenant.schema
963
+ if schema:
964
+ try:
965
+ ensure_embedding_dimension(self.db_url, self.embeddings.dimension, schema=schema)
966
+ except Exception as e:
967
+ logger.warning(f"Failed to ensure embedding dimension for schema {schema}: {e}")
968
+ except Exception as e:
969
+ logger.warning(f"Failed to run schema migrations: {e}")
911
970
 
912
971
  logger.info(f"Connecting to PostgreSQL at {self.db_url}")
913
972
 
@@ -5416,6 +5475,13 @@ class MemoryEngine(MemoryEngineInterface):
5416
5475
  task_payload: dict[str, Any] = {"contents": contents}
5417
5476
  if document_tags:
5418
5477
  task_payload["document_tags"] = document_tags
5478
+ # Pass tenant_id and api_key_id through task payload so the worker
5479
+ # can propagate request context to downstream operations (e.g.,
5480
+ # consolidation and mental model refreshes triggered after retain).
5481
+ if request_context.tenant_id:
5482
+ task_payload["_tenant_id"] = request_context.tenant_id
5483
+ if request_context.api_key_id:
5484
+ task_payload["_api_key_id"] = request_context.api_key_id
5419
5485
 
5420
5486
  result = await self._submit_async_operation(
5421
5487
  bank_id=bank_id,
@@ -5448,11 +5514,21 @@ class MemoryEngine(MemoryEngineInterface):
5448
5514
  Dict with operation_id
5449
5515
  """
5450
5516
  await self._authenticate_tenant(request_context)
5517
+
5518
+ # Pass tenant_id and api_key_id through task payload so the worker
5519
+ # can provide request context to extension hooks (e.g., usage metering
5520
+ # for mental model refreshes triggered by consolidation).
5521
+ task_payload: dict[str, Any] = {}
5522
+ if request_context.tenant_id:
5523
+ task_payload["_tenant_id"] = request_context.tenant_id
5524
+ if request_context.api_key_id:
5525
+ task_payload["_api_key_id"] = request_context.api_key_id
5526
+
5451
5527
  return await self._submit_async_operation(
5452
5528
  bank_id=bank_id,
5453
5529
  operation_type="consolidation",
5454
5530
  task_type="consolidation",
5455
- task_payload={},
5531
+ task_payload=task_payload,
5456
5532
  dedupe_by_bank=True,
5457
5533
  )
5458
5534
 
@@ -5482,13 +5558,21 @@ class MemoryEngine(MemoryEngineInterface):
5482
5558
  if not mental_model:
5483
5559
  raise ValueError(f"Mental model {mental_model_id} not found in bank {bank_id}")
5484
5560
 
5561
+ # Pass tenant_id and api_key_id through task payload so the worker
5562
+ # can provide request context to extension hooks.
5563
+ task_payload: dict[str, Any] = {
5564
+ "mental_model_id": mental_model_id,
5565
+ }
5566
+ if request_context.tenant_id:
5567
+ task_payload["_tenant_id"] = request_context.tenant_id
5568
+ if request_context.api_key_id:
5569
+ task_payload["_api_key_id"] = request_context.api_key_id
5570
+
5485
5571
  return await self._submit_async_operation(
5486
5572
  bank_id=bank_id,
5487
5573
  operation_type="refresh_mental_model",
5488
5574
  task_type="refresh_mental_model",
5489
- task_payload={
5490
- "mental_model_id": mental_model_id,
5491
- },
5575
+ task_payload=task_payload,
5492
5576
  result_metadata={"mental_model_id": mental_model_id, "name": mental_model["name"]},
5493
5577
  dedupe_by_bank=False,
5494
5578
  )
@@ -0,0 +1,14 @@
1
+ """
2
+ LLM provider implementations.
3
+
4
+ This package contains concrete implementations of the LLMInterface for various providers.
5
+ """
6
+
7
+ from .anthropic_llm import AnthropicLLM
8
+ from .claude_code_llm import ClaudeCodeLLM
9
+ from .codex_llm import CodexLLM
10
+ from .gemini_llm import GeminiLLM
11
+ from .mock_llm import MockLLM
12
+ from .openai_compatible_llm import OpenAICompatibleLLM
13
+
14
+ __all__ = ["AnthropicLLM", "ClaudeCodeLLM", "CodexLLM", "GeminiLLM", "MockLLM", "OpenAICompatibleLLM"]