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.
- hindsight_api/admin/cli.py +59 -0
- hindsight_api/alembic/versions/h3c4d5e6f7g8_mental_models_v4.py +112 -0
- hindsight_api/alembic/versions/i4d5e6f7g8h9_delete_opinions.py +41 -0
- hindsight_api/alembic/versions/j5e6f7g8h9i0_mental_model_versions.py +95 -0
- hindsight_api/alembic/versions/k6f7g8h9i0j1_add_directive_subtype.py +58 -0
- hindsight_api/alembic/versions/l7g8h9i0j1k2_add_worker_columns.py +109 -0
- hindsight_api/alembic/versions/m8h9i0j1k2l3_mental_model_id_to_text.py +41 -0
- hindsight_api/alembic/versions/n9i0j1k2l3m4_learnings_and_pinned_reflections.py +134 -0
- hindsight_api/alembic/versions/o0j1k2l3m4n5_migrate_mental_models_data.py +113 -0
- hindsight_api/alembic/versions/p1k2l3m4n5o6_new_knowledge_architecture.py +194 -0
- hindsight_api/alembic/versions/q2l3m4n5o6p7_fix_mental_model_fact_type.py +50 -0
- hindsight_api/alembic/versions/r3m4n5o6p7q8_add_reflect_response_to_reflections.py +47 -0
- hindsight_api/alembic/versions/s4n5o6p7q8r9_add_consolidated_at_to_memory_units.py +53 -0
- hindsight_api/alembic/versions/t5o6p7q8r9s0_rename_mental_models_to_observations.py +134 -0
- hindsight_api/alembic/versions/u6p7q8r9s0t1_mental_models_text_id.py +41 -0
- hindsight_api/alembic/versions/v7q8r9s0t1u2_add_max_tokens_to_mental_models.py +50 -0
- hindsight_api/api/http.py +1119 -93
- hindsight_api/api/mcp.py +11 -191
- hindsight_api/config.py +145 -45
- hindsight_api/engine/consolidation/__init__.py +5 -0
- hindsight_api/engine/consolidation/consolidator.py +859 -0
- hindsight_api/engine/consolidation/prompts.py +69 -0
- hindsight_api/engine/cross_encoder.py +114 -9
- hindsight_api/engine/directives/__init__.py +5 -0
- hindsight_api/engine/directives/models.py +37 -0
- hindsight_api/engine/embeddings.py +102 -5
- hindsight_api/engine/interface.py +32 -13
- hindsight_api/engine/llm_wrapper.py +505 -43
- hindsight_api/engine/memory_engine.py +2090 -1089
- hindsight_api/engine/mental_models/__init__.py +14 -0
- hindsight_api/engine/mental_models/models.py +53 -0
- hindsight_api/engine/reflect/__init__.py +18 -0
- hindsight_api/engine/reflect/agent.py +933 -0
- hindsight_api/engine/reflect/models.py +109 -0
- hindsight_api/engine/reflect/observations.py +186 -0
- hindsight_api/engine/reflect/prompts.py +483 -0
- hindsight_api/engine/reflect/tools.py +437 -0
- hindsight_api/engine/reflect/tools_schema.py +250 -0
- hindsight_api/engine/response_models.py +130 -4
- hindsight_api/engine/retain/bank_utils.py +79 -201
- hindsight_api/engine/retain/fact_extraction.py +81 -48
- hindsight_api/engine/retain/fact_storage.py +5 -8
- hindsight_api/engine/retain/link_utils.py +5 -8
- hindsight_api/engine/retain/orchestrator.py +1 -55
- hindsight_api/engine/retain/types.py +2 -2
- hindsight_api/engine/search/graph_retrieval.py +2 -2
- hindsight_api/engine/search/link_expansion_retrieval.py +164 -29
- hindsight_api/engine/search/mpfp_retrieval.py +1 -1
- hindsight_api/engine/search/retrieval.py +14 -14
- hindsight_api/engine/search/think_utils.py +41 -140
- hindsight_api/engine/search/trace.py +0 -1
- hindsight_api/engine/search/tracer.py +2 -5
- hindsight_api/engine/search/types.py +0 -3
- hindsight_api/engine/task_backend.py +112 -196
- hindsight_api/engine/utils.py +0 -151
- hindsight_api/extensions/__init__.py +10 -1
- hindsight_api/extensions/builtin/tenant.py +5 -1
- hindsight_api/extensions/operation_validator.py +81 -4
- hindsight_api/extensions/tenant.py +26 -0
- hindsight_api/main.py +16 -5
- hindsight_api/mcp_local.py +12 -53
- hindsight_api/mcp_tools.py +494 -0
- hindsight_api/models.py +0 -2
- hindsight_api/worker/__init__.py +11 -0
- hindsight_api/worker/main.py +296 -0
- hindsight_api/worker/poller.py +486 -0
- {hindsight_api-0.3.0.dist-info → hindsight_api-0.4.0.dist-info}/METADATA +12 -6
- hindsight_api-0.4.0.dist-info/RECORD +112 -0
- {hindsight_api-0.3.0.dist-info → hindsight_api-0.4.0.dist-info}/entry_points.txt +1 -0
- hindsight_api/engine/retain/observation_regeneration.py +0 -254
- hindsight_api/engine/search/observation_utils.py +0 -125
- hindsight_api/engine/search/scoring.py +0 -159
- hindsight_api-0.3.0.dist-info/RECORD +0 -82
- {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()
|