hindsight-api 0.1.1__tar.gz → 0.1.3__tar.gz

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 (61) hide show
  1. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/PKG-INFO +1 -1
  2. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/api/http.py +30 -1
  3. hindsight_api-0.1.3/hindsight_api/banner.py +89 -0
  4. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/config.py +2 -2
  5. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/engine/llm_wrapper.py +35 -6
  6. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/engine/memory_engine.py +13 -3
  7. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/engine/retain/link_utils.py +51 -1
  8. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/main.py +17 -9
  9. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/pg0.py +8 -7
  10. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/pyproject.toml +1 -1
  11. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/.gitignore +0 -0
  12. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/README.md +0 -0
  13. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/__init__.py +0 -0
  14. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/alembic/README +0 -0
  15. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/alembic/env.py +0 -0
  16. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/alembic/script.py.mako +0 -0
  17. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/alembic/versions/5a366d414dce_initial_schema.py +0 -0
  18. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/alembic/versions/b7c4d8e9f1a2_add_chunks_table.py +0 -0
  19. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/alembic/versions/c8e5f2a3b4d1_add_retain_params_to_documents.py +0 -0
  20. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/alembic/versions/d9f6a3b4c5e2_rename_bank_to_interactions.py +0 -0
  21. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/alembic/versions/e0a1b2c3d4e5_disposition_to_3_traits.py +0 -0
  22. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/alembic/versions/rename_personality_to_disposition.py +0 -0
  23. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/api/__init__.py +0 -0
  24. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/api/mcp.py +0 -0
  25. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/engine/__init__.py +0 -0
  26. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/engine/cross_encoder.py +0 -0
  27. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/engine/db_utils.py +0 -0
  28. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/engine/embeddings.py +0 -0
  29. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/engine/entity_resolver.py +0 -0
  30. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/engine/query_analyzer.py +0 -0
  31. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/engine/response_models.py +0 -0
  32. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/engine/retain/__init__.py +0 -0
  33. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/engine/retain/bank_utils.py +0 -0
  34. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/engine/retain/chunk_storage.py +0 -0
  35. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/engine/retain/deduplication.py +0 -0
  36. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/engine/retain/embedding_processing.py +0 -0
  37. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/engine/retain/embedding_utils.py +0 -0
  38. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/engine/retain/entity_processing.py +0 -0
  39. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/engine/retain/fact_extraction.py +0 -0
  40. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/engine/retain/fact_storage.py +0 -0
  41. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/engine/retain/link_creation.py +0 -0
  42. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/engine/retain/observation_regeneration.py +0 -0
  43. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/engine/retain/orchestrator.py +0 -0
  44. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/engine/retain/types.py +0 -0
  45. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/engine/search/__init__.py +0 -0
  46. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/engine/search/fusion.py +0 -0
  47. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/engine/search/observation_utils.py +0 -0
  48. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/engine/search/reranking.py +0 -0
  49. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/engine/search/retrieval.py +0 -0
  50. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/engine/search/scoring.py +0 -0
  51. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/engine/search/temporal_extraction.py +0 -0
  52. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/engine/search/think_utils.py +0 -0
  53. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/engine/search/trace.py +0 -0
  54. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/engine/search/tracer.py +0 -0
  55. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/engine/search/types.py +0 -0
  56. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/engine/task_backend.py +0 -0
  57. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/engine/utils.py +0 -0
  58. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/metrics.py +0 -0
  59. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/migrations.py +0 -0
  60. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/models.py +0 -0
  61. {hindsight_api-0.1.1 → hindsight_api-0.1.3}/hindsight_api/server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hindsight-api
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: Temporal + Semantic + Entity Memory System for AI agents using PostgreSQL
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: alembic>=1.17.1
@@ -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,
@@ -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()
@@ -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 = "groq"
36
- DEFAULT_LLM_MODEL = "openai/gpt-oss-20b"
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
- self._client = AsyncOpenAI(api_key=self.api_key, base_url=self.base_url, max_retries=0)
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
- logger.info(
98
- f"Initialized LLM: provider={self.provider}, model={self.model}, base_url={self.base_url}"
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,
@@ -149,7 +172,12 @@ class LLMProvider:
149
172
 
150
173
  if max_completion_tokens is not None:
151
174
  call_params["max_completion_tokens"] = max_completion_tokens
152
- if temperature is not None:
175
+ # Check if model supports reasoning parameter (o1, o3, gpt-5 families)
176
+ model_lower = self.model.lower()
177
+ is_reasoning_model = any(x in model_lower for x in ["gpt-5", "o1", "o3"])
178
+
179
+ # GPT-5/o1/o3 family doesn't support custom temperature (only default 1)
180
+ if temperature is not None and not is_reasoning_model:
153
181
  call_params["temperature"] = temperature
154
182
 
155
183
  # Provider-specific parameters
@@ -216,7 +244,8 @@ class LLMProvider:
216
244
  except APIConnectionError as e:
217
245
  last_exception = e
218
246
  if attempt < max_retries:
219
- logger.warning(f"Connection error, retrying... (attempt {attempt + 1}/{max_retries + 1})")
247
+ status_code = getattr(e, 'status_code', None) or getattr(getattr(e, 'response', None), 'status_code', None)
248
+ logger.warning(f"Connection error, retrying... (attempt {attempt + 1}/{max_retries + 1}) - status_code={status_code}, message={e}")
220
249
  backoff = min(initial_backoff * (2 ** attempt), max_backoff)
221
250
  await asyncio.sleep(backoff)
222
251
  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
- similarity = float(similarities[idx])
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:
@@ -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
- print(f"\nStarting Hindsight API...")
188
- print(f" URL: http://{args.host}:{args.port}")
189
- print(f" Database: {config.database_url}")
190
- print(f" LLM: {config.llm_provider} / {config.llm_model}")
191
- print(f" Embeddings: {config.embeddings_provider}")
192
- print(f" Reranker: {config.reranker_provider}")
193
- if config.mcp_enabled:
194
- print(f" MCP: enabled at /mcp")
195
- print()
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
 
@@ -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.warning(f"pg0 start attempt {attempt}/{max_retries} failed: {last_error.strip()}")
261
- logger.info(f"Retrying in {delay:.1f}s...")
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.warning(f"pg0 start attempt {attempt}/{max_retries} failed: {last_error.strip()}")
264
+ logger.debug(f"pg0 start attempt {attempt}/{max_retries} failed: {last_error.strip()}")
265
265
 
266
- # All retries exhausted - use constructed URI as fallback
267
- uri = f"postgresql://{self.username}:{self.password}@localhost:{self.port}/{self.database}"
268
- logger.warning(f"All pg0 start attempts failed, using constructed URI: {uri}")
269
- return uri
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."""
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "hindsight-api"
7
- version = "0.1.1"
7
+ version = "0.1.3"
8
8
  description = "Temporal + Semantic + Entity Memory System for AI agents using PostgreSQL"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
File without changes
File without changes