hindsight-api 0.1.5__py3-none-any.whl → 0.1.7__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 (64) hide show
  1. hindsight_api/__init__.py +10 -9
  2. hindsight_api/alembic/env.py +5 -8
  3. hindsight_api/alembic/versions/5a366d414dce_initial_schema.py +266 -180
  4. hindsight_api/alembic/versions/b7c4d8e9f1a2_add_chunks_table.py +32 -32
  5. hindsight_api/alembic/versions/c8e5f2a3b4d1_add_retain_params_to_documents.py +11 -11
  6. hindsight_api/alembic/versions/d9f6a3b4c5e2_rename_bank_to_interactions.py +7 -12
  7. hindsight_api/alembic/versions/e0a1b2c3d4e5_disposition_to_3_traits.py +23 -15
  8. hindsight_api/alembic/versions/rename_personality_to_disposition.py +30 -21
  9. hindsight_api/api/__init__.py +10 -10
  10. hindsight_api/api/http.py +575 -593
  11. hindsight_api/api/mcp.py +30 -28
  12. hindsight_api/banner.py +13 -6
  13. hindsight_api/config.py +9 -13
  14. hindsight_api/engine/__init__.py +9 -9
  15. hindsight_api/engine/cross_encoder.py +22 -21
  16. hindsight_api/engine/db_utils.py +5 -4
  17. hindsight_api/engine/embeddings.py +22 -21
  18. hindsight_api/engine/entity_resolver.py +81 -75
  19. hindsight_api/engine/llm_wrapper.py +61 -79
  20. hindsight_api/engine/memory_engine.py +603 -625
  21. hindsight_api/engine/query_analyzer.py +100 -97
  22. hindsight_api/engine/response_models.py +105 -106
  23. hindsight_api/engine/retain/__init__.py +9 -16
  24. hindsight_api/engine/retain/bank_utils.py +34 -58
  25. hindsight_api/engine/retain/chunk_storage.py +4 -12
  26. hindsight_api/engine/retain/deduplication.py +9 -28
  27. hindsight_api/engine/retain/embedding_processing.py +4 -11
  28. hindsight_api/engine/retain/embedding_utils.py +3 -4
  29. hindsight_api/engine/retain/entity_processing.py +7 -17
  30. hindsight_api/engine/retain/fact_extraction.py +155 -165
  31. hindsight_api/engine/retain/fact_storage.py +11 -23
  32. hindsight_api/engine/retain/link_creation.py +11 -39
  33. hindsight_api/engine/retain/link_utils.py +166 -95
  34. hindsight_api/engine/retain/observation_regeneration.py +39 -52
  35. hindsight_api/engine/retain/orchestrator.py +72 -62
  36. hindsight_api/engine/retain/types.py +49 -43
  37. hindsight_api/engine/search/__init__.py +5 -5
  38. hindsight_api/engine/search/fusion.py +6 -15
  39. hindsight_api/engine/search/graph_retrieval.py +22 -23
  40. hindsight_api/engine/search/mpfp_retrieval.py +76 -92
  41. hindsight_api/engine/search/observation_utils.py +9 -16
  42. hindsight_api/engine/search/reranking.py +4 -7
  43. hindsight_api/engine/search/retrieval.py +87 -66
  44. hindsight_api/engine/search/scoring.py +5 -7
  45. hindsight_api/engine/search/temporal_extraction.py +8 -11
  46. hindsight_api/engine/search/think_utils.py +115 -39
  47. hindsight_api/engine/search/trace.py +68 -39
  48. hindsight_api/engine/search/tracer.py +44 -35
  49. hindsight_api/engine/search/types.py +20 -17
  50. hindsight_api/engine/task_backend.py +21 -26
  51. hindsight_api/engine/utils.py +25 -10
  52. hindsight_api/main.py +21 -40
  53. hindsight_api/mcp_local.py +190 -0
  54. hindsight_api/metrics.py +44 -30
  55. hindsight_api/migrations.py +10 -8
  56. hindsight_api/models.py +60 -72
  57. hindsight_api/pg0.py +22 -23
  58. hindsight_api/server.py +3 -6
  59. hindsight_api-0.1.7.dist-info/METADATA +178 -0
  60. hindsight_api-0.1.7.dist-info/RECORD +64 -0
  61. {hindsight_api-0.1.5.dist-info → hindsight_api-0.1.7.dist-info}/entry_points.txt +1 -0
  62. hindsight_api-0.1.5.dist-info/METADATA +0 -42
  63. hindsight_api-0.1.5.dist-info/RECORD +0 -63
  64. {hindsight_api-0.1.5.dist-info → hindsight_api-0.1.7.dist-info}/WHEEL +0 -0
@@ -0,0 +1,190 @@
1
+ """
2
+ Local MCP server for use with Claude Code (stdio transport).
3
+
4
+ This runs a fully local Hindsight instance with embedded PostgreSQL (pg0).
5
+ No external database or server required.
6
+
7
+ Run with:
8
+ hindsight-local-mcp
9
+
10
+ Or with uvx:
11
+ uvx hindsight-api@latest hindsight-local-mcp
12
+
13
+ Configure in Claude Code's MCP settings:
14
+ {
15
+ "mcpServers": {
16
+ "hindsight": {
17
+ "command": "uvx",
18
+ "args": ["hindsight-api@latest", "hindsight-local-mcp"],
19
+ "env": {
20
+ "HINDSIGHT_API_LLM_API_KEY": "your-openai-key"
21
+ }
22
+ }
23
+ }
24
+ }
25
+
26
+ Environment variables:
27
+ HINDSIGHT_API_LLM_API_KEY: Required. API key for LLM provider.
28
+ HINDSIGHT_API_LLM_PROVIDER: Optional. LLM provider (default: "openai").
29
+ HINDSIGHT_API_LLM_MODEL: Optional. LLM model (default: "gpt-4o-mini").
30
+ HINDSIGHT_API_MCP_LOCAL_BANK_ID: Optional. Memory bank ID (default: "mcp").
31
+ HINDSIGHT_API_LOG_LEVEL: Optional. Log level (default: "info").
32
+ """
33
+
34
+ import logging
35
+ import os
36
+ import sys
37
+
38
+ from mcp.server.fastmcp import FastMCP
39
+
40
+ from hindsight_api.config import (
41
+ DEFAULT_MCP_LOCAL_BANK_ID,
42
+ ENV_MCP_LOCAL_BANK_ID,
43
+ )
44
+
45
+ # Configure logging - default to info
46
+ _log_level_str = os.environ.get("HINDSIGHT_API_LOG_LEVEL", "info").lower()
47
+ _log_level_map = {
48
+ "critical": logging.CRITICAL,
49
+ "error": logging.ERROR,
50
+ "warning": logging.WARNING,
51
+ "info": logging.INFO,
52
+ "debug": logging.DEBUG,
53
+ }
54
+ logging.basicConfig(
55
+ level=_log_level_map.get(_log_level_str, logging.WARNING),
56
+ format="%(asctime)s - %(levelname)s - %(name)s - %(message)s",
57
+ stream=sys.stderr, # MCP uses stdout for protocol, logs go to stderr
58
+ )
59
+ logger = logging.getLogger(__name__)
60
+
61
+
62
+ def create_local_mcp_server(bank_id: str, memory=None) -> FastMCP:
63
+ """
64
+ Create a stdio MCP server with retain/recall tools.
65
+
66
+ Args:
67
+ bank_id: The memory bank ID to use for all operations.
68
+ memory: Optional MemoryEngine instance. If not provided, creates one with pg0.
69
+
70
+ Returns:
71
+ Configured FastMCP server instance.
72
+ """
73
+ # Import here to avoid slow startup if just checking --help
74
+ from hindsight_api import MemoryEngine
75
+ from hindsight_api.engine.memory_engine import Budget
76
+ from hindsight_api.engine.response_models import VALID_RECALL_FACT_TYPES
77
+
78
+ # Create memory engine with pg0 embedded database if not provided
79
+ if memory is None:
80
+ memory = MemoryEngine(db_url="pg0://hindsight-mcp")
81
+
82
+ mcp = FastMCP("hindsight")
83
+
84
+ @mcp.tool()
85
+ async def retain(content: str, context: str = "general") -> dict:
86
+ """
87
+ Store important information to long-term memory.
88
+
89
+ Use this tool PROACTIVELY whenever the user shares:
90
+ - Personal facts, preferences, or interests
91
+ - Important events or milestones
92
+ - User history, experiences, or background
93
+ - Decisions, opinions, or stated preferences
94
+ - Goals, plans, or future intentions
95
+ - Relationships or people mentioned
96
+ - Work context, projects, or responsibilities
97
+
98
+ Args:
99
+ content: The fact/memory to store (be specific and include relevant details)
100
+ context: Category for the memory (e.g., 'preferences', 'work', 'hobbies', 'family'). Default: 'general'
101
+ """
102
+ import asyncio
103
+
104
+ async def _retain():
105
+ try:
106
+ await memory.retain_batch_async(bank_id=bank_id, contents=[{"content": content, "context": context}])
107
+ except Exception as e:
108
+ logger.error(f"Error storing memory: {e}", exc_info=True)
109
+
110
+ # Fire and forget - don't block on memory storage
111
+ asyncio.create_task(_retain())
112
+ return {"status": "accepted", "message": "Memory storage initiated"}
113
+
114
+ @mcp.tool()
115
+ async def recall(query: str, max_tokens: int = 4096, budget: str = "low") -> dict:
116
+ """
117
+ Search memories to provide personalized, context-aware responses.
118
+
119
+ Use this tool PROACTIVELY to:
120
+ - Check user's preferences before making suggestions
121
+ - Recall user's history to provide continuity
122
+ - Remember user's goals and context
123
+ - Personalize responses based on past interactions
124
+
125
+ Args:
126
+ query: Natural language search query (e.g., "user's food preferences", "what projects is user working on")
127
+ max_tokens: Maximum tokens to return in results (default: 4096)
128
+ budget: Search budget level - "low", "mid", or "high" (default: "low")
129
+ """
130
+ try:
131
+ # Map string budget to enum
132
+ budget_map = {"low": Budget.LOW, "mid": Budget.MID, "high": Budget.HIGH}
133
+ budget_enum = budget_map.get(budget.lower(), Budget.LOW)
134
+
135
+ search_result = await memory.recall_async(
136
+ bank_id=bank_id,
137
+ query=query,
138
+ fact_type=list(VALID_RECALL_FACT_TYPES),
139
+ budget=budget_enum,
140
+ max_tokens=max_tokens,
141
+ )
142
+
143
+ return search_result.model_dump()
144
+ except Exception as e:
145
+ logger.error(f"Error searching: {e}", exc_info=True)
146
+ return {"error": str(e), "results": []}
147
+
148
+ return mcp
149
+
150
+
151
+ async def _initialize_and_run(bank_id: str):
152
+ """Initialize memory and run the MCP server."""
153
+ from hindsight_api import MemoryEngine
154
+
155
+ # Create and initialize memory engine with pg0 embedded database
156
+ print("Initializing memory engine...", file=sys.stderr)
157
+ memory = MemoryEngine(db_url="pg0://hindsight-mcp")
158
+ await memory.initialize()
159
+ print("Memory engine initialized.", file=sys.stderr)
160
+
161
+ # Create and run the server
162
+ mcp = create_local_mcp_server(bank_id, memory=memory)
163
+ await mcp.run_stdio_async()
164
+
165
+
166
+ def main():
167
+ """Main entry point for the stdio MCP server."""
168
+ import asyncio
169
+
170
+ from hindsight_api.config import ENV_LLM_API_KEY, get_config
171
+
172
+ # Check for required environment variables
173
+ config = get_config()
174
+ if not config.llm_api_key:
175
+ print(f"Error: {ENV_LLM_API_KEY} environment variable is required", file=sys.stderr)
176
+ print("Set it in your MCP configuration or shell environment", file=sys.stderr)
177
+ sys.exit(1)
178
+
179
+ # Get bank ID from environment, default to "mcp"
180
+ bank_id = os.environ.get(ENV_MCP_LOCAL_BANK_ID, DEFAULT_MCP_LOCAL_BANK_ID)
181
+
182
+ # Print startup message to stderr (stdout is reserved for MCP protocol)
183
+ print(f"Hindsight MCP server starting (bank_id={bank_id})...", file=sys.stderr)
184
+
185
+ # Run the async initialization and server
186
+ asyncio.run(_initialize_and_run(bank_id))
187
+
188
+
189
+ if __name__ == "__main__":
190
+ main()
hindsight_api/metrics.py CHANGED
@@ -6,16 +6,15 @@ This module provides metrics for:
6
6
  - Token usage (input/output) per operation
7
7
  - Per-bank granularity via labels
8
8
  """
9
+
9
10
  import logging
10
- from typing import Dict, Any, Optional
11
- from contextlib import contextmanager
12
11
  import time
12
+ from contextlib import contextmanager
13
13
 
14
14
  from opentelemetry import metrics
15
+ from opentelemetry.exporter.prometheus import PrometheusMetricReader
15
16
  from opentelemetry.sdk.metrics import MeterProvider
16
17
  from opentelemetry.sdk.resources import Resource
17
- from opentelemetry.exporter.prometheus import PrometheusMetricReader
18
- from prometheus_client import REGISTRY
19
18
 
20
19
  logger = logging.getLogger(__name__)
21
20
 
@@ -39,19 +38,18 @@ def initialize_metrics(service_name: str = "hindsight-api", service_version: str
39
38
  global _meter
40
39
 
41
40
  # Create resource with service information
42
- resource = Resource.create({
43
- "service.name": service_name,
44
- "service.version": service_version,
45
- })
41
+ resource = Resource.create(
42
+ {
43
+ "service.name": service_name,
44
+ "service.version": service_version,
45
+ }
46
+ )
46
47
 
47
48
  # Create Prometheus metric reader
48
49
  prometheus_reader = PrometheusMetricReader()
49
50
 
50
51
  # Create meter provider with Prometheus exporter
51
- provider = MeterProvider(
52
- resource=resource,
53
- metric_readers=[prometheus_reader]
54
- )
52
+ provider = MeterProvider(resource=resource, metric_readers=[prometheus_reader])
55
53
 
56
54
  # Set the global meter provider
57
55
  metrics.set_meter_provider(provider)
@@ -73,11 +71,19 @@ class MetricsCollectorBase:
73
71
  """Base class for metrics collectors."""
74
72
 
75
73
  @contextmanager
76
- def record_operation(self, operation: str, bank_id: str, budget: Optional[str] = None, max_tokens: Optional[int] = None):
74
+ def record_operation(self, operation: str, bank_id: str, budget: str | None = None, max_tokens: int | None = None):
77
75
  """Context manager to record operation duration and status."""
78
76
  raise NotImplementedError
79
77
 
80
- def record_tokens(self, operation: str, bank_id: str, input_tokens: int = 0, output_tokens: int = 0, budget: Optional[str] = None, max_tokens: Optional[int] = None):
78
+ def record_tokens(
79
+ self,
80
+ operation: str,
81
+ bank_id: str,
82
+ input_tokens: int = 0,
83
+ output_tokens: int = 0,
84
+ budget: str | None = None,
85
+ max_tokens: int | None = None,
86
+ ):
81
87
  """Record token usage for an operation."""
82
88
  raise NotImplementedError
83
89
 
@@ -86,11 +92,19 @@ class NoOpMetricsCollector(MetricsCollectorBase):
86
92
  """No-op metrics collector that does nothing. Used when metrics are disabled."""
87
93
 
88
94
  @contextmanager
89
- def record_operation(self, operation: str, bank_id: str, budget: Optional[str] = None, max_tokens: Optional[int] = None):
95
+ def record_operation(self, operation: str, bank_id: str, budget: str | None = None, max_tokens: int | None = None):
90
96
  """No-op context manager."""
91
97
  yield
92
98
 
93
- def record_tokens(self, operation: str, bank_id: str, input_tokens: int = 0, output_tokens: int = 0, budget: Optional[str] = None, max_tokens: Optional[int] = None):
99
+ def record_tokens(
100
+ self,
101
+ operation: str,
102
+ bank_id: str,
103
+ input_tokens: int = 0,
104
+ output_tokens: int = 0,
105
+ budget: str | None = None,
106
+ max_tokens: int | None = None,
107
+ ):
94
108
  """No-op token recording."""
95
109
  pass
96
110
 
@@ -108,33 +122,25 @@ class MetricsCollector(MetricsCollectorBase):
108
122
  # Operation latency histogram (in seconds)
109
123
  # Records duration of retain, recall, reflect operations
110
124
  self.operation_duration = self.meter.create_histogram(
111
- name="hindsight.operation.duration",
112
- description="Duration of Hindsight operations in seconds",
113
- unit="s"
125
+ name="hindsight.operation.duration", description="Duration of Hindsight operations in seconds", unit="s"
114
126
  )
115
127
 
116
128
  # Token usage counters
117
129
  self.tokens_input = self.meter.create_counter(
118
- name="hindsight.tokens.input",
119
- description="Number of input tokens consumed",
120
- unit="tokens"
130
+ name="hindsight.tokens.input", description="Number of input tokens consumed", unit="tokens"
121
131
  )
122
132
 
123
133
  self.tokens_output = self.meter.create_counter(
124
- name="hindsight.tokens.output",
125
- description="Number of output tokens generated",
126
- unit="tokens"
134
+ name="hindsight.tokens.output", description="Number of output tokens generated", unit="tokens"
127
135
  )
128
136
 
129
137
  # Operation counter (success/failure)
130
138
  self.operation_total = self.meter.create_counter(
131
- name="hindsight.operation.total",
132
- description="Total number of operations executed",
133
- unit="operations"
139
+ name="hindsight.operation.total", description="Total number of operations executed", unit="operations"
134
140
  )
135
141
 
136
142
  @contextmanager
137
- def record_operation(self, operation: str, bank_id: str, budget: Optional[str] = None, max_tokens: Optional[int] = None):
143
+ def record_operation(self, operation: str, bank_id: str, budget: str | None = None, max_tokens: int | None = None):
138
144
  """
139
145
  Context manager to record operation duration and status.
140
146
 
@@ -175,7 +181,15 @@ class MetricsCollector(MetricsCollectorBase):
175
181
  # Record operation count
176
182
  self.operation_total.add(1, attributes)
177
183
 
178
- def record_tokens(self, operation: str, bank_id: str, input_tokens: int = 0, output_tokens: int = 0, budget: Optional[str] = None, max_tokens: Optional[int] = None):
184
+ def record_tokens(
185
+ self,
186
+ operation: str,
187
+ bank_id: str,
188
+ input_tokens: int = 0,
189
+ output_tokens: int = 0,
190
+ budget: str | None = None,
191
+ max_tokens: int | None = None,
192
+ ):
179
193
  """
180
194
  Record token usage for an operation.
181
195
 
@@ -11,11 +11,10 @@ safe rolling deployments.
11
11
 
12
12
  No alembic.ini required - all configuration is done programmatically.
13
13
  """
14
+
14
15
  import logging
15
16
  import os
16
- import shutil
17
17
  from pathlib import Path
18
- from typing import Optional
19
18
 
20
19
  from alembic import command
21
20
  from alembic.config import Config
@@ -31,7 +30,7 @@ def _run_migrations_internal(database_url: str, script_location: str) -> None:
31
30
  """
32
31
  Internal function to run migrations without locking.
33
32
  """
34
- logger.info(f"Running database migrations to head...")
33
+ logger.info("Running database migrations to head...")
35
34
  logger.info(f"Database URL: {database_url}")
36
35
  logger.info(f"Script location: {script_location}")
37
36
 
@@ -57,7 +56,7 @@ def _run_migrations_internal(database_url: str, script_location: str) -> None:
57
56
  logger.info("Database migrations completed successfully")
58
57
 
59
58
 
60
- def run_migrations(database_url: str, script_location: Optional[str] = None) -> None:
59
+ def run_migrations(database_url: str, script_location: str | None = None) -> None:
61
60
  """
62
61
  Run database migrations to the latest version using programmatic Alembic configuration.
63
62
 
@@ -97,8 +96,7 @@ def run_migrations(database_url: str, script_location: Optional[str] = None) ->
97
96
  script_path = Path(script_location)
98
97
  if not script_path.exists():
99
98
  raise FileNotFoundError(
100
- f"Alembic script location not found at {script_location}. "
101
- "Database migrations cannot be run."
99
+ f"Alembic script location not found at {script_location}. Database migrations cannot be run."
102
100
  )
103
101
 
104
102
  # Use PostgreSQL advisory lock to coordinate between distributed workers
@@ -130,7 +128,9 @@ def run_migrations(database_url: str, script_location: Optional[str] = None) ->
130
128
  raise RuntimeError("Database migration failed") from e
131
129
 
132
130
 
133
- def check_migration_status(database_url: Optional[str] = None, script_location: Optional[str] = None) -> tuple[str | None, str | None]:
131
+ def check_migration_status(
132
+ database_url: str | None = None, script_location: str | None = None
133
+ ) -> tuple[str | None, str | None]:
134
134
  """
135
135
  Check current database schema version and latest available version.
136
136
 
@@ -151,7 +151,9 @@ def check_migration_status(database_url: Optional[str] = None, script_location:
151
151
  if database_url is None:
152
152
  database_url = os.getenv("HINDSIGHT_API_DATABASE_URL")
153
153
  if not database_url:
154
- logger.warning("Database URL not provided and HINDSIGHT_API_DATABASE_URL not set, cannot check migration status")
154
+ logger.warning(
155
+ "Database URL not provided and HINDSIGHT_API_DATABASE_URL not set, cannot check migration status"
156
+ )
155
157
  return None, None
156
158
 
157
159
  # Get current revision from database