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.
Files changed (47) hide show
  1. hindsight_api/__init__.py +2 -0
  2. hindsight_api/alembic/env.py +24 -1
  3. hindsight_api/alembic/versions/d9f6a3b4c5e2_rename_bank_to_interactions.py +14 -4
  4. hindsight_api/alembic/versions/e0a1b2c3d4e5_disposition_to_3_traits.py +54 -13
  5. hindsight_api/alembic/versions/rename_personality_to_disposition.py +18 -7
  6. hindsight_api/api/http.py +253 -230
  7. hindsight_api/api/mcp.py +14 -3
  8. hindsight_api/config.py +11 -0
  9. hindsight_api/daemon.py +204 -0
  10. hindsight_api/engine/__init__.py +12 -1
  11. hindsight_api/engine/entity_resolver.py +38 -37
  12. hindsight_api/engine/interface.py +592 -0
  13. hindsight_api/engine/llm_wrapper.py +176 -6
  14. hindsight_api/engine/memory_engine.py +1092 -293
  15. hindsight_api/engine/retain/bank_utils.py +13 -12
  16. hindsight_api/engine/retain/chunk_storage.py +3 -2
  17. hindsight_api/engine/retain/fact_storage.py +10 -7
  18. hindsight_api/engine/retain/link_utils.py +17 -16
  19. hindsight_api/engine/retain/observation_regeneration.py +17 -16
  20. hindsight_api/engine/retain/orchestrator.py +2 -3
  21. hindsight_api/engine/retain/types.py +25 -8
  22. hindsight_api/engine/search/graph_retrieval.py +6 -5
  23. hindsight_api/engine/search/mpfp_retrieval.py +8 -7
  24. hindsight_api/engine/search/reranking.py +17 -0
  25. hindsight_api/engine/search/retrieval.py +12 -11
  26. hindsight_api/engine/search/think_utils.py +1 -1
  27. hindsight_api/engine/search/tracer.py +1 -1
  28. hindsight_api/engine/task_backend.py +32 -0
  29. hindsight_api/extensions/__init__.py +66 -0
  30. hindsight_api/extensions/base.py +81 -0
  31. hindsight_api/extensions/builtin/__init__.py +18 -0
  32. hindsight_api/extensions/builtin/tenant.py +33 -0
  33. hindsight_api/extensions/context.py +110 -0
  34. hindsight_api/extensions/http.py +89 -0
  35. hindsight_api/extensions/loader.py +125 -0
  36. hindsight_api/extensions/operation_validator.py +325 -0
  37. hindsight_api/extensions/tenant.py +63 -0
  38. hindsight_api/main.py +97 -17
  39. hindsight_api/mcp_local.py +7 -1
  40. hindsight_api/migrations.py +54 -10
  41. hindsight_api/models.py +15 -0
  42. hindsight_api/pg0.py +1 -1
  43. {hindsight_api-0.1.11.dist-info → hindsight_api-0.1.13.dist-info}/METADATA +1 -1
  44. hindsight_api-0.1.13.dist-info/RECORD +75 -0
  45. hindsight_api-0.1.11.dist-info/RECORD +0 -64
  46. {hindsight_api-0.1.11.dist-info → hindsight_api-0.1.13.dist-info}/WHEEL +0 -0
  47. {hindsight_api-0.1.11.dist-info → hindsight_api-0.1.13.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,66 @@
1
+ """
2
+ Hindsight Extensions System.
3
+
4
+ Extensions allow customizing and extending Hindsight behavior without modifying core code.
5
+ Extensions are loaded via environment variables pointing to implementation classes.
6
+
7
+ Example:
8
+ HINDSIGHT_API_OPERATION_VALIDATOR_EXTENSION=mypackage.validators:MyValidator
9
+ HINDSIGHT_API_OPERATION_VALIDATOR_MAX_RETRIES=3
10
+
11
+ HINDSIGHT_API_HTTP_EXTENSION=mypackage.http:MyHttpExtension
12
+ HINDSIGHT_API_HTTP_SOME_CONFIG=value
13
+
14
+ Extensions receive an ExtensionContext that provides a controlled API for interacting
15
+ with the system (e.g., running migrations for tenant schemas).
16
+ """
17
+
18
+ from hindsight_api.extensions.base import Extension
19
+ from hindsight_api.extensions.builtin import ApiKeyTenantExtension
20
+ from hindsight_api.extensions.context import DefaultExtensionContext, ExtensionContext
21
+ from hindsight_api.extensions.http import HttpExtension
22
+ from hindsight_api.extensions.loader import load_extension
23
+ from hindsight_api.extensions.operation_validator import (
24
+ OperationValidationError,
25
+ OperationValidatorExtension,
26
+ RecallContext,
27
+ RecallResult,
28
+ ReflectContext,
29
+ ReflectResultContext,
30
+ RetainContext,
31
+ RetainResult,
32
+ ValidationResult,
33
+ )
34
+ from hindsight_api.extensions.tenant import (
35
+ AuthenticationError,
36
+ TenantContext,
37
+ TenantExtension,
38
+ )
39
+ from hindsight_api.models import RequestContext
40
+
41
+ __all__ = [
42
+ # Base
43
+ "Extension",
44
+ "load_extension",
45
+ # Context
46
+ "ExtensionContext",
47
+ "DefaultExtensionContext",
48
+ # HTTP Extension
49
+ "HttpExtension",
50
+ # Operation Validator
51
+ "OperationValidationError",
52
+ "OperationValidatorExtension",
53
+ "RecallContext",
54
+ "RecallResult",
55
+ "ReflectContext",
56
+ "ReflectResultContext",
57
+ "RetainContext",
58
+ "RetainResult",
59
+ "ValidationResult",
60
+ # Tenant/Auth
61
+ "ApiKeyTenantExtension",
62
+ "AuthenticationError",
63
+ "RequestContext",
64
+ "TenantContext",
65
+ "TenantExtension",
66
+ ]
@@ -0,0 +1,81 @@
1
+ """Base Extension class for all Hindsight extensions."""
2
+
3
+ from abc import ABC
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from hindsight_api.extensions.context import ExtensionContext
8
+
9
+
10
+ class Extension(ABC):
11
+ """
12
+ Base class for all Hindsight extensions.
13
+
14
+ Extensions are loaded via environment variables and receive configuration
15
+ from prefixed environment variables.
16
+
17
+ Example:
18
+ HINDSIGHT_API_MY_EXTENSION=mypackage.ext:MyExtension
19
+ HINDSIGHT_API_MY_SOME_CONFIG=value
20
+
21
+ The extension receives: {"some_config": "value"}
22
+
23
+ Extensions also receive an ExtensionContext that provides a controlled API
24
+ for interacting with the system (e.g., running migrations for tenant schemas).
25
+ """
26
+
27
+ def __init__(self, config: dict[str, str]):
28
+ """
29
+ Initialize the extension with configuration.
30
+
31
+ Args:
32
+ config: Dictionary of configuration values from environment variables.
33
+ Keys are lowercased with the prefix stripped.
34
+ """
35
+ self.config = config
36
+ self._context: "ExtensionContext | None" = None
37
+
38
+ def set_context(self, context: "ExtensionContext") -> None:
39
+ """
40
+ Set the extension context.
41
+
42
+ Called by the extension loader after instantiation.
43
+ Extensions should not call this directly.
44
+
45
+ Args:
46
+ context: The ExtensionContext providing system APIs.
47
+ """
48
+ self._context = context
49
+
50
+ @property
51
+ def context(self) -> "ExtensionContext":
52
+ """
53
+ Get the extension context.
54
+
55
+ Returns:
56
+ The ExtensionContext providing system APIs.
57
+
58
+ Raises:
59
+ RuntimeError: If context has not been set yet.
60
+ """
61
+ if self._context is None:
62
+ raise RuntimeError(
63
+ "Extension context not set. Context is available after the extension is loaded by the system."
64
+ )
65
+ return self._context
66
+
67
+ async def on_startup(self) -> None:
68
+ """
69
+ Called when the application starts.
70
+
71
+ Override to perform initialization tasks like connecting to external services.
72
+ """
73
+ pass
74
+
75
+ async def on_shutdown(self) -> None:
76
+ """
77
+ Called when the application shuts down.
78
+
79
+ Override to perform cleanup tasks like closing connections.
80
+ """
81
+ pass
@@ -0,0 +1,18 @@
1
+ """
2
+ Built-in extension implementations.
3
+
4
+ These are ready-to-use implementations of the extension interfaces.
5
+ They can be used directly or serve as examples for custom implementations.
6
+
7
+ Available built-in extensions:
8
+ - ApiKeyTenantExtension: Simple API key validation with public schema
9
+
10
+ Example usage:
11
+ HINDSIGHT_API_TENANT_EXTENSION=hindsight_api.extensions.builtin.tenant:ApiKeyTenantExtension
12
+ """
13
+
14
+ from hindsight_api.extensions.builtin.tenant import ApiKeyTenantExtension
15
+
16
+ __all__ = [
17
+ "ApiKeyTenantExtension",
18
+ ]
@@ -0,0 +1,33 @@
1
+ """Built-in tenant extension implementations."""
2
+
3
+ from hindsight_api.extensions.tenant import AuthenticationError, TenantContext, TenantExtension
4
+ from hindsight_api.models import RequestContext
5
+
6
+
7
+ class ApiKeyTenantExtension(TenantExtension):
8
+ """
9
+ Built-in tenant extension that validates API key against an environment variable.
10
+
11
+ This is a simple implementation that:
12
+ 1. Validates the API key matches HINDSIGHT_API_TENANT_API_KEY
13
+ 2. Returns 'public' as the schema for all authenticated requests
14
+
15
+ Configuration:
16
+ HINDSIGHT_API_TENANT_EXTENSION=hindsight_api.extensions.builtin.tenant:ApiKeyTenantExtension
17
+ HINDSIGHT_API_TENANT_API_KEY=your-secret-key
18
+
19
+ For multi-tenant setups with separate schemas per tenant, implement a custom
20
+ TenantExtension that looks up the schema based on the API key or token claims.
21
+ """
22
+
23
+ def __init__(self, config: dict[str, str]):
24
+ super().__init__(config)
25
+ self.expected_api_key = config.get("api_key")
26
+ if not self.expected_api_key:
27
+ raise ValueError("HINDSIGHT_API_TENANT_API_KEY is required when using ApiKeyTenantExtension")
28
+
29
+ async def authenticate(self, context: RequestContext) -> TenantContext:
30
+ """Validate API key and return public schema context."""
31
+ if context.api_key != self.expected_api_key:
32
+ raise AuthenticationError("Invalid API key")
33
+ return TenantContext(schema_name="public")
@@ -0,0 +1,110 @@
1
+ """Extension context providing a controlled API for extensions to interact with the system."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from hindsight_api.engine.interface import MemoryEngineInterface
8
+
9
+
10
+ class ExtensionContext(ABC):
11
+ """
12
+ Abstract context providing a controlled API for extensions.
13
+
14
+ Extensions receive this context instead of direct access to internal
15
+ components like MemoryEngine or database connections. This provides:
16
+ - A stable API that won't break when internals change
17
+ - Security by limiting what extensions can access
18
+ - Clear documentation of what extensions can do
19
+
20
+ Built-in implementation:
21
+ hindsight_api.extensions.builtin.context.DefaultExtensionContext
22
+
23
+ Example usage in an extension:
24
+ class MyTenantExtension(TenantExtension):
25
+ async def on_startup(self) -> None:
26
+ # Run migrations for a new tenant schema
27
+ await self.context.run_migration("tenant_acme")
28
+
29
+ class MyHttpExtension(HttpExtension):
30
+ def get_router(self, memory):
31
+ # Use memory engine for custom endpoints
32
+ engine = self.context.get_memory_engine()
33
+ ...
34
+ """
35
+
36
+ @abstractmethod
37
+ async def run_migration(self, schema: str) -> None:
38
+ """
39
+ Run database migrations for a specific schema.
40
+
41
+ This creates the schema if it doesn't exist and runs all pending
42
+ migrations. Uses advisory locks to coordinate between distributed workers.
43
+
44
+ Args:
45
+ schema: PostgreSQL schema name (e.g., "tenant_acme").
46
+ The schema will be created if it doesn't exist.
47
+
48
+ Raises:
49
+ RuntimeError: If migrations fail to complete.
50
+
51
+ Example:
52
+ # Provision a new tenant schema
53
+ await context.run_migration("tenant_acme")
54
+ """
55
+ ...
56
+
57
+ @abstractmethod
58
+ def get_memory_engine(self) -> "MemoryEngineInterface":
59
+ """
60
+ Get the memory engine interface.
61
+
62
+ Returns the MemoryEngineInterface for performing memory operations
63
+ like retain, recall, reflect, and entity/document management.
64
+
65
+ Returns:
66
+ MemoryEngineInterface instance.
67
+
68
+ Example:
69
+ engine = context.get_memory_engine()
70
+ result = await engine.recall_async(bank_id, query)
71
+ """
72
+ ...
73
+
74
+
75
+ class DefaultExtensionContext(ExtensionContext):
76
+ """
77
+ Default implementation of ExtensionContext.
78
+
79
+ Uses the system's database URL and migration infrastructure.
80
+ """
81
+
82
+ def __init__(
83
+ self,
84
+ database_url: str,
85
+ memory_engine: "MemoryEngineInterface | None" = None,
86
+ ):
87
+ """
88
+ Initialize the context.
89
+
90
+ Args:
91
+ database_url: SQLAlchemy database URL for migrations.
92
+ memory_engine: Optional MemoryEngine instance for memory operations.
93
+ """
94
+ self._database_url = database_url
95
+ self._memory_engine = memory_engine
96
+
97
+ async def run_migration(self, schema: str) -> None:
98
+ """Run migrations for a specific schema."""
99
+ from hindsight_api.migrations import run_migrations
100
+
101
+ run_migrations(self._database_url, schema=schema)
102
+
103
+ def get_memory_engine(self) -> "MemoryEngineInterface":
104
+ """Get the memory engine interface."""
105
+ if self._memory_engine is None:
106
+ raise RuntimeError(
107
+ "Memory engine not configured in ExtensionContext. "
108
+ "Ensure the context was created with a memory_engine parameter."
109
+ )
110
+ return self._memory_engine
@@ -0,0 +1,89 @@
1
+ """
2
+ HTTP Extension for adding custom endpoints to the Hindsight API.
3
+
4
+ This extension allows adding custom HTTP endpoints under the /ext/ path prefix.
5
+ The extension provides a FastAPI router that is mounted on the main application.
6
+ """
7
+
8
+ from abc import ABC, abstractmethod
9
+ from typing import TYPE_CHECKING
10
+
11
+ from fastapi import APIRouter
12
+
13
+ from hindsight_api.extensions.base import Extension
14
+
15
+ if TYPE_CHECKING:
16
+ from hindsight_api import MemoryEngine
17
+
18
+
19
+ class HttpExtension(Extension, ABC):
20
+ """
21
+ Base class for HTTP extensions that add custom API endpoints.
22
+
23
+ HTTP extensions provide a FastAPI router that gets mounted under /ext/.
24
+ The extension has full control over the routes, request/response models, and handlers.
25
+
26
+ Example:
27
+ ```python
28
+ from fastapi import APIRouter
29
+ from hindsight_api.extensions import HttpExtension
30
+
31
+ class MyHttpExtension(HttpExtension):
32
+ def get_router(self, memory: MemoryEngine) -> APIRouter:
33
+ router = APIRouter()
34
+
35
+ @router.get("/hello")
36
+ async def hello():
37
+ return {"message": "Hello from extension!"}
38
+
39
+ @router.post("/custom/{bank_id}/action")
40
+ async def custom_action(bank_id: str):
41
+ # Access memory engine for database operations
42
+ pool = await memory._get_pool()
43
+ # ... custom logic
44
+ return {"status": "ok"}
45
+
46
+ return router
47
+ ```
48
+
49
+ The routes will be available at:
50
+ - GET /ext/hello
51
+ - POST /ext/custom/{bank_id}/action
52
+
53
+ Configuration via environment variables:
54
+ HINDSIGHT_API_HTTP_EXTENSION=mypackage.ext:MyHttpExtension
55
+ HINDSIGHT_API_HTTP_SOME_CONFIG=value
56
+
57
+ The extension receives config: {"some_config": "value"}
58
+ """
59
+
60
+ @abstractmethod
61
+ def get_router(self, memory: "MemoryEngine") -> APIRouter:
62
+ """
63
+ Return a FastAPI router with custom endpoints.
64
+
65
+ The router will be mounted at /ext/ on the main application.
66
+ All routes defined in the router will be prefixed with /ext/.
67
+
68
+ Args:
69
+ memory: The MemoryEngine instance for database access and core operations.
70
+ Use this to access the connection pool, run queries, or call
71
+ memory operations like retain, recall, etc.
72
+
73
+ Returns:
74
+ A FastAPI APIRouter with the custom endpoints defined.
75
+
76
+ Example:
77
+ ```python
78
+ def get_router(self, memory: MemoryEngine) -> APIRouter:
79
+ router = APIRouter(tags=["My Extension"])
80
+
81
+ @router.get("/status")
82
+ async def status():
83
+ health = await memory.health_check()
84
+ return {"extension": "healthy", "memory": health}
85
+
86
+ return router
87
+ ```
88
+ """
89
+ pass
@@ -0,0 +1,125 @@
1
+ """Extension loader utilities."""
2
+
3
+ import importlib
4
+ import logging
5
+ import os
6
+ from typing import TYPE_CHECKING, TypeVar
7
+
8
+ from hindsight_api.extensions.base import Extension
9
+
10
+ if TYPE_CHECKING:
11
+ from hindsight_api.extensions.context import ExtensionContext
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ T = TypeVar("T", bound=Extension)
16
+
17
+
18
+ class ExtensionLoadError(Exception):
19
+ """Raised when an extension fails to load."""
20
+
21
+ pass
22
+
23
+
24
+ def load_extension(
25
+ prefix: str,
26
+ base_class: type[T],
27
+ env_prefix: str = "HINDSIGHT_API",
28
+ context: "ExtensionContext | None" = None,
29
+ ) -> T | None:
30
+ """
31
+ Load an extension from environment variable configuration.
32
+
33
+ The extension class is specified via {env_prefix}_{prefix}_EXTENSION environment
34
+ variable in the format "module.path:ClassName".
35
+
36
+ Configuration for the extension is collected from all environment variables
37
+ matching {env_prefix}_{prefix}_* (excluding the EXTENSION variable itself).
38
+
39
+ Args:
40
+ prefix: The extension prefix (e.g., "OPERATION_VALIDATOR").
41
+ base_class: The base class that the extension must inherit from.
42
+ env_prefix: The environment variable prefix (default: "HINDSIGHT_API").
43
+ context: Optional ExtensionContext to provide system APIs to the extension.
44
+
45
+ Returns:
46
+ An instance of the extension, or None if not configured.
47
+
48
+ Raises:
49
+ ExtensionLoadError: If the extension fails to load or validate.
50
+
51
+ Example:
52
+ HINDSIGHT_API_OPERATION_VALIDATOR_EXTENSION=mypackage.validators:MyValidator
53
+ HINDSIGHT_API_OPERATION_VALIDATOR_MAX_REQUESTS=100
54
+
55
+ ext = load_extension("OPERATION_VALIDATOR", OperationValidatorExtension)
56
+ # ext.config == {"max_requests": "100"}
57
+ """
58
+ env_var = f"{env_prefix}_{prefix}_EXTENSION"
59
+ ext_path = os.getenv(env_var)
60
+
61
+ if not ext_path:
62
+ logger.debug(f"No extension configured for {env_var}")
63
+ return None
64
+
65
+ logger.info(f"Loading extension from {env_var}={ext_path}")
66
+
67
+ # Parse "module.path:ClassName"
68
+ if ":" not in ext_path:
69
+ raise ExtensionLoadError(f"Invalid extension path '{ext_path}'. Expected format: 'module.path:ClassName'")
70
+
71
+ module_path, class_name = ext_path.rsplit(":", 1)
72
+
73
+ # Import the module
74
+ try:
75
+ module = importlib.import_module(module_path)
76
+ except ImportError as e:
77
+ raise ExtensionLoadError(f"Failed to import extension module '{module_path}': {e}") from e
78
+
79
+ # Get the class
80
+ try:
81
+ ext_class = getattr(module, class_name)
82
+ except AttributeError as e:
83
+ raise ExtensionLoadError(f"Extension class '{class_name}' not found in module '{module_path}'") from e
84
+
85
+ # Validate inheritance
86
+ if not isinstance(ext_class, type) or not issubclass(ext_class, base_class):
87
+ raise ExtensionLoadError(f"Extension class '{ext_class.__name__}' must inherit from '{base_class.__name__}'")
88
+
89
+ # Collect configuration from environment variables
90
+ config = _collect_config(env_prefix, prefix)
91
+
92
+ logger.info(f"Loaded extension {ext_class.__name__} with config keys: {list(config.keys())}")
93
+
94
+ # Instantiate the extension
95
+ try:
96
+ extension = ext_class(config)
97
+ except Exception as e:
98
+ raise ExtensionLoadError(f"Failed to instantiate extension '{ext_class.__name__}': {e}") from e
99
+
100
+ # Set the context if provided
101
+ if context is not None:
102
+ extension.set_context(context)
103
+ logger.debug(f"Set context on extension {ext_class.__name__}")
104
+
105
+ return extension
106
+
107
+
108
+ def _collect_config(env_prefix: str, prefix: str) -> dict[str, str]:
109
+ """
110
+ Collect configuration from environment variables.
111
+
112
+ Collects all variables matching {env_prefix}_{prefix}_* except for
113
+ {env_prefix}_{prefix}_EXTENSION, strips the prefix, and lowercases keys.
114
+ """
115
+ config = {}
116
+ full_prefix = f"{env_prefix}_{prefix}_"
117
+ extension_var = f"{full_prefix}EXTENSION"
118
+
119
+ for key, value in os.environ.items():
120
+ if key.startswith(full_prefix) and key != extension_var:
121
+ # Strip prefix and lowercase the key
122
+ config_key = key[len(full_prefix) :].lower()
123
+ config[config_key] = value
124
+
125
+ return config