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.
- hindsight_api/__init__.py +1 -1
- hindsight_api/alembic/versions/5a366d414dce_initial_schema.py +16 -2
- hindsight_api/api/http.py +83 -1
- hindsight_api/banner.py +3 -0
- hindsight_api/config.py +44 -6
- hindsight_api/daemon.py +18 -112
- hindsight_api/engine/llm_interface.py +146 -0
- hindsight_api/engine/llm_wrapper.py +304 -1327
- hindsight_api/engine/memory_engine.py +125 -41
- hindsight_api/engine/providers/__init__.py +14 -0
- hindsight_api/engine/providers/anthropic_llm.py +434 -0
- hindsight_api/engine/providers/claude_code_llm.py +352 -0
- hindsight_api/engine/providers/codex_llm.py +527 -0
- hindsight_api/engine/providers/gemini_llm.py +502 -0
- hindsight_api/engine/providers/mock_llm.py +234 -0
- hindsight_api/engine/providers/openai_compatible_llm.py +745 -0
- hindsight_api/engine/retain/fact_extraction.py +13 -9
- hindsight_api/engine/retain/fact_storage.py +5 -3
- hindsight_api/extensions/__init__.py +10 -0
- hindsight_api/extensions/builtin/tenant.py +36 -0
- hindsight_api/extensions/operation_validator.py +129 -0
- hindsight_api/main.py +6 -21
- hindsight_api/migrations.py +75 -0
- hindsight_api/worker/main.py +41 -11
- hindsight_api/worker/poller.py +26 -14
- {hindsight_api-0.4.6.dist-info → hindsight_api-0.4.8.dist-info}/METADATA +2 -1
- {hindsight_api-0.4.6.dist-info → hindsight_api-0.4.8.dist-info}/RECORD +29 -21
- {hindsight_api-0.4.6.dist-info → hindsight_api-0.4.8.dist-info}/WHEEL +0 -0
- {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
|
-
|
|
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 (
|
|
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
|
|
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.
|
|
511
|
+
# from the task's _schema field.
|
|
510
512
|
if request_context.internal:
|
|
511
513
|
return _current_schema.get()
|
|
512
514
|
|
|
513
|
-
#
|
|
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
|
-
#
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
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"]
|