hindsight-api 0.3.0__py3-none-any.whl → 0.4.0__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 (74) hide show
  1. hindsight_api/admin/cli.py +59 -0
  2. hindsight_api/alembic/versions/h3c4d5e6f7g8_mental_models_v4.py +112 -0
  3. hindsight_api/alembic/versions/i4d5e6f7g8h9_delete_opinions.py +41 -0
  4. hindsight_api/alembic/versions/j5e6f7g8h9i0_mental_model_versions.py +95 -0
  5. hindsight_api/alembic/versions/k6f7g8h9i0j1_add_directive_subtype.py +58 -0
  6. hindsight_api/alembic/versions/l7g8h9i0j1k2_add_worker_columns.py +109 -0
  7. hindsight_api/alembic/versions/m8h9i0j1k2l3_mental_model_id_to_text.py +41 -0
  8. hindsight_api/alembic/versions/n9i0j1k2l3m4_learnings_and_pinned_reflections.py +134 -0
  9. hindsight_api/alembic/versions/o0j1k2l3m4n5_migrate_mental_models_data.py +113 -0
  10. hindsight_api/alembic/versions/p1k2l3m4n5o6_new_knowledge_architecture.py +194 -0
  11. hindsight_api/alembic/versions/q2l3m4n5o6p7_fix_mental_model_fact_type.py +50 -0
  12. hindsight_api/alembic/versions/r3m4n5o6p7q8_add_reflect_response_to_reflections.py +47 -0
  13. hindsight_api/alembic/versions/s4n5o6p7q8r9_add_consolidated_at_to_memory_units.py +53 -0
  14. hindsight_api/alembic/versions/t5o6p7q8r9s0_rename_mental_models_to_observations.py +134 -0
  15. hindsight_api/alembic/versions/u6p7q8r9s0t1_mental_models_text_id.py +41 -0
  16. hindsight_api/alembic/versions/v7q8r9s0t1u2_add_max_tokens_to_mental_models.py +50 -0
  17. hindsight_api/api/http.py +1119 -93
  18. hindsight_api/api/mcp.py +11 -191
  19. hindsight_api/config.py +145 -45
  20. hindsight_api/engine/consolidation/__init__.py +5 -0
  21. hindsight_api/engine/consolidation/consolidator.py +859 -0
  22. hindsight_api/engine/consolidation/prompts.py +69 -0
  23. hindsight_api/engine/cross_encoder.py +114 -9
  24. hindsight_api/engine/directives/__init__.py +5 -0
  25. hindsight_api/engine/directives/models.py +37 -0
  26. hindsight_api/engine/embeddings.py +102 -5
  27. hindsight_api/engine/interface.py +32 -13
  28. hindsight_api/engine/llm_wrapper.py +505 -43
  29. hindsight_api/engine/memory_engine.py +2090 -1089
  30. hindsight_api/engine/mental_models/__init__.py +14 -0
  31. hindsight_api/engine/mental_models/models.py +53 -0
  32. hindsight_api/engine/reflect/__init__.py +18 -0
  33. hindsight_api/engine/reflect/agent.py +933 -0
  34. hindsight_api/engine/reflect/models.py +109 -0
  35. hindsight_api/engine/reflect/observations.py +186 -0
  36. hindsight_api/engine/reflect/prompts.py +483 -0
  37. hindsight_api/engine/reflect/tools.py +437 -0
  38. hindsight_api/engine/reflect/tools_schema.py +250 -0
  39. hindsight_api/engine/response_models.py +130 -4
  40. hindsight_api/engine/retain/bank_utils.py +79 -201
  41. hindsight_api/engine/retain/fact_extraction.py +81 -48
  42. hindsight_api/engine/retain/fact_storage.py +5 -8
  43. hindsight_api/engine/retain/link_utils.py +5 -8
  44. hindsight_api/engine/retain/orchestrator.py +1 -55
  45. hindsight_api/engine/retain/types.py +2 -2
  46. hindsight_api/engine/search/graph_retrieval.py +2 -2
  47. hindsight_api/engine/search/link_expansion_retrieval.py +164 -29
  48. hindsight_api/engine/search/mpfp_retrieval.py +1 -1
  49. hindsight_api/engine/search/retrieval.py +14 -14
  50. hindsight_api/engine/search/think_utils.py +41 -140
  51. hindsight_api/engine/search/trace.py +0 -1
  52. hindsight_api/engine/search/tracer.py +2 -5
  53. hindsight_api/engine/search/types.py +0 -3
  54. hindsight_api/engine/task_backend.py +112 -196
  55. hindsight_api/engine/utils.py +0 -151
  56. hindsight_api/extensions/__init__.py +10 -1
  57. hindsight_api/extensions/builtin/tenant.py +5 -1
  58. hindsight_api/extensions/operation_validator.py +81 -4
  59. hindsight_api/extensions/tenant.py +26 -0
  60. hindsight_api/main.py +16 -5
  61. hindsight_api/mcp_local.py +12 -53
  62. hindsight_api/mcp_tools.py +494 -0
  63. hindsight_api/models.py +0 -2
  64. hindsight_api/worker/__init__.py +11 -0
  65. hindsight_api/worker/main.py +296 -0
  66. hindsight_api/worker/poller.py +486 -0
  67. {hindsight_api-0.3.0.dist-info → hindsight_api-0.4.0.dist-info}/METADATA +12 -6
  68. hindsight_api-0.4.0.dist-info/RECORD +112 -0
  69. {hindsight_api-0.3.0.dist-info → hindsight_api-0.4.0.dist-info}/entry_points.txt +1 -0
  70. hindsight_api/engine/retain/observation_regeneration.py +0 -254
  71. hindsight_api/engine/search/observation_utils.py +0 -125
  72. hindsight_api/engine/search/scoring.py +0 -159
  73. hindsight_api-0.3.0.dist-info/RECORD +0 -82
  74. {hindsight_api-0.3.0.dist-info → hindsight_api-0.4.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,296 @@
1
+ """
2
+ Command-line interface for Hindsight Worker.
3
+
4
+ Run the worker with:
5
+ hindsight-worker
6
+
7
+ Stop with Ctrl+C (graceful shutdown).
8
+ """
9
+
10
+ import argparse
11
+ import asyncio
12
+ import atexit
13
+ import logging
14
+ import os
15
+ import signal
16
+ import socket
17
+ import sys
18
+ import warnings
19
+
20
+ from ..config import get_config
21
+ from ..engine.task_backend import SyncTaskBackend
22
+ from .poller import WorkerPoller
23
+
24
+ # Filter deprecation warnings from third-party libraries
25
+ warnings.filterwarnings("ignore", message="websockets.legacy is deprecated")
26
+ warnings.filterwarnings("ignore", message="websockets.server.WebSocketServerProtocol is deprecated")
27
+
28
+ # Disable tokenizers parallelism to avoid warnings
29
+ os.environ["TOKENIZERS_PARALLELISM"] = "false"
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ def create_worker_app(poller: WorkerPoller, memory):
35
+ """Create a minimal FastAPI app for worker metrics and health."""
36
+ from fastapi import FastAPI
37
+ from fastapi.responses import JSONResponse, Response
38
+ from prometheus_client import CONTENT_TYPE_LATEST, generate_latest
39
+
40
+ from ..metrics import create_metrics_collector, get_metrics_collector, initialize_metrics
41
+
42
+ app = FastAPI(
43
+ title="Hindsight Worker",
44
+ description="Worker process for distributed task execution",
45
+ )
46
+
47
+ # Initialize OpenTelemetry metrics
48
+ try:
49
+ prometheus_reader = initialize_metrics(service_name="hindsight-worker", service_version="1.0.0")
50
+ create_metrics_collector()
51
+ app.state.prometheus_reader = prometheus_reader
52
+ logger.info("Metrics initialized - available at /metrics endpoint")
53
+ except Exception as e:
54
+ logger.warning(f"Failed to initialize metrics: {e}. Metrics will be disabled.")
55
+ app.state.prometheus_reader = None
56
+
57
+ # Set up DB pool metrics if available
58
+ metrics_collector = get_metrics_collector()
59
+ if memory._pool is not None and hasattr(metrics_collector, "set_db_pool"):
60
+ metrics_collector.set_db_pool(memory._pool)
61
+ logger.info("DB pool metrics configured")
62
+
63
+ @app.get(
64
+ "/health",
65
+ summary="Health check endpoint",
66
+ description="Returns worker health status including database connectivity",
67
+ tags=["Monitoring"],
68
+ )
69
+ async def health_endpoint():
70
+ """Health check endpoint."""
71
+ health = await memory.health_check()
72
+ health["worker_id"] = poller.worker_id
73
+ health["is_shutdown"] = poller.is_shutdown
74
+ status_code = 200 if health.get("status") == "healthy" else 503
75
+ return JSONResponse(content=health, status_code=status_code)
76
+
77
+ @app.get(
78
+ "/metrics",
79
+ summary="Prometheus metrics endpoint",
80
+ description="Exports metrics in Prometheus format for scraping",
81
+ tags=["Monitoring"],
82
+ )
83
+ async def metrics_endpoint():
84
+ """Return Prometheus metrics."""
85
+ metrics_data = generate_latest()
86
+ return Response(content=metrics_data, media_type=CONTENT_TYPE_LATEST)
87
+
88
+ @app.get(
89
+ "/",
90
+ summary="Worker info",
91
+ description="Basic worker information",
92
+ tags=["Info"],
93
+ )
94
+ async def root():
95
+ """Return basic worker info."""
96
+ return {
97
+ "service": "hindsight-worker",
98
+ "worker_id": poller.worker_id,
99
+ "is_shutdown": poller.is_shutdown,
100
+ }
101
+
102
+ return app
103
+
104
+
105
+ def main():
106
+ """Main entry point for the hindsight-worker CLI."""
107
+ # Load configuration from environment
108
+ config = get_config()
109
+
110
+ parser = argparse.ArgumentParser(
111
+ prog="hindsight-worker",
112
+ description="Hindsight Worker - distributed task processor",
113
+ )
114
+
115
+ # Worker options
116
+ parser.add_argument(
117
+ "--worker-id",
118
+ default=config.worker_id or socket.gethostname(),
119
+ help="Worker identifier (default: hostname, env: HINDSIGHT_API_WORKER_ID)",
120
+ )
121
+ parser.add_argument(
122
+ "--poll-interval",
123
+ type=int,
124
+ default=config.worker_poll_interval_ms,
125
+ help=f"Poll interval in milliseconds (default: {config.worker_poll_interval_ms}, env: HINDSIGHT_API_WORKER_POLL_INTERVAL_MS)",
126
+ )
127
+ parser.add_argument(
128
+ "--batch-size",
129
+ type=int,
130
+ default=config.worker_batch_size,
131
+ help=f"Tasks to claim per poll (default: {config.worker_batch_size}, env: HINDSIGHT_API_WORKER_BATCH_SIZE)",
132
+ )
133
+ parser.add_argument(
134
+ "--max-retries",
135
+ type=int,
136
+ default=config.worker_max_retries,
137
+ help=f"Max retries before marking failed (default: {config.worker_max_retries}, env: HINDSIGHT_API_WORKER_MAX_RETRIES)",
138
+ )
139
+
140
+ # HTTP server options
141
+ parser.add_argument(
142
+ "--http-port",
143
+ type=int,
144
+ default=config.worker_http_port,
145
+ help=f"HTTP port for metrics/health endpoints (default: {config.worker_http_port}, env: HINDSIGHT_API_WORKER_HTTP_PORT)",
146
+ )
147
+ parser.add_argument(
148
+ "--http-host",
149
+ default="0.0.0.0",
150
+ help="HTTP host to bind (default: 0.0.0.0)",
151
+ )
152
+
153
+ # Logging options
154
+ parser.add_argument(
155
+ "--log-level",
156
+ default=config.log_level,
157
+ choices=["critical", "error", "warning", "info", "debug", "trace"],
158
+ help=f"Log level (default: {config.log_level}, env: HINDSIGHT_API_LOG_LEVEL)",
159
+ )
160
+
161
+ args = parser.parse_args()
162
+
163
+ # Configure logging
164
+ config.configure_logging()
165
+
166
+ # Import MemoryEngine here to avoid circular imports
167
+ from .. import MemoryEngine
168
+
169
+ print(f"Starting Hindsight Worker: {args.worker_id}")
170
+ print(f" Poll interval: {args.poll_interval}ms")
171
+ print(f" Batch size: {args.batch_size}")
172
+ print(f" Max retries: {args.max_retries}")
173
+ print(f" HTTP server: {args.http_host}:{args.http_port}")
174
+ print()
175
+
176
+ # Global references for cleanup
177
+ memory = None
178
+ poller = None
179
+
180
+ async def run():
181
+ nonlocal memory, poller
182
+ import uvicorn
183
+
184
+ from ..extensions import TenantExtension, load_extension
185
+
186
+ # Initialize MemoryEngine
187
+ # Workers use SyncTaskBackend because they execute tasks directly,
188
+ # they don't need to store tasks (they poll from DB)
189
+ memory = MemoryEngine(
190
+ run_migrations=False, # Workers don't run migrations
191
+ task_backend=SyncTaskBackend(),
192
+ )
193
+
194
+ await memory.initialize()
195
+
196
+ print(f"Database connected: {config.database_url}")
197
+
198
+ # Load tenant extension for dynamic schema discovery
199
+ tenant_extension = load_extension("TENANT", TenantExtension)
200
+
201
+ if tenant_extension:
202
+ print("Tenant extension loaded - schemas will be discovered dynamically on each poll")
203
+ else:
204
+ print("No tenant extension configured, using public schema only")
205
+
206
+ # Create a single poller that handles all schemas dynamically
207
+ poller = WorkerPoller(
208
+ pool=memory._pool,
209
+ worker_id=args.worker_id,
210
+ executor=memory.execute_task,
211
+ poll_interval_ms=args.poll_interval,
212
+ batch_size=args.batch_size,
213
+ max_retries=args.max_retries,
214
+ tenant_extension=tenant_extension,
215
+ )
216
+
217
+ # Create the HTTP app for metrics/health
218
+ app = create_worker_app(poller, memory)
219
+
220
+ # Setup signal handlers for graceful shutdown
221
+ shutdown_requested = asyncio.Event()
222
+
223
+ def signal_handler(signum, frame):
224
+ print(f"\nReceived signal {signum}, initiating graceful shutdown...")
225
+ shutdown_requested.set()
226
+
227
+ signal.signal(signal.SIGINT, signal_handler)
228
+ signal.signal(signal.SIGTERM, signal_handler)
229
+
230
+ # Create uvicorn config and server
231
+ uvicorn_config = uvicorn.Config(
232
+ app,
233
+ host=args.http_host,
234
+ port=args.http_port,
235
+ log_level="info", # Reduce uvicorn noise
236
+ access_log=False,
237
+ )
238
+ server = uvicorn.Server(uvicorn_config)
239
+
240
+ # Run the poller and HTTP server concurrently
241
+ poller_task = asyncio.create_task(poller.run())
242
+ http_task = asyncio.create_task(server.serve())
243
+
244
+ print(f"Worker started. Metrics available at http://{args.http_host}:{args.http_port}/metrics")
245
+
246
+ # Wait for shutdown signal
247
+ await shutdown_requested.wait()
248
+
249
+ # Graceful shutdown
250
+ print("Shutting down HTTP server...")
251
+ server.should_exit = True
252
+
253
+ print("Waiting for poller to finish...")
254
+ await poller.shutdown_graceful(timeout=30.0)
255
+ poller_task.cancel()
256
+ try:
257
+ await poller_task
258
+ except asyncio.CancelledError:
259
+ pass
260
+
261
+ # Wait for HTTP server to finish
262
+ try:
263
+ await asyncio.wait_for(http_task, timeout=5.0)
264
+ except asyncio.TimeoutError:
265
+ http_task.cancel()
266
+ try:
267
+ await http_task
268
+ except asyncio.CancelledError:
269
+ pass
270
+
271
+ # Close memory engine
272
+ await memory.close()
273
+ print("Worker shutdown complete")
274
+
275
+ def cleanup():
276
+ """Synchronous cleanup for atexit."""
277
+ if memory is not None and memory._pg0 is not None:
278
+ try:
279
+ loop = asyncio.new_event_loop()
280
+ loop.run_until_complete(memory._pg0.stop())
281
+ loop.close()
282
+ print("\npg0 stopped.")
283
+ except Exception as e:
284
+ print(f"\nError stopping pg0: {e}")
285
+
286
+ atexit.register(cleanup)
287
+
288
+ try:
289
+ asyncio.run(run())
290
+ except KeyboardInterrupt:
291
+ print("\nWorker interrupted")
292
+ sys.exit(0)
293
+
294
+
295
+ if __name__ == "__main__":
296
+ main()