hindsight-api 0.1.2__py3-none-any.whl → 0.1.4__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/api/http.py +30 -1
- hindsight_api/banner.py +89 -0
- hindsight_api/config.py +2 -2
- hindsight_api/engine/llm_wrapper.py +54 -11
- hindsight_api/engine/memory_engine.py +13 -3
- hindsight_api/engine/retain/link_utils.py +51 -1
- hindsight_api/main.py +17 -9
- hindsight_api/pg0.py +8 -7
- {hindsight_api-0.1.2.dist-info → hindsight_api-0.1.4.dist-info}/METADATA +1 -1
- {hindsight_api-0.1.2.dist-info → hindsight_api-0.1.4.dist-info}/RECORD +12 -11
- {hindsight_api-0.1.2.dist-info → hindsight_api-0.1.4.dist-info}/WHEEL +0 -0
- {hindsight_api-0.1.2.dist-info → hindsight_api-0.1.4.dist-info}/entry_points.txt +0 -0
hindsight_api/api/http.py
CHANGED
|
@@ -672,11 +672,15 @@ class DeleteResponse(BaseModel):
|
|
|
672
672
|
"""Response model for delete operations."""
|
|
673
673
|
model_config = ConfigDict(json_schema_extra={
|
|
674
674
|
"example": {
|
|
675
|
-
"success": True
|
|
675
|
+
"success": True,
|
|
676
|
+
"message": "Deleted successfully",
|
|
677
|
+
"deleted_count": 10
|
|
676
678
|
}
|
|
677
679
|
})
|
|
678
680
|
|
|
679
681
|
success: bool
|
|
682
|
+
message: Optional[str] = None
|
|
683
|
+
deleted_count: Optional[int] = None
|
|
680
684
|
|
|
681
685
|
|
|
682
686
|
def create_app(memory: MemoryEngine, initialize_memory: bool = True) -> FastAPI:
|
|
@@ -1696,6 +1700,31 @@ def _register_routes(app: FastAPI):
|
|
|
1696
1700
|
raise HTTPException(status_code=500, detail=str(e))
|
|
1697
1701
|
|
|
1698
1702
|
|
|
1703
|
+
@app.delete(
|
|
1704
|
+
"/v1/default/banks/{bank_id}",
|
|
1705
|
+
response_model=DeleteResponse,
|
|
1706
|
+
summary="Delete memory bank",
|
|
1707
|
+
description="Delete an entire memory bank including all memories, entities, documents, and the bank profile itself. "
|
|
1708
|
+
"This is a destructive operation that cannot be undone.",
|
|
1709
|
+
operation_id="delete_bank",
|
|
1710
|
+
tags=["Banks"]
|
|
1711
|
+
)
|
|
1712
|
+
async def api_delete_bank(bank_id: str):
|
|
1713
|
+
"""Delete an entire memory bank and all its data."""
|
|
1714
|
+
try:
|
|
1715
|
+
result = await app.state.memory.delete_bank(bank_id)
|
|
1716
|
+
return DeleteResponse(
|
|
1717
|
+
success=True,
|
|
1718
|
+
message=f"Bank '{bank_id}' and all associated data deleted successfully",
|
|
1719
|
+
deleted_count=result.get("memory_units_deleted", 0) + result.get("entities_deleted", 0) + result.get("documents_deleted", 0)
|
|
1720
|
+
)
|
|
1721
|
+
except Exception as e:
|
|
1722
|
+
import traceback
|
|
1723
|
+
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
|
1724
|
+
logger.error(f"Error in DELETE /v1/default/banks/{bank_id}: {error_detail}")
|
|
1725
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
1726
|
+
|
|
1727
|
+
|
|
1699
1728
|
@app.post(
|
|
1700
1729
|
"/v1/default/banks/{bank_id}/memories",
|
|
1701
1730
|
response_model=RetainResponse,
|
hindsight_api/banner.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Banner display for Hindsight API startup.
|
|
3
|
+
|
|
4
|
+
Shows the logo and tagline with gradient colors.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# Gradient colors: #0074d9 -> #009296
|
|
8
|
+
GRADIENT_START = (0, 116, 217) # #0074d9
|
|
9
|
+
GRADIENT_END = (0, 146, 150) # #009296
|
|
10
|
+
|
|
11
|
+
# Pre-generated logo (generated by test-logo.py)
|
|
12
|
+
LOGO = """\
|
|
13
|
+
\033[38;2;9;127;184m\u2584\033[0m\033[48;2;8;130;178m\033[38;2;5;133;186m\u2584\033[0m \033[48;2;10;143;160m\033[38;2;10;143;165m\u2584\033[0m\033[38;2;7;140;156m\u2584\033[0m
|
|
14
|
+
\033[38;2;8;125;192m\u2584\033[0m \033[38;2;3;132;191m\u2580\033[0m\033[38;2;2;133;192m\u2584\033[0m \033[38;2;3;132;180m\u2584\033[0m\033[38;2;1;137;184m\u2584\033[0m\033[38;2;3;133;174m\u2584\033[0m \033[38;2;3;142;176m\u2584\033[0m\033[38;2;4;142;169m\u2580\033[0m \033[38;2;10;144;164m\u2584\033[0m
|
|
15
|
+
\033[38;2;6;121;195m\u2580\033[0m\033[38;2;5;128;203m\u2580\033[0m\033[48;2;5;124;195m\033[38;2;3;125;200m\u2584\033[0m\033[38;2;2;126;196m\u2584\033[0m\033[48;2;3;128;188m\033[38;2;1;131;196m\u2584\033[0m\033[48;2;0;152;219m\033[38;2;2;131;191m\u2584\033[0m\033[38;2;1;141;196m\u2580\033[0m\033[38;2;1;135;183m\u2580\033[0m\033[38;2;1;148;198m\u2580\033[0m\033[48;2;1;156;202m\033[38;2;2;135;180m\u2584\033[0m\033[48;2;4;134;169m\033[38;2;1;137;177m\u2584\033[0m\033[38;2;3;138;173m\u2584\033[0m\033[48;2;6;137;165m\033[38;2;2;140;170m\u2584\033[0m\033[38;2;7;144;169m\u2580\033[0m\033[38;2;7;139;158m\u2580\033[0m
|
|
16
|
+
\033[48;2;2;128;202m\033[38;2;2;124;201m\u2584\033[0m\033[48;2;1;130;201m\033[38;2;0;135;212m\u2584\033[0m\033[38;2;2;128;196m\u2584\033[0m \033[48;2;2;142;204m\033[38;2;7;138;199m\u2584\033[0m \033[38;2;1;135;186m\u2584\033[0m\033[48;2;1;142;186m\033[38;2;2;144;194m\u2584\033[0m\033[48;2;3;138;176m\033[38;2;2;134;176m\u2584\033[0m
|
|
17
|
+
\033[48;2;8;118;200m\033[38;2;8;121;209m\u2584\033[0m\033[38;2;3;121;203m\u2580\033[0m \033[38;2;3;122;192m\u2580\033[0m\033[38;2;1;138;216m\u2580\033[0m\033[48;2;0;138;210m\033[38;2;3;128;198m\u2584\033[0m\033[48;2;0;126;188m\033[38;2;2;131;198m\u2584\033[0m\033[48;2;0;142;205m\033[38;2;3;132;193m\u2584\033[0m\033[38;2;1;140;196m\u2580\033[0m \033[38;2;4;134;175m\u2580\033[0m\033[48;2;13;135;167m\033[38;2;8;136;174m\u2584\033[0m """
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _interpolate_color(start: tuple, end: tuple, t: float) -> tuple:
|
|
21
|
+
"""Interpolate between two RGB colors."""
|
|
22
|
+
return (
|
|
23
|
+
int(start[0] + (end[0] - start[0]) * t),
|
|
24
|
+
int(start[1] + (end[1] - start[1]) * t),
|
|
25
|
+
int(start[2] + (end[2] - start[2]) * t),
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def gradient_text(text: str, start: tuple = GRADIENT_START, end: tuple = GRADIENT_END) -> str:
|
|
30
|
+
"""Render text with a gradient color effect."""
|
|
31
|
+
result = []
|
|
32
|
+
length = len(text)
|
|
33
|
+
for i, char in enumerate(text):
|
|
34
|
+
if char == ' ':
|
|
35
|
+
result.append(' ')
|
|
36
|
+
else:
|
|
37
|
+
t = i / max(length - 1, 1)
|
|
38
|
+
r, g, b = _interpolate_color(start, end, t)
|
|
39
|
+
result.append(f"\033[38;2;{r};{g};{b}m{char}")
|
|
40
|
+
result.append("\033[0m")
|
|
41
|
+
return "".join(result)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def print_banner():
|
|
45
|
+
"""Print the Hindsight startup banner."""
|
|
46
|
+
print(LOGO)
|
|
47
|
+
tagline = gradient_text("Hindsight: Agent Memory That Works Like Human Memory")
|
|
48
|
+
print(f"\n {tagline}\n")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def color(text: str, t: float = 0.0) -> str:
|
|
52
|
+
"""Color text using gradient position (0.0 = start, 1.0 = end)."""
|
|
53
|
+
r, g, b = _interpolate_color(GRADIENT_START, GRADIENT_END, t)
|
|
54
|
+
return f"\033[38;2;{r};{g};{b}m{text}\033[0m"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def color_start(text: str) -> str:
|
|
58
|
+
"""Color text with gradient start color (#0074d9)."""
|
|
59
|
+
return color(text, 0.0)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def color_end(text: str) -> str:
|
|
63
|
+
"""Color text with gradient end color (#009296)."""
|
|
64
|
+
return color(text, 1.0)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def color_mid(text: str) -> str:
|
|
68
|
+
"""Color text with gradient middle color."""
|
|
69
|
+
return color(text, 0.5)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def dim(text: str) -> str:
|
|
73
|
+
"""Dim/gray text."""
|
|
74
|
+
return f"\033[38;2;128;128;128m{text}\033[0m"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def print_startup_info(host: str, port: int, database_url: str, llm_provider: str,
|
|
78
|
+
llm_model: str, embeddings_provider: str, reranker_provider: str,
|
|
79
|
+
mcp_enabled: bool = False):
|
|
80
|
+
"""Print styled startup information."""
|
|
81
|
+
print(color_start("Starting Hindsight API..."))
|
|
82
|
+
print(f" {dim('URL:')} {color(f'http://{host}:{port}', 0.2)}")
|
|
83
|
+
print(f" {dim('Database:')} {color(database_url, 0.4)}")
|
|
84
|
+
print(f" {dim('LLM:')} {color(f'{llm_provider} / {llm_model}', 0.6)}")
|
|
85
|
+
print(f" {dim('Embeddings:')} {color(embeddings_provider, 0.8)}")
|
|
86
|
+
print(f" {dim('Reranker:')} {color(reranker_provider, 1.0)}")
|
|
87
|
+
if mcp_enabled:
|
|
88
|
+
print(f" {dim('MCP:')} {color_end('enabled at /mcp')}")
|
|
89
|
+
print()
|
hindsight_api/config.py
CHANGED
|
@@ -32,8 +32,8 @@ ENV_MCP_ENABLED = "HINDSIGHT_API_MCP_ENABLED"
|
|
|
32
32
|
|
|
33
33
|
# Default values
|
|
34
34
|
DEFAULT_DATABASE_URL = "pg0"
|
|
35
|
-
DEFAULT_LLM_PROVIDER = "
|
|
36
|
-
DEFAULT_LLM_MODEL = "
|
|
35
|
+
DEFAULT_LLM_PROVIDER = "openai"
|
|
36
|
+
DEFAULT_LLM_MODEL = "gpt-5-mini"
|
|
37
37
|
|
|
38
38
|
DEFAULT_EMBEDDINGS_PROVIDER = "local"
|
|
39
39
|
DEFAULT_EMBEDDINGS_LOCAL_MODEL = "BAAI/bge-small-en-v1.5"
|
|
@@ -91,12 +91,35 @@ class LLMProvider:
|
|
|
91
91
|
self._client = AsyncOpenAI(api_key="ollama", base_url=self.base_url, max_retries=0)
|
|
92
92
|
self._gemini_client = None
|
|
93
93
|
else:
|
|
94
|
-
|
|
94
|
+
# Only pass base_url if it's set (OpenAI uses default URL otherwise)
|
|
95
|
+
client_kwargs = {"api_key": self.api_key, "max_retries": 0}
|
|
96
|
+
if self.base_url:
|
|
97
|
+
client_kwargs["base_url"] = self.base_url
|
|
98
|
+
self._client = AsyncOpenAI(**client_kwargs)
|
|
95
99
|
self._gemini_client = None
|
|
96
100
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
101
|
+
async def verify_connection(self) -> None:
|
|
102
|
+
"""
|
|
103
|
+
Verify that the LLM provider is configured correctly by making a simple test call.
|
|
104
|
+
|
|
105
|
+
Raises:
|
|
106
|
+
RuntimeError: If the connection test fails.
|
|
107
|
+
"""
|
|
108
|
+
try:
|
|
109
|
+
logger.info(f"Verifying LLM: provider={self.provider}, model={self.model}, base_url={self.base_url or 'default'}...")
|
|
110
|
+
await self.call(
|
|
111
|
+
messages=[{"role": "user", "content": "Say 'ok'"}],
|
|
112
|
+
max_completion_tokens=10,
|
|
113
|
+
max_retries=2,
|
|
114
|
+
initial_backoff=0.5,
|
|
115
|
+
max_backoff=2.0,
|
|
116
|
+
)
|
|
117
|
+
# If we get here without exception, the connection is working
|
|
118
|
+
logger.info(f"LLM verified: {self.provider}/{self.model}")
|
|
119
|
+
except Exception as e:
|
|
120
|
+
raise RuntimeError(
|
|
121
|
+
f"LLM connection verification failed for {self.provider}/{self.model}: {e}"
|
|
122
|
+
) from e
|
|
100
123
|
|
|
101
124
|
async def call(
|
|
102
125
|
self,
|
|
@@ -147,19 +170,38 @@ class LLMProvider:
|
|
|
147
170
|
"messages": messages,
|
|
148
171
|
}
|
|
149
172
|
|
|
173
|
+
# Check if model supports reasoning parameter (o1, o3, gpt-5 families)
|
|
174
|
+
model_lower = self.model.lower()
|
|
175
|
+
is_reasoning_model = any(x in model_lower for x in ["gpt-5", "o1", "o3"])
|
|
176
|
+
|
|
177
|
+
# For GPT-4 and GPT-4.1 models, cap max_completion_tokens to 32000
|
|
178
|
+
is_gpt4_model = any(x in model_lower for x in ["gpt-4.1", "gpt-4-"])
|
|
150
179
|
if max_completion_tokens is not None:
|
|
180
|
+
if is_gpt4_model and max_completion_tokens > 32000:
|
|
181
|
+
max_completion_tokens = 32000
|
|
182
|
+
# For reasoning models, max_completion_tokens includes reasoning + output tokens
|
|
183
|
+
# Enforce minimum of 16000 to ensure enough space for both
|
|
184
|
+
if is_reasoning_model and max_completion_tokens < 16000:
|
|
185
|
+
max_completion_tokens = 16000
|
|
151
186
|
call_params["max_completion_tokens"] = max_completion_tokens
|
|
152
|
-
|
|
187
|
+
|
|
188
|
+
# GPT-5/o1/o3 family doesn't support custom temperature (only default 1)
|
|
189
|
+
if temperature is not None and not is_reasoning_model:
|
|
153
190
|
call_params["temperature"] = temperature
|
|
154
191
|
|
|
192
|
+
# Set reasoning_effort for reasoning models (OpenAI gpt-5, o1, o3)
|
|
193
|
+
if is_reasoning_model and self.provider == "openai":
|
|
194
|
+
call_params["reasoning_effort"] = self.reasoning_effort
|
|
195
|
+
|
|
155
196
|
# Provider-specific parameters
|
|
156
197
|
if self.provider == "groq":
|
|
157
198
|
call_params["seed"] = DEFAULT_LLM_SEED
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
"
|
|
162
|
-
|
|
199
|
+
extra_body = {"service_tier": "auto"}
|
|
200
|
+
# Only add reasoning parameters for reasoning models
|
|
201
|
+
if is_reasoning_model:
|
|
202
|
+
extra_body["reasoning_effort"] = self.reasoning_effort
|
|
203
|
+
extra_body["include_reasoning"] = False
|
|
204
|
+
call_params["extra_body"] = extra_body
|
|
163
205
|
|
|
164
206
|
last_exception = None
|
|
165
207
|
|
|
@@ -216,7 +258,8 @@ class LLMProvider:
|
|
|
216
258
|
except APIConnectionError as e:
|
|
217
259
|
last_exception = e
|
|
218
260
|
if attempt < max_retries:
|
|
219
|
-
|
|
261
|
+
status_code = getattr(e, 'status_code', None) or getattr(getattr(e, 'response', None), 'status_code', None)
|
|
262
|
+
logger.warning(f"Connection error, retrying... (attempt {attempt + 1}/{max_retries + 1}) - status_code={status_code}, message={e}")
|
|
220
263
|
backoff = min(initial_backoff * (2 ** attempt), max_backoff)
|
|
221
264
|
await asyncio.sleep(backoff)
|
|
222
265
|
continue
|
|
@@ -453,12 +453,17 @@ class MemoryEngine:
|
|
|
453
453
|
# Query analyzer load is sync and CPU-bound
|
|
454
454
|
await loop.run_in_executor(None, self.query_analyzer.load)
|
|
455
455
|
|
|
456
|
+
async def verify_llm():
|
|
457
|
+
"""Verify LLM connection is working."""
|
|
458
|
+
await self._llm_config.verify_connection()
|
|
459
|
+
|
|
456
460
|
# Run pg0 and all model initializations in parallel
|
|
457
461
|
await asyncio.gather(
|
|
458
462
|
start_pg0(),
|
|
459
463
|
init_embeddings(),
|
|
460
464
|
init_cross_encoder(),
|
|
461
465
|
init_query_analyzer(),
|
|
466
|
+
verify_llm(),
|
|
462
467
|
)
|
|
463
468
|
|
|
464
469
|
# Run database migrations if enabled
|
|
@@ -1791,10 +1796,14 @@ class MemoryEngine:
|
|
|
1791
1796
|
# Delete entities (cascades to unit_entities, entity_cooccurrences, memory_links with entity_id)
|
|
1792
1797
|
await conn.execute("DELETE FROM entities WHERE bank_id = $1", bank_id)
|
|
1793
1798
|
|
|
1799
|
+
# Delete the bank profile itself
|
|
1800
|
+
await conn.execute("DELETE FROM banks WHERE bank_id = $1", bank_id)
|
|
1801
|
+
|
|
1794
1802
|
return {
|
|
1795
1803
|
"memory_units_deleted": units_count,
|
|
1796
1804
|
"entities_deleted": entities_count,
|
|
1797
|
-
"documents_deleted": documents_count
|
|
1805
|
+
"documents_deleted": documents_count,
|
|
1806
|
+
"bank_deleted": True
|
|
1798
1807
|
}
|
|
1799
1808
|
|
|
1800
1809
|
except Exception as e:
|
|
@@ -1839,10 +1848,11 @@ class MemoryEngine:
|
|
|
1839
1848
|
""", *query_params)
|
|
1840
1849
|
|
|
1841
1850
|
# Get links, filtering to only include links between units of the selected agent
|
|
1851
|
+
# Use DISTINCT ON with LEAST/GREATEST to deduplicate bidirectional links
|
|
1842
1852
|
unit_ids = [row['id'] for row in units]
|
|
1843
1853
|
if unit_ids:
|
|
1844
1854
|
links = await conn.fetch("""
|
|
1845
|
-
SELECT
|
|
1855
|
+
SELECT DISTINCT ON (LEAST(ml.from_unit_id, ml.to_unit_id), GREATEST(ml.from_unit_id, ml.to_unit_id), ml.link_type, COALESCE(ml.entity_id, '00000000-0000-0000-0000-000000000000'::uuid))
|
|
1846
1856
|
ml.from_unit_id,
|
|
1847
1857
|
ml.to_unit_id,
|
|
1848
1858
|
ml.link_type,
|
|
@@ -1851,7 +1861,7 @@ class MemoryEngine:
|
|
|
1851
1861
|
FROM memory_links ml
|
|
1852
1862
|
LEFT JOIN entities e ON ml.entity_id = e.id
|
|
1853
1863
|
WHERE ml.from_unit_id = ANY($1::uuid[]) AND ml.to_unit_id = ANY($1::uuid[])
|
|
1854
|
-
ORDER BY ml.link_type, ml.weight DESC
|
|
1864
|
+
ORDER BY LEAST(ml.from_unit_id, ml.to_unit_id), GREATEST(ml.from_unit_id, ml.to_unit_id), ml.link_type, COALESCE(ml.entity_id, '00000000-0000-0000-0000-000000000000'::uuid), ml.weight DESC
|
|
1855
1865
|
""", unit_ids)
|
|
1856
1866
|
else:
|
|
1857
1867
|
links = []
|
|
@@ -390,6 +390,27 @@ async def create_temporal_links_batch_per_fact(
|
|
|
390
390
|
# Filter and create links in memory (much faster than N queries)
|
|
391
391
|
link_gen_start = time_mod.time()
|
|
392
392
|
links = compute_temporal_links(new_units, all_candidates, time_window_hours)
|
|
393
|
+
|
|
394
|
+
# Also compute temporal links WITHIN the new batch (new units to each other)
|
|
395
|
+
if len(new_units) > 1:
|
|
396
|
+
# Convert new_units dict to candidate format for within-batch linking
|
|
397
|
+
new_unit_items = list(new_units.items())
|
|
398
|
+
for i, (unit_id, event_date) in enumerate(new_unit_items):
|
|
399
|
+
unit_event_date_norm = _normalize_datetime(event_date)
|
|
400
|
+
|
|
401
|
+
# Compare with other new units (only those after this one to avoid duplicates)
|
|
402
|
+
for j in range(i + 1, len(new_unit_items)):
|
|
403
|
+
other_id, other_event_date = new_unit_items[j]
|
|
404
|
+
other_event_date_norm = _normalize_datetime(other_event_date)
|
|
405
|
+
|
|
406
|
+
# Check if within time window
|
|
407
|
+
time_diff_hours = abs((unit_event_date_norm - other_event_date_norm).total_seconds() / 3600)
|
|
408
|
+
if time_diff_hours <= time_window_hours:
|
|
409
|
+
weight = max(0.3, 1.0 - (time_diff_hours / time_window_hours))
|
|
410
|
+
# Create bidirectional links
|
|
411
|
+
links.append((unit_id, other_id, 'temporal', weight, None))
|
|
412
|
+
links.append((other_id, unit_id, 'temporal', weight, None))
|
|
413
|
+
|
|
393
414
|
_log(log_buffer, f" [7.3] Generate {len(links)} temporal links: {time_mod.time() - link_gen_start:.3f}s")
|
|
394
415
|
|
|
395
416
|
if links:
|
|
@@ -514,9 +535,38 @@ async def create_semantic_links_batch(
|
|
|
514
535
|
|
|
515
536
|
for idx in sorted_indices:
|
|
516
537
|
similar_id = existing_ids[idx]
|
|
517
|
-
|
|
538
|
+
# Clamp to [0, 1] to handle floating point precision issues
|
|
539
|
+
similarity = float(min(1.0, max(0.0, similarities[idx])))
|
|
518
540
|
all_links.append((unit_id, similar_id, 'semantic', similarity, None))
|
|
519
541
|
|
|
542
|
+
# Also compute similarities WITHIN the new batch (new units to each other)
|
|
543
|
+
# Apply the same top_k limit per unit as we do for existing units
|
|
544
|
+
if len(unit_ids) > 1:
|
|
545
|
+
new_embeddings_matrix = np.array(embeddings)
|
|
546
|
+
|
|
547
|
+
for i, unit_id in enumerate(unit_ids):
|
|
548
|
+
# Compute similarities with all OTHER new units
|
|
549
|
+
other_indices = [j for j in range(len(unit_ids)) if j != i]
|
|
550
|
+
if not other_indices:
|
|
551
|
+
continue
|
|
552
|
+
|
|
553
|
+
other_embeddings = new_embeddings_matrix[other_indices]
|
|
554
|
+
similarities = np.dot(other_embeddings, new_embeddings_matrix[i])
|
|
555
|
+
|
|
556
|
+
# Find top-k above threshold (same logic as existing units)
|
|
557
|
+
above_threshold = np.where(similarities >= threshold)[0]
|
|
558
|
+
|
|
559
|
+
if len(above_threshold) > 0:
|
|
560
|
+
# Sort by similarity (descending) and take top-k
|
|
561
|
+
sorted_local_indices = above_threshold[np.argsort(-similarities[above_threshold])][:top_k]
|
|
562
|
+
|
|
563
|
+
for local_idx in sorted_local_indices:
|
|
564
|
+
other_idx = other_indices[local_idx]
|
|
565
|
+
other_id = unit_ids[other_idx]
|
|
566
|
+
# Clamp to [0, 1] to handle floating point precision issues
|
|
567
|
+
similarity = float(min(1.0, max(0.0, similarities[local_idx])))
|
|
568
|
+
all_links.append((unit_id, other_id, 'semantic', similarity, None))
|
|
569
|
+
|
|
520
570
|
_log(log_buffer, f" [8.2] Compute similarities & generate {len(all_links)} semantic links: {time_mod.time() - compute_start:.3f}s")
|
|
521
571
|
|
|
522
572
|
if all_links:
|
hindsight_api/main.py
CHANGED
|
@@ -21,6 +21,10 @@ from . import MemoryEngine
|
|
|
21
21
|
from .api import create_app
|
|
22
22
|
from .config import get_config, HindsightConfig
|
|
23
23
|
|
|
24
|
+
from .banner import print_banner
|
|
25
|
+
print()
|
|
26
|
+
print_banner()
|
|
27
|
+
|
|
24
28
|
# Filter deprecation warnings from third-party libraries
|
|
25
29
|
warnings.filterwarnings("ignore", message="websockets.legacy is deprecated")
|
|
26
30
|
warnings.filterwarnings("ignore", message="websockets.server.WebSocketServerProtocol is deprecated")
|
|
@@ -184,15 +188,19 @@ def main():
|
|
|
184
188
|
if args.ssl_certfile:
|
|
185
189
|
uvicorn_config["ssl_certfile"] = args.ssl_certfile
|
|
186
190
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
from .banner import print_startup_info
|
|
194
|
+
print_startup_info(
|
|
195
|
+
host=args.host,
|
|
196
|
+
port=args.port,
|
|
197
|
+
database_url=config.database_url,
|
|
198
|
+
llm_provider=config.llm_provider,
|
|
199
|
+
llm_model=config.llm_model,
|
|
200
|
+
embeddings_provider=config.embeddings_provider,
|
|
201
|
+
reranker_provider=config.reranker_provider,
|
|
202
|
+
mcp_enabled=config.mcp_enabled,
|
|
203
|
+
)
|
|
196
204
|
|
|
197
205
|
uvicorn.run(**uvicorn_config)
|
|
198
206
|
|
hindsight_api/pg0.py
CHANGED
|
@@ -257,16 +257,17 @@ class EmbeddedPostgres:
|
|
|
257
257
|
last_error = stderr or f"pg0 start returned exit code {returncode}"
|
|
258
258
|
if attempt < max_retries:
|
|
259
259
|
delay = retry_delay * (2 ** (attempt - 1))
|
|
260
|
-
logger.
|
|
261
|
-
logger.
|
|
260
|
+
logger.debug(f"pg0 start attempt {attempt}/{max_retries} failed: {last_error.strip()}")
|
|
261
|
+
logger.debug(f"Retrying in {delay:.1f}s...")
|
|
262
262
|
await asyncio.sleep(delay)
|
|
263
263
|
else:
|
|
264
|
-
logger.
|
|
264
|
+
logger.debug(f"pg0 start attempt {attempt}/{max_retries} failed: {last_error.strip()}")
|
|
265
265
|
|
|
266
|
-
# All retries exhausted -
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
266
|
+
# All retries exhausted - fail
|
|
267
|
+
raise RuntimeError(
|
|
268
|
+
f"Failed to start embedded PostgreSQL after {max_retries} attempts. "
|
|
269
|
+
f"Last error: {last_error.strip() if last_error else 'unknown'}"
|
|
270
|
+
)
|
|
270
271
|
|
|
271
272
|
async def stop(self) -> None:
|
|
272
273
|
"""Stop the PostgreSQL server."""
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
hindsight_api/__init__.py,sha256=gPkRHnMATZqBgc7b-Mcro4f_gY9W0BlnGBE0zg1t_IY,1139
|
|
2
|
-
hindsight_api/
|
|
3
|
-
hindsight_api/
|
|
2
|
+
hindsight_api/banner.py,sha256=0dsNMXZSuOlj5lkzFykYB19BBOaePgzDTPydPEFel9M,4604
|
|
3
|
+
hindsight_api/config.py,sha256=k2WoA2VAaeAiB0M1JHckl_lO8idC6D1ouxVlIS6w9-g,5180
|
|
4
|
+
hindsight_api/main.py,sha256=aPftkauWHDaC7We0OF08EsDYuwjBHMZTPznoan-nxnc,6131
|
|
4
5
|
hindsight_api/metrics.py,sha256=j4-eeqVjjcGQxAxS_GgEaBNm10KdUxrGS_I2d1IM1hY,7255
|
|
5
6
|
hindsight_api/migrations.py,sha256=nSbU37ZszVZifYJTU_vEXfusTxWaUea9dRi7-Ao3-SQ,7349
|
|
6
7
|
hindsight_api/models.py,sha256=ncIi8agl3PVk7ffyXlJosFym1jJZZhVmXTguZ3EnAEc,12515
|
|
7
|
-
hindsight_api/pg0.py,sha256=
|
|
8
|
+
hindsight_api/pg0.py,sha256=MwZ0KW9_Vm2YTIO27j_YoK8zQQPc06YXD7Z7xJgZ59U,13503
|
|
8
9
|
hindsight_api/server.py,sha256=C_w3_xzKRVkSsFkujhMz9S4nDlAc0eOFClGIefTCZIk,1263
|
|
9
10
|
hindsight_api/alembic/README,sha256=MVlc9TYmr57RbhXET6QxgyCcwWP7w-vLkEsirENqiIQ,38
|
|
10
11
|
hindsight_api/alembic/env.py,sha256=i0gc3GN2rWidtqRp-vdvnJTIR0zl1X4Uokqp_WnTsAo,4837
|
|
@@ -16,15 +17,15 @@ hindsight_api/alembic/versions/d9f6a3b4c5e2_rename_bank_to_interactions.py,sha25
|
|
|
16
17
|
hindsight_api/alembic/versions/e0a1b2c3d4e5_disposition_to_3_traits.py,sha256=fykAU4V4jq1wVpCBoH0TzvSFyeyxZtk-6hpaZAbrFxU,2327
|
|
17
18
|
hindsight_api/alembic/versions/rename_personality_to_disposition.py,sha256=xlp-Is96e3TvCwSqhPctQRqLBHcl3dvDmlzbCMZGw1A,2196
|
|
18
19
|
hindsight_api/api/__init__.py,sha256=dIJqoygqYaEgm-Bd7qwZ4UTnb9UPyXtlDxZnQpvVC0o,2946
|
|
19
|
-
hindsight_api/api/http.py,sha256=
|
|
20
|
+
hindsight_api/api/http.py,sha256=G23QBZw097boDBNYeox_MdfLWdmDUZczrakTvGTzb3Q,71195
|
|
20
21
|
hindsight_api/api/mcp.py,sha256=DldtC8LQUguAbJSSHnEBS65wC-AQBjcF8rl51xUR1gQ,7808
|
|
21
22
|
hindsight_api/engine/__init__.py,sha256=W_y6iAHgu-HUpvdXlI6JJ0KO42wVkrWvcUSJZqTCj_M,1406
|
|
22
23
|
hindsight_api/engine/cross_encoder.py,sha256=D6iTTXv23R1n8O_IoAsBCsCbevZTNhnJSt4AirADksg,10789
|
|
23
24
|
hindsight_api/engine/db_utils.py,sha256=p1Ne70wPP327xdPI_XjMfnagilY8sknbkhEIZuED6DU,2724
|
|
24
25
|
hindsight_api/engine/embeddings.py,sha256=RdK9A3lUjp1FZFArllhTgKo70Pot4ZUEJ1Pw70BpNmk,10218
|
|
25
26
|
hindsight_api/engine/entity_resolver.py,sha256=w5DPCuYNsK4GF8Qe3oY7jCKcOT1WYx2h0YD1nX0QRtA,23184
|
|
26
|
-
hindsight_api/engine/llm_wrapper.py,sha256=
|
|
27
|
-
hindsight_api/engine/memory_engine.py,sha256=
|
|
27
|
+
hindsight_api/engine/llm_wrapper.py,sha256=d1v6DdmoYfmfb6MgJbviekH2Hlo3L1bamKKE_TVNw7k,21112
|
|
28
|
+
hindsight_api/engine/memory_engine.py,sha256=QDCZ4scEjkyTLtEV6XSjnRiWhew5huoEds0CHw12bvA,134924
|
|
28
29
|
hindsight_api/engine/query_analyzer.py,sha256=K0QCg7tsbqtwC7TR5wt3FPoP8QDuZsX9r0Zljc8nnYo,19733
|
|
29
30
|
hindsight_api/engine/response_models.py,sha256=e-_vE1zAVFLpkl6SeHIYvHcQ4Z-AaOdq0jjjhh8yHk4,8683
|
|
30
31
|
hindsight_api/engine/task_backend.py,sha256=ojxMC9PeHdnkWVs2ozeqycjI_1mmpkDa0_Qfej9AHrg,7287
|
|
@@ -39,7 +40,7 @@ hindsight_api/engine/retain/entity_processing.py,sha256=F_6yYjf7Me5khg-X57ZW4wK5
|
|
|
39
40
|
hindsight_api/engine/retain/fact_extraction.py,sha256=dZ0FYepvndUfWTowGxM5vrrF0NV_LIIGsANA6Ze6Mf4,50818
|
|
40
41
|
hindsight_api/engine/retain/fact_storage.py,sha256=rKJiWr_1lrqyB6s0mTCnTiHVZIUbCfd3zigNwISnVPI,5637
|
|
41
42
|
hindsight_api/engine/retain/link_creation.py,sha256=rkYKO73dWBL8BbRBeiwNgHzwrU-sKWUjmrgLIxr3LiA,3280
|
|
42
|
-
hindsight_api/engine/retain/link_utils.py,sha256=
|
|
43
|
+
hindsight_api/engine/retain/link_utils.py,sha256=kTMxO9fOXHQvRJZ-gsTrdrKmfw16trWKtvkC2ahQ8o0,31640
|
|
43
44
|
hindsight_api/engine/retain/observation_regeneration.py,sha256=ykEMZihF1Vt8Z7427k1OJKyEjYp0qIs9P1IqP6eyI58,8069
|
|
44
45
|
hindsight_api/engine/retain/orchestrator.py,sha256=z71wMuJsdjLFlhhwnLHuJ3Y3QQk-9w_faKn1zZUYCXw,17156
|
|
45
46
|
hindsight_api/engine/retain/types.py,sha256=JJ4t8Qtp64kTPB9CKOFDXqdos2i8GZXmJZNzBDaNwHY,6514
|
|
@@ -54,7 +55,7 @@ hindsight_api/engine/search/think_utils.py,sha256=VJJXFmBg03yO4Mg--UBMlTQW9IZOj2
|
|
|
54
55
|
hindsight_api/engine/search/trace.py,sha256=Hx-siW9yAfqZoK9LG6esbed0vQuHMNsGxSvCg4FK6-4,11042
|
|
55
56
|
hindsight_api/engine/search/tracer.py,sha256=LQ78knpMxyZmPUvm3PJNN2opCyA-LpB47JZ84n0g2pw,15074
|
|
56
57
|
hindsight_api/engine/search/types.py,sha256=qIeHW_gT7f291vteTZXygAM8oAaPp2dq6uEdvOyOwzs,5488
|
|
57
|
-
hindsight_api-0.1.
|
|
58
|
-
hindsight_api-0.1.
|
|
59
|
-
hindsight_api-0.1.
|
|
60
|
-
hindsight_api-0.1.
|
|
58
|
+
hindsight_api-0.1.4.dist-info/METADATA,sha256=voQtL9sUm15LNX0M9tiyybr-nl9SwuSZMYU2pk9kRL4,1466
|
|
59
|
+
hindsight_api-0.1.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
60
|
+
hindsight_api-0.1.4.dist-info/entry_points.txt,sha256=ZDj1gJCi6Ga6VLdPgRSrRizQ4dUTreefjeG_tO1CuHk,58
|
|
61
|
+
hindsight_api-0.1.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|