hindsight-api 0.0.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.
- hindsight_api/__init__.py +38 -0
- hindsight_api/api/__init__.py +105 -0
- hindsight_api/api/http.py +1872 -0
- hindsight_api/api/mcp.py +157 -0
- hindsight_api/engine/__init__.py +47 -0
- hindsight_api/engine/cross_encoder.py +97 -0
- hindsight_api/engine/db_utils.py +93 -0
- hindsight_api/engine/embeddings.py +113 -0
- hindsight_api/engine/entity_resolver.py +575 -0
- hindsight_api/engine/llm_wrapper.py +269 -0
- hindsight_api/engine/memory_engine.py +3095 -0
- hindsight_api/engine/query_analyzer.py +519 -0
- hindsight_api/engine/response_models.py +222 -0
- hindsight_api/engine/retain/__init__.py +50 -0
- hindsight_api/engine/retain/bank_utils.py +423 -0
- hindsight_api/engine/retain/chunk_storage.py +82 -0
- hindsight_api/engine/retain/deduplication.py +104 -0
- hindsight_api/engine/retain/embedding_processing.py +62 -0
- hindsight_api/engine/retain/embedding_utils.py +54 -0
- hindsight_api/engine/retain/entity_processing.py +90 -0
- hindsight_api/engine/retain/fact_extraction.py +1027 -0
- hindsight_api/engine/retain/fact_storage.py +176 -0
- hindsight_api/engine/retain/link_creation.py +121 -0
- hindsight_api/engine/retain/link_utils.py +651 -0
- hindsight_api/engine/retain/orchestrator.py +405 -0
- hindsight_api/engine/retain/types.py +206 -0
- hindsight_api/engine/search/__init__.py +15 -0
- hindsight_api/engine/search/fusion.py +122 -0
- hindsight_api/engine/search/observation_utils.py +132 -0
- hindsight_api/engine/search/reranking.py +103 -0
- hindsight_api/engine/search/retrieval.py +503 -0
- hindsight_api/engine/search/scoring.py +161 -0
- hindsight_api/engine/search/temporal_extraction.py +64 -0
- hindsight_api/engine/search/think_utils.py +255 -0
- hindsight_api/engine/search/trace.py +215 -0
- hindsight_api/engine/search/tracer.py +447 -0
- hindsight_api/engine/search/types.py +160 -0
- hindsight_api/engine/task_backend.py +223 -0
- hindsight_api/engine/utils.py +203 -0
- hindsight_api/metrics.py +227 -0
- hindsight_api/migrations.py +163 -0
- hindsight_api/models.py +309 -0
- hindsight_api/pg0.py +425 -0
- hindsight_api/web/__init__.py +12 -0
- hindsight_api/web/server.py +143 -0
- hindsight_api-0.0.13.dist-info/METADATA +41 -0
- hindsight_api-0.0.13.dist-info/RECORD +48 -0
- hindsight_api-0.0.13.dist-info/WHEEL +4 -0
hindsight_api/pg0.py
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import platform
|
|
6
|
+
import re
|
|
7
|
+
import shutil
|
|
8
|
+
import stat
|
|
9
|
+
import subprocess
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
# pg0 configuration
|
|
18
|
+
BINARY_NAME = "pg0"
|
|
19
|
+
DEFAULT_PORT = 5555
|
|
20
|
+
DEFAULT_USERNAME = "hindsight"
|
|
21
|
+
DEFAULT_PASSWORD = "hindsight"
|
|
22
|
+
DEFAULT_DATABASE = "hindsight"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_platform_binary_name() -> str:
|
|
26
|
+
"""Get the appropriate binary name for the current platform.
|
|
27
|
+
|
|
28
|
+
Supported platforms:
|
|
29
|
+
- macOS ARM64 (darwin-aarch64)
|
|
30
|
+
- Linux x86_64 (gnu)
|
|
31
|
+
- Linux ARM64 (gnu)
|
|
32
|
+
- Windows x86_64
|
|
33
|
+
"""
|
|
34
|
+
system = platform.system().lower()
|
|
35
|
+
machine = platform.machine().lower()
|
|
36
|
+
|
|
37
|
+
# Normalize architecture names
|
|
38
|
+
if machine in ("x86_64", "amd64"):
|
|
39
|
+
arch = "x86_64"
|
|
40
|
+
elif machine in ("arm64", "aarch64"):
|
|
41
|
+
arch = "aarch64"
|
|
42
|
+
else:
|
|
43
|
+
raise RuntimeError(
|
|
44
|
+
f"Embedded PostgreSQL is not supported on architecture: {machine}. "
|
|
45
|
+
f"Supported architectures: x86_64/amd64 (Linux, Windows), aarch64/arm64 (macOS, Linux)"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
if system == "darwin" and arch == "aarch64":
|
|
49
|
+
return "pg0-darwin-aarch64"
|
|
50
|
+
elif system == "linux" and arch == "x86_64":
|
|
51
|
+
return "pg0-linux-x86_64-gnu"
|
|
52
|
+
elif system == "linux" and arch == "aarch64":
|
|
53
|
+
return "pg0-linux-aarch64-gnu"
|
|
54
|
+
elif system == "windows" and arch == "x86_64":
|
|
55
|
+
return "pg0-windows-x86_64.exe"
|
|
56
|
+
else:
|
|
57
|
+
raise RuntimeError(
|
|
58
|
+
f"Embedded PostgreSQL is not supported on {system}-{arch}. "
|
|
59
|
+
f"Supported platforms: darwin-aarch64 (macOS ARM), linux-x86_64-gnu, linux-aarch64-gnu, windows-x86_64"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_download_url(
|
|
64
|
+
version: str = "latest",
|
|
65
|
+
repo: str = "vectorize-io/pg0",
|
|
66
|
+
) -> str:
|
|
67
|
+
"""Get the download URL for pg0 binary."""
|
|
68
|
+
binary_name = get_platform_binary_name()
|
|
69
|
+
|
|
70
|
+
if version == "latest":
|
|
71
|
+
return f"https://github.com/{repo}/releases/latest/download/{binary_name}"
|
|
72
|
+
else:
|
|
73
|
+
return f"https://github.com/{repo}/releases/download/{version}/{binary_name}"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _find_pg0_binary() -> Optional[Path]:
|
|
77
|
+
"""Find pg0 binary in PATH or default install location."""
|
|
78
|
+
# First check PATH
|
|
79
|
+
pg0_in_path = shutil.which("pg0")
|
|
80
|
+
if pg0_in_path:
|
|
81
|
+
return Path(pg0_in_path)
|
|
82
|
+
|
|
83
|
+
# Fall back to default install location
|
|
84
|
+
default_path = Path.home() / ".hindsight" / "bin" / "pg0"
|
|
85
|
+
if default_path.exists() and os.access(default_path, os.X_OK):
|
|
86
|
+
return default_path
|
|
87
|
+
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class EmbeddedPostgres:
|
|
92
|
+
"""
|
|
93
|
+
Manages an embedded PostgreSQL server instance using pg0.
|
|
94
|
+
|
|
95
|
+
This class handles:
|
|
96
|
+
- Finding or downloading the pg0 CLI
|
|
97
|
+
- Starting/stopping the PostgreSQL server
|
|
98
|
+
- Getting the connection URI
|
|
99
|
+
|
|
100
|
+
Example:
|
|
101
|
+
pg = EmbeddedPostgres()
|
|
102
|
+
await pg.ensure_installed()
|
|
103
|
+
await pg.start()
|
|
104
|
+
uri = await pg.get_uri()
|
|
105
|
+
# ... use uri with asyncpg ...
|
|
106
|
+
await pg.stop()
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
def __init__(
|
|
110
|
+
self,
|
|
111
|
+
version: str = "latest",
|
|
112
|
+
port: int = DEFAULT_PORT,
|
|
113
|
+
username: str = DEFAULT_USERNAME,
|
|
114
|
+
password: str = DEFAULT_PASSWORD,
|
|
115
|
+
database: str = DEFAULT_DATABASE,
|
|
116
|
+
name: str = "hindsight",
|
|
117
|
+
):
|
|
118
|
+
"""
|
|
119
|
+
Initialize the embedded PostgreSQL manager.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
version: Version of pg0 to download if not found. Defaults to "latest"
|
|
123
|
+
port: Port to listen on. Defaults to 5555
|
|
124
|
+
username: Username for the database. Defaults to "hindsight"
|
|
125
|
+
password: Password for the database. Defaults to "hindsight"
|
|
126
|
+
database: Database name to create. Defaults to "hindsight"
|
|
127
|
+
name: Instance name for pg0. Defaults to "hindsight"
|
|
128
|
+
"""
|
|
129
|
+
self.version = version
|
|
130
|
+
self.port = port
|
|
131
|
+
self.username = username
|
|
132
|
+
self.password = password
|
|
133
|
+
self.database = database
|
|
134
|
+
self.name = name
|
|
135
|
+
|
|
136
|
+
# Will be set when binary is found/installed
|
|
137
|
+
self._binary_path: Optional[Path] = _find_pg0_binary()
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def binary_path(self) -> Path:
|
|
141
|
+
"""Get the path to the pg0 binary."""
|
|
142
|
+
if self._binary_path is None:
|
|
143
|
+
# Default install location
|
|
144
|
+
return Path.home() / ".hindsight" / "bin" / "pg0"
|
|
145
|
+
return self._binary_path
|
|
146
|
+
|
|
147
|
+
def is_installed(self) -> bool:
|
|
148
|
+
"""Check if pg0 is available (in PATH or installed)."""
|
|
149
|
+
self._binary_path = _find_pg0_binary()
|
|
150
|
+
return self._binary_path is not None
|
|
151
|
+
|
|
152
|
+
async def ensure_installed(self) -> None:
|
|
153
|
+
"""
|
|
154
|
+
Ensure pg0 is available.
|
|
155
|
+
|
|
156
|
+
First checks PATH, then default location, then downloads if needed.
|
|
157
|
+
"""
|
|
158
|
+
if self.is_installed():
|
|
159
|
+
logger.debug(f"pg0 found at {self._binary_path}")
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
logger.info("pg0 not found, downloading...")
|
|
163
|
+
|
|
164
|
+
# Log platform information
|
|
165
|
+
binary_name = get_platform_binary_name()
|
|
166
|
+
logger.info(f"Detected platform: system={platform.system()}, machine={platform.machine()}")
|
|
167
|
+
|
|
168
|
+
# Install to default location
|
|
169
|
+
install_dir = Path.home() / ".hindsight" / "bin"
|
|
170
|
+
install_dir.mkdir(parents=True, exist_ok=True)
|
|
171
|
+
install_path = install_dir / "pg0"
|
|
172
|
+
|
|
173
|
+
# Download the binary
|
|
174
|
+
download_url = get_download_url(self.version)
|
|
175
|
+
logger.info(f"Downloading from {download_url}")
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
async with httpx.AsyncClient(follow_redirects=True, timeout=300.0) as client:
|
|
179
|
+
response = await client.get(download_url)
|
|
180
|
+
response.raise_for_status()
|
|
181
|
+
|
|
182
|
+
# Write binary to disk
|
|
183
|
+
with open(install_path, "wb") as f:
|
|
184
|
+
f.write(response.content)
|
|
185
|
+
|
|
186
|
+
# Make executable on Unix
|
|
187
|
+
if platform.system() != "Windows":
|
|
188
|
+
st = os.stat(install_path)
|
|
189
|
+
os.chmod(install_path, st.st_mode | stat.S_IEXEC)
|
|
190
|
+
|
|
191
|
+
self._binary_path = install_path
|
|
192
|
+
logger.info(f"Installed pg0 to {install_path}")
|
|
193
|
+
|
|
194
|
+
except httpx.HTTPError as e:
|
|
195
|
+
raise RuntimeError(f"Failed to download pg0: {e}") from e
|
|
196
|
+
|
|
197
|
+
def _run_command(self, *args: str, capture_output: bool = True) -> subprocess.CompletedProcess:
|
|
198
|
+
"""Run a pg0 command synchronously."""
|
|
199
|
+
cmd = [str(self.binary_path), *args]
|
|
200
|
+
return subprocess.run(cmd, capture_output=capture_output, text=True)
|
|
201
|
+
|
|
202
|
+
async def _run_command_async(self, *args: str, timeout: int = 120) -> tuple[int, str, str]:
|
|
203
|
+
"""Run a pg0 command asynchronously."""
|
|
204
|
+
cmd = [str(self.binary_path), *args]
|
|
205
|
+
|
|
206
|
+
def run_sync():
|
|
207
|
+
try:
|
|
208
|
+
result = subprocess.run(
|
|
209
|
+
cmd,
|
|
210
|
+
stdin=subprocess.DEVNULL,
|
|
211
|
+
stdout=subprocess.PIPE,
|
|
212
|
+
stderr=subprocess.PIPE,
|
|
213
|
+
text=True,
|
|
214
|
+
timeout=timeout,
|
|
215
|
+
)
|
|
216
|
+
return result.returncode, result.stdout, result.stderr
|
|
217
|
+
except subprocess.TimeoutExpired:
|
|
218
|
+
return 1, "", "Command timed out"
|
|
219
|
+
|
|
220
|
+
loop = asyncio.get_event_loop()
|
|
221
|
+
return await loop.run_in_executor(None, run_sync)
|
|
222
|
+
|
|
223
|
+
def _extract_uri_from_output(self, output: str) -> Optional[str]:
|
|
224
|
+
"""Extract the PostgreSQL URI from pg0 start output."""
|
|
225
|
+
match = re.search(r"Connection URI:\s*(postgresql://[^\s]+)", output)
|
|
226
|
+
if match:
|
|
227
|
+
return match.group(1)
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
async def start(self, max_retries: int = 3, retry_delay: float = 2.0) -> str:
|
|
231
|
+
"""
|
|
232
|
+
Start the PostgreSQL server with retry logic.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
max_retries: Maximum number of start attempts (default: 3)
|
|
236
|
+
retry_delay: Initial delay between retries in seconds (default: 2.0)
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
The connection URI for the started server.
|
|
240
|
+
|
|
241
|
+
Raises:
|
|
242
|
+
RuntimeError: If the server fails to start after all retries.
|
|
243
|
+
"""
|
|
244
|
+
if not self.is_installed():
|
|
245
|
+
raise RuntimeError("pg0 is not installed. Call ensure_installed() first.")
|
|
246
|
+
|
|
247
|
+
logger.info(f"Starting embedded PostgreSQL (name: {self.name}, port: {self.port})...")
|
|
248
|
+
|
|
249
|
+
last_error = None
|
|
250
|
+
for attempt in range(1, max_retries + 1):
|
|
251
|
+
returncode, stdout, stderr = await self._run_command_async(
|
|
252
|
+
"start",
|
|
253
|
+
"--name", self.name,
|
|
254
|
+
"--port", str(self.port),
|
|
255
|
+
"--username", self.username,
|
|
256
|
+
"--password", self.password,
|
|
257
|
+
"--database", self.database,
|
|
258
|
+
timeout=300,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# Try to extract URI from output
|
|
262
|
+
uri = self._extract_uri_from_output(stdout)
|
|
263
|
+
if uri:
|
|
264
|
+
logger.info(f"PostgreSQL started on port {self.port}")
|
|
265
|
+
return uri
|
|
266
|
+
|
|
267
|
+
# Check if pg0 info can find the running instance
|
|
268
|
+
try:
|
|
269
|
+
uri = await self.get_uri()
|
|
270
|
+
logger.info(f"PostgreSQL started on port {self.port}")
|
|
271
|
+
return uri
|
|
272
|
+
except RuntimeError:
|
|
273
|
+
pass
|
|
274
|
+
|
|
275
|
+
# Start failed, log and retry
|
|
276
|
+
last_error = stderr or f"pg0 start returned exit code {returncode}"
|
|
277
|
+
if attempt < max_retries:
|
|
278
|
+
delay = retry_delay * (2 ** (attempt - 1))
|
|
279
|
+
logger.warning(f"pg0 start attempt {attempt}/{max_retries} failed: {last_error.strip()}")
|
|
280
|
+
logger.info(f"Retrying in {delay:.1f}s...")
|
|
281
|
+
await asyncio.sleep(delay)
|
|
282
|
+
else:
|
|
283
|
+
logger.warning(f"pg0 start attempt {attempt}/{max_retries} failed: {last_error.strip()}")
|
|
284
|
+
|
|
285
|
+
# All retries exhausted - use constructed URI as fallback
|
|
286
|
+
uri = f"postgresql://{self.username}:{self.password}@localhost:{self.port}/{self.database}"
|
|
287
|
+
logger.warning(f"All pg0 start attempts failed, using constructed URI: {uri}")
|
|
288
|
+
return uri
|
|
289
|
+
|
|
290
|
+
async def stop(self) -> None:
|
|
291
|
+
"""Stop the PostgreSQL server."""
|
|
292
|
+
if not self.is_installed():
|
|
293
|
+
return
|
|
294
|
+
|
|
295
|
+
logger.info(f"Stopping embedded PostgreSQL (name: {self.name})...")
|
|
296
|
+
|
|
297
|
+
returncode, stdout, stderr = await self._run_command_async("stop", "--name", self.name)
|
|
298
|
+
|
|
299
|
+
if returncode != 0:
|
|
300
|
+
if "not running" in stderr.lower():
|
|
301
|
+
return
|
|
302
|
+
raise RuntimeError(f"Failed to stop PostgreSQL: {stderr}")
|
|
303
|
+
|
|
304
|
+
logger.info("Embedded PostgreSQL stopped")
|
|
305
|
+
|
|
306
|
+
async def _get_info(self) -> dict:
|
|
307
|
+
"""Get info from pg0 using the `info -o json` command."""
|
|
308
|
+
if not self.is_installed():
|
|
309
|
+
raise RuntimeError("pg0 is not installed.")
|
|
310
|
+
|
|
311
|
+
returncode, stdout, stderr = await self._run_command_async(
|
|
312
|
+
"info", "--name", self.name, "-o", "json"
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
if returncode != 0:
|
|
316
|
+
raise RuntimeError(f"Failed to get PostgreSQL info: {stderr}")
|
|
317
|
+
|
|
318
|
+
try:
|
|
319
|
+
return json.loads(stdout.strip())
|
|
320
|
+
except json.JSONDecodeError as e:
|
|
321
|
+
raise RuntimeError(f"Failed to parse pg0 info output: {e}")
|
|
322
|
+
|
|
323
|
+
async def get_uri(self) -> str:
|
|
324
|
+
"""Get the connection URI for the PostgreSQL server."""
|
|
325
|
+
info = await self._get_info()
|
|
326
|
+
uri = info.get("uri")
|
|
327
|
+
if not uri:
|
|
328
|
+
raise RuntimeError("PostgreSQL server is not running or URI not available")
|
|
329
|
+
return uri
|
|
330
|
+
|
|
331
|
+
async def status(self) -> dict:
|
|
332
|
+
"""Get the status of the PostgreSQL server."""
|
|
333
|
+
if not self.is_installed():
|
|
334
|
+
return {"installed": False, "running": False}
|
|
335
|
+
|
|
336
|
+
try:
|
|
337
|
+
info = await self._get_info()
|
|
338
|
+
return {
|
|
339
|
+
"installed": True,
|
|
340
|
+
"running": info.get("running", False),
|
|
341
|
+
"uri": info.get("uri"),
|
|
342
|
+
}
|
|
343
|
+
except RuntimeError:
|
|
344
|
+
return {"installed": True, "running": False}
|
|
345
|
+
|
|
346
|
+
async def is_running(self) -> bool:
|
|
347
|
+
"""Check if the PostgreSQL server is currently running."""
|
|
348
|
+
if not self.is_installed():
|
|
349
|
+
return False
|
|
350
|
+
try:
|
|
351
|
+
info = await self._get_info()
|
|
352
|
+
return info.get("running", False)
|
|
353
|
+
except RuntimeError:
|
|
354
|
+
return False
|
|
355
|
+
|
|
356
|
+
async def ensure_running(self) -> str:
|
|
357
|
+
"""
|
|
358
|
+
Ensure the PostgreSQL server is running.
|
|
359
|
+
|
|
360
|
+
Installs if needed, starts if not running.
|
|
361
|
+
|
|
362
|
+
Returns:
|
|
363
|
+
The connection URI.
|
|
364
|
+
"""
|
|
365
|
+
await self.ensure_installed()
|
|
366
|
+
|
|
367
|
+
if await self.is_running():
|
|
368
|
+
return await self.get_uri()
|
|
369
|
+
|
|
370
|
+
return await self.start()
|
|
371
|
+
|
|
372
|
+
def uninstall(self) -> None:
|
|
373
|
+
"""Remove the pg0 binary (only if we installed it)."""
|
|
374
|
+
default_path = Path.home() / ".hindsight" / "bin" / "pg0"
|
|
375
|
+
if default_path.exists():
|
|
376
|
+
default_path.unlink()
|
|
377
|
+
logger.info(f"Removed {default_path}")
|
|
378
|
+
|
|
379
|
+
def clear_data(self) -> None:
|
|
380
|
+
"""Remove all PostgreSQL data (destructive!)."""
|
|
381
|
+
result = self._run_command("drop", "--name", self.name, "--force")
|
|
382
|
+
if result.returncode == 0:
|
|
383
|
+
logger.info(f"Dropped pg0 instance {self.name}")
|
|
384
|
+
else:
|
|
385
|
+
logger.warning(f"Failed to drop pg0 instance {self.name}: {result.stderr}")
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
# Convenience functions
|
|
389
|
+
|
|
390
|
+
_default_instance: Optional[EmbeddedPostgres] = None
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def get_embedded_postgres() -> EmbeddedPostgres:
|
|
394
|
+
"""Get or create the default EmbeddedPostgres instance."""
|
|
395
|
+
global _default_instance
|
|
396
|
+
|
|
397
|
+
if _default_instance is None:
|
|
398
|
+
_default_instance = EmbeddedPostgres()
|
|
399
|
+
|
|
400
|
+
return _default_instance
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
async def start_embedded_postgres() -> str:
|
|
404
|
+
"""
|
|
405
|
+
Quick start function for embedded PostgreSQL.
|
|
406
|
+
|
|
407
|
+
Downloads, installs, and starts PostgreSQL in one call.
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
Connection URI string
|
|
411
|
+
|
|
412
|
+
Example:
|
|
413
|
+
db_url = await start_embedded_postgres()
|
|
414
|
+
conn = await asyncpg.connect(db_url)
|
|
415
|
+
"""
|
|
416
|
+
pg = get_embedded_postgres()
|
|
417
|
+
return await pg.ensure_running()
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
async def stop_embedded_postgres() -> None:
|
|
421
|
+
"""Stop the default embedded PostgreSQL instance."""
|
|
422
|
+
global _default_instance
|
|
423
|
+
|
|
424
|
+
if _default_instance:
|
|
425
|
+
await _default_instance.stop()
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Web interface for memory system.
|
|
3
|
+
|
|
4
|
+
Provides FastAPI app and visualization interface.
|
|
5
|
+
"""
|
|
6
|
+
from hindsight_api.api import create_app
|
|
7
|
+
|
|
8
|
+
# Note: Don't import app from .server here to avoid circular import warnings
|
|
9
|
+
# when running with `python -m hindsight_api.web.server`
|
|
10
|
+
# If you need the app, import it directly: from hindsight_api.web.server import app
|
|
11
|
+
|
|
12
|
+
__all__ = ["create_app"]
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FastAPI server for memory graph visualization and API.
|
|
3
|
+
|
|
4
|
+
Provides REST API endpoints for memory operations and serves
|
|
5
|
+
the interactive visualization interface.
|
|
6
|
+
"""
|
|
7
|
+
import warnings
|
|
8
|
+
|
|
9
|
+
# Filter deprecation warnings from third-party libraries
|
|
10
|
+
warnings.filterwarnings("ignore", message="websockets.legacy is deprecated")
|
|
11
|
+
warnings.filterwarnings("ignore", message="websockets.server.WebSocketServerProtocol is deprecated")
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
import atexit
|
|
15
|
+
import logging
|
|
16
|
+
import os
|
|
17
|
+
import argparse
|
|
18
|
+
import signal
|
|
19
|
+
import sys
|
|
20
|
+
|
|
21
|
+
from hindsight_api import MemoryEngine
|
|
22
|
+
from hindsight_api.api import create_app
|
|
23
|
+
|
|
24
|
+
# Disable tokenizers parallelism to avoid warnings
|
|
25
|
+
os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _cleanup_pg0():
|
|
29
|
+
"""Synchronous cleanup function to stop pg0 on exit."""
|
|
30
|
+
global _memory
|
|
31
|
+
if _memory is not None and _memory._pg0 is not None:
|
|
32
|
+
try:
|
|
33
|
+
# Run async stop in a new event loop
|
|
34
|
+
loop = asyncio.new_event_loop()
|
|
35
|
+
loop.run_until_complete(_memory._pg0.stop())
|
|
36
|
+
loop.close()
|
|
37
|
+
print("\npg0 stopped.")
|
|
38
|
+
except Exception as e:
|
|
39
|
+
print(f"\nError stopping pg0: {e}")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# Register cleanup on normal exit
|
|
43
|
+
atexit.register(_cleanup_pg0)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _signal_handler(signum, frame):
|
|
47
|
+
"""Handle SIGINT/SIGTERM to ensure pg0 cleanup."""
|
|
48
|
+
print(f"\nReceived signal {signum}, shutting down...")
|
|
49
|
+
_cleanup_pg0()
|
|
50
|
+
sys.exit(0)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# Register signal handlers for graceful shutdown
|
|
54
|
+
signal.signal(signal.SIGINT, _signal_handler)
|
|
55
|
+
signal.signal(signal.SIGTERM, _signal_handler)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# Create app at module level (required for uvicorn import string)
|
|
59
|
+
_memory = MemoryEngine(
|
|
60
|
+
db_url=os.getenv("HINDSIGHT_API_DATABASE_URL", "pg0"),
|
|
61
|
+
memory_llm_provider=os.getenv("HINDSIGHT_API_LLM_PROVIDER", "groq"),
|
|
62
|
+
memory_llm_api_key=os.getenv("HINDSIGHT_API_LLM_API_KEY"),
|
|
63
|
+
memory_llm_model=os.getenv("HINDSIGHT_API_LLM_MODEL", "openai/gpt-oss-120b"),
|
|
64
|
+
memory_llm_base_url=os.getenv("HINDSIGHT_API_LLM_BASE_URL") or None,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Check if MCP should be enabled
|
|
68
|
+
mcp_enabled = os.getenv("HINDSIGHT_API_MCP_ENABLED", "true").lower() == "true"
|
|
69
|
+
|
|
70
|
+
# Create unified app with both HTTP and optionally MCP
|
|
71
|
+
app = create_app(
|
|
72
|
+
memory=_memory,
|
|
73
|
+
http_api_enabled=True,
|
|
74
|
+
mcp_api_enabled=mcp_enabled,
|
|
75
|
+
mcp_mount_path="/mcp"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
if __name__ == "__main__":
|
|
80
|
+
import uvicorn
|
|
81
|
+
|
|
82
|
+
# Get log level from environment variable (default: info)
|
|
83
|
+
env_log_level = os.environ.get("HINDSIGHT_API_LOG_LEVEL", "info").lower()
|
|
84
|
+
if env_log_level not in ["critical", "error", "warning", "info", "debug", "trace"]:
|
|
85
|
+
env_log_level = "info"
|
|
86
|
+
|
|
87
|
+
# Parse CLI arguments
|
|
88
|
+
parser = argparse.ArgumentParser(description="Memory Graph API Server")
|
|
89
|
+
parser.add_argument("--host", default="0.0.0.0", help="Host to bind to (default: 0.0.0.0)")
|
|
90
|
+
parser.add_argument("--port", type=int, default=8888, help="Port to bind to (default: 8888)")
|
|
91
|
+
parser.add_argument("--reload", action="store_true", help="Enable auto-reload on code changes")
|
|
92
|
+
parser.add_argument("--workers", type=int, default=1, help="Number of worker processes (default: 1)")
|
|
93
|
+
parser.add_argument("--log-level", default=env_log_level, choices=["critical", "error", "warning", "info", "debug", "trace"],
|
|
94
|
+
help=f"Log level (default: {env_log_level}, from HINDSIGHT_API_LOG_LEVEL)")
|
|
95
|
+
parser.add_argument("--access-log", action="store_true", help="Enable access log")
|
|
96
|
+
parser.add_argument("--no-access-log", dest="access_log", action="store_false", help="Disable access log")
|
|
97
|
+
parser.add_argument("--proxy-headers", action="store_true", help="Enable X-Forwarded-Proto, X-Forwarded-For headers")
|
|
98
|
+
parser.add_argument("--forwarded-allow-ips", default=None, help="Comma separated list of IPs to trust with proxy headers")
|
|
99
|
+
parser.add_argument("--ssl-keyfile", default=None, help="SSL key file")
|
|
100
|
+
parser.add_argument("--ssl-certfile", default=None, help="SSL certificate file")
|
|
101
|
+
parser.set_defaults(access_log=False)
|
|
102
|
+
|
|
103
|
+
args = parser.parse_args()
|
|
104
|
+
|
|
105
|
+
# Configure Python logging based on log level
|
|
106
|
+
log_level_map = {
|
|
107
|
+
"critical": logging.CRITICAL,
|
|
108
|
+
"error": logging.ERROR,
|
|
109
|
+
"warning": logging.WARNING,
|
|
110
|
+
"info": logging.INFO,
|
|
111
|
+
"debug": logging.DEBUG,
|
|
112
|
+
"trace": logging.DEBUG, # Python doesn't have TRACE, use DEBUG
|
|
113
|
+
}
|
|
114
|
+
logging.basicConfig(
|
|
115
|
+
level=log_level_map.get(args.log_level, logging.INFO),
|
|
116
|
+
format="%(asctime)s - %(levelname)s - %(name)s - %(message)s"
|
|
117
|
+
)
|
|
118
|
+
logging.info(f"Starting Hindsight API on {args.host}:{args.port}")
|
|
119
|
+
|
|
120
|
+
app_ref = "hindsight_api.web.server:app"
|
|
121
|
+
|
|
122
|
+
# Prepare uvicorn config
|
|
123
|
+
uvicorn_config = {
|
|
124
|
+
"app": app_ref,
|
|
125
|
+
"host": args.host,
|
|
126
|
+
"port": args.port,
|
|
127
|
+
"reload": args.reload,
|
|
128
|
+
"workers": args.workers,
|
|
129
|
+
"log_level": args.log_level,
|
|
130
|
+
"access_log": args.access_log,
|
|
131
|
+
"proxy_headers": args.proxy_headers,
|
|
132
|
+
"ws": "wsproto", # Use wsproto instead of websockets to avoid deprecation warnings
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# Add optional parameters if provided
|
|
136
|
+
if args.forwarded_allow_ips:
|
|
137
|
+
uvicorn_config["forwarded_allow_ips"] = args.forwarded_allow_ips
|
|
138
|
+
if args.ssl_keyfile:
|
|
139
|
+
uvicorn_config["ssl_keyfile"] = args.ssl_keyfile
|
|
140
|
+
if args.ssl_certfile:
|
|
141
|
+
uvicorn_config["ssl_certfile"] = args.ssl_certfile
|
|
142
|
+
|
|
143
|
+
uvicorn.run(**uvicorn_config)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hindsight-api
|
|
3
|
+
Version: 0.0.13
|
|
4
|
+
Summary: Temporal + Semantic + Entity Memory System for AI agents using PostgreSQL
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: alembic>=1.17.1
|
|
7
|
+
Requires-Dist: asyncpg>=0.29.0
|
|
8
|
+
Requires-Dist: dateparser>=1.2.2
|
|
9
|
+
Requires-Dist: fastapi[standard]>=0.120.3
|
|
10
|
+
Requires-Dist: fastmcp>=2.0.0
|
|
11
|
+
Requires-Dist: greenlet>=3.2.4
|
|
12
|
+
Requires-Dist: httpx>=0.27.0
|
|
13
|
+
Requires-Dist: langchain-text-splitters>=0.3.0
|
|
14
|
+
Requires-Dist: openai>=1.0.0
|
|
15
|
+
Requires-Dist: opentelemetry-api>=1.20.0
|
|
16
|
+
Requires-Dist: opentelemetry-exporter-prometheus>=0.41b0
|
|
17
|
+
Requires-Dist: opentelemetry-instrumentation-fastapi>=0.41b0
|
|
18
|
+
Requires-Dist: opentelemetry-sdk>=1.20.0
|
|
19
|
+
Requires-Dist: pgvector>=0.4.1
|
|
20
|
+
Requires-Dist: psycopg2-binary>=2.9.11
|
|
21
|
+
Requires-Dist: pydantic>=2.0.0
|
|
22
|
+
Requires-Dist: python-dateutil>=2.8.0
|
|
23
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
24
|
+
Requires-Dist: rich>=13.0.0
|
|
25
|
+
Requires-Dist: sentence-transformers>=2.2.0
|
|
26
|
+
Requires-Dist: sqlalchemy>=2.0.44
|
|
27
|
+
Requires-Dist: tiktoken>=0.12.0
|
|
28
|
+
Requires-Dist: torch>=2.0.0
|
|
29
|
+
Requires-Dist: transformers>=4.30.0
|
|
30
|
+
Requires-Dist: uvicorn>=0.38.0
|
|
31
|
+
Requires-Dist: wsproto>=1.0.0
|
|
32
|
+
Provides-Extra: test
|
|
33
|
+
Requires-Dist: filelock>=3.0.0; extra == 'test'
|
|
34
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'test'
|
|
35
|
+
Requires-Dist: pytest-timeout>=2.4.0; extra == 'test'
|
|
36
|
+
Requires-Dist: pytest-xdist>=3.0.0; extra == 'test'
|
|
37
|
+
Requires-Dist: pytest>=7.0.0; extra == 'test'
|
|
38
|
+
Requires-Dist: testcontainers[postgres]>=4.0.0; extra == 'test'
|
|
39
|
+
Description-Content-Type: text/markdown
|
|
40
|
+
|
|
41
|
+
# Memory
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
hindsight_api/__init__.py,sha256=yQWYWUWEhvs1OY1coENhZV_CuOAWmN_YKZXQMIvGN94,851
|
|
2
|
+
hindsight_api/metrics.py,sha256=j4-eeqVjjcGQxAxS_GgEaBNm10KdUxrGS_I2d1IM1hY,7255
|
|
3
|
+
hindsight_api/migrations.py,sha256=VY-ILJLWEY1IaeJgQ2jlAVUtPLzq_41Dytg_DjuF0GA,6402
|
|
4
|
+
hindsight_api/models.py,sha256=1vMn9jmDQvohfmxZXr1SYnhz5vhz52nrTd93A_lkVNE,12606
|
|
5
|
+
hindsight_api/pg0.py,sha256=scFcYngOwbZ2oOQb7TysnUHgNgPyiN30pjPcIqMDmao,14158
|
|
6
|
+
hindsight_api/api/__init__.py,sha256=cDqM1tXOk1aq5Hy__vJ97O4XOQUB4Qt-ea_szTnbQ3o,3037
|
|
7
|
+
hindsight_api/api/http.py,sha256=anjh8axWcWF1dyqW3CnE9TUObLKxryjeQxT_keQEMak,71551
|
|
8
|
+
hindsight_api/api/mcp.py,sha256=NbRSbEGih7zCFMmwddLgD_UYv-WjvwYWHLq2-vzz4SA,7862
|
|
9
|
+
hindsight_api/engine/__init__.py,sha256=5DU5DvnJdzkrgNgKchpzkiJr-37I-kE1tegJg2LF04k,1214
|
|
10
|
+
hindsight_api/engine/cross_encoder.py,sha256=kfwLiqlQUfvOgLyrkRReO1wWlO020lGbLXY8U0jKiPA,2875
|
|
11
|
+
hindsight_api/engine/db_utils.py,sha256=p1Ne70wPP327xdPI_XjMfnagilY8sknbkhEIZuED6DU,2724
|
|
12
|
+
hindsight_api/engine/embeddings.py,sha256=a0wox2SCIE7ezgy-B5_23Cp1_icYiUR3g06hPpzi_ck,3586
|
|
13
|
+
hindsight_api/engine/entity_resolver.py,sha256=y_KWDkWaJwKluhGgJYAr_Amg4GTzyJAnrmRKnsyevsk,21737
|
|
14
|
+
hindsight_api/engine/llm_wrapper.py,sha256=HeAJDwNZjDkWR-6SVIrRJ7XmP4V-euciMrSLfjOLRYg,11378
|
|
15
|
+
hindsight_api/engine/memory_engine.py,sha256=3vUcZRSUIOYCMf_R_tqDjT1Q3FpAoVSrhG026weJEYQ,127985
|
|
16
|
+
hindsight_api/engine/query_analyzer.py,sha256=K0QCg7tsbqtwC7TR5wt3FPoP8QDuZsX9r0Zljc8nnYo,19733
|
|
17
|
+
hindsight_api/engine/response_models.py,sha256=eRafLz6JTRUsTZadfZxffMYNEz6kho3cF31m8xc4t4c,8783
|
|
18
|
+
hindsight_api/engine/task_backend.py,sha256=ojxMC9PeHdnkWVs2ozeqycjI_1mmpkDa0_Qfej9AHrg,7287
|
|
19
|
+
hindsight_api/engine/utils.py,sha256=VAjpZSbdiwhlE6cDlYfTt_-5hIJ--0xtfixETK0LPSk,6910
|
|
20
|
+
hindsight_api/engine/retain/__init__.py,sha256=L_QuR1YLHsJ7OCmVFNsZe8WDjbsTTHL-wCiUXtw1aUE,1230
|
|
21
|
+
hindsight_api/engine/retain/bank_utils.py,sha256=fvUR7bAlStLJ1z_kbs3AuF2XzPrMtXgHCA1u8uoU5nI,14482
|
|
22
|
+
hindsight_api/engine/retain/chunk_storage.py,sha256=rjmfnllS185tmjJGkMjWZ9q_6hJO4N6Ll9jgPx6f5xo,2081
|
|
23
|
+
hindsight_api/engine/retain/deduplication.py,sha256=9YXgVI_m1Mtz5Cv46ZceCEs0GwpLqTPHrZ-vlWlXk6I,3313
|
|
24
|
+
hindsight_api/engine/retain/embedding_processing.py,sha256=cHTt3rPvDCWBWVPfSeg6bwH8HoXYGmP4bvS21boNONI,1734
|
|
25
|
+
hindsight_api/engine/retain/embedding_utils.py,sha256=Q24h_iw6pRAW2vDWPvauWY1o3bXLzW3eWvSxDALDiE0,1588
|
|
26
|
+
hindsight_api/engine/retain/entity_processing.py,sha256=meHOjsFzdvh1tbe6YlTofhcUs2Y6TcAN3S-0EKOvFP0,2705
|
|
27
|
+
hindsight_api/engine/retain/fact_extraction.py,sha256=D7nnDn7U0UhsAwbo9qahPSpGxiRP-L5tdHT1JAIKM44,45254
|
|
28
|
+
hindsight_api/engine/retain/fact_storage.py,sha256=gRRQf_FCLsj5lUvdlOaxJsS5JosM6IhO_pik8Ur8VFg,5717
|
|
29
|
+
hindsight_api/engine/retain/link_creation.py,sha256=XJx7U3HboJLHtGgt_tHGsCa58lGo2ZyywzMNosrY9Xc,3154
|
|
30
|
+
hindsight_api/engine/retain/link_utils.py,sha256=PAXalIhAPZGcJv8EugcpwNgoWZ2D_ciVU3brHL-m090,26226
|
|
31
|
+
hindsight_api/engine/retain/orchestrator.py,sha256=I-EVH2REQLE3CypvWjcB9iZoJcl6dhXo3QPJMeWUz_4,17524
|
|
32
|
+
hindsight_api/engine/retain/types.py,sha256=AJPYxMy0Fh7zje2TPKXjPnr1QxaU7aVBCvjfCaPqvt8,6218
|
|
33
|
+
hindsight_api/engine/search/__init__.py,sha256=7X6U10bVw0JRWxQdE5RCfVpawDlSUldi1mPoCzY0V0A,363
|
|
34
|
+
hindsight_api/engine/search/fusion.py,sha256=so6LU7kWRR-VJd1Pxlu8idRJ7P2WLCoDwXUnb8jQifo,4309
|
|
35
|
+
hindsight_api/engine/search/observation_utils.py,sha256=SPrDx6M0daJ_zLLkk78GlQIG3EL7DqMKSu_etKerUfU,4331
|
|
36
|
+
hindsight_api/engine/search/reranking.py,sha256=Bk5i5kal5yy4CM8m2uSxAumLPgLeHdncBX6wk4WTmEI,3525
|
|
37
|
+
hindsight_api/engine/search/retrieval.py,sha256=kfQTU34LPLgB1QVcCAv7v2IPhOB2ag68xJ8RzvdSP10,19661
|
|
38
|
+
hindsight_api/engine/search/scoring.py,sha256=feFPalpbIMndp8j2Ab0zvu7fRq3c43Wmzrjw3piQ0eM,5167
|
|
39
|
+
hindsight_api/engine/search/temporal_extraction.py,sha256=5klrZdza3mkgk5A15_m_j4IIfOHMc6fUR9UJuzLa790,1812
|
|
40
|
+
hindsight_api/engine/search/think_utils.py,sha256=OOrMKEpkHvMD0-vmLZPk18s2AISLqPCJO68CyRccQDI,9629
|
|
41
|
+
hindsight_api/engine/search/trace.py,sha256=GT86_LVKMyG2mw6EJzPjafvbqaot6XVy5fZ033pMXG8,11036
|
|
42
|
+
hindsight_api/engine/search/tracer.py,sha256=mcM9qZpj3YFudrBCESwc6YKNAiWIMx1lScXWn5ru-ok,15017
|
|
43
|
+
hindsight_api/engine/search/types.py,sha256=qIeHW_gT7f291vteTZXygAM8oAaPp2dq6uEdvOyOwzs,5488
|
|
44
|
+
hindsight_api/web/__init__.py,sha256=WABqyqiAVFJJWOhKCytkj5Vcb61eAsRib3Ek7IMX6_U,378
|
|
45
|
+
hindsight_api/web/server.py,sha256=oPNJ_z4DO38MdK7Juyh2LdH0ipZ_BQF48cUM-4B_Uw0,5379
|
|
46
|
+
hindsight_api-0.0.13.dist-info/METADATA,sha256=Lr36kApNId06zH-6m9LNNXR4zYLefSBMn2NqCSnMN24,1496
|
|
47
|
+
hindsight_api-0.0.13.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
48
|
+
hindsight_api-0.0.13.dist-info/RECORD,,
|