Skip to content

Commit fa554b8

Browse files
committed
brandind and misc fixes
1 parent f813a80 commit fa554b8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+3842
-3356
lines changed

AGENTS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,7 @@ Note: The maintained wrapper `hindsight_client.py` and `README.md` are preserved
145145
- PostgreSQL with pgvector extension
146146
- Schema managed via Alembic migrations in `hindsight-api/alembic/`, db migrations happen during api startup, no manual commands
147147
- Key tables: `banks`, `memory_units`, `documents`, `entities`, `entity_links`
148+
149+
# Branding
150+
## Colors
151+
- Primary: gradient from #0074d9 to #009296

README.md

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44

55
[Documentation](https://vectorize-io.github.io/hindsight)[Paper](#coming-soon)[Examples](https://github.com/vectorize-io/hindsight-cookbook)
66

7-
[![CI](https://github.com/vectorize-io/hindsight/actions/workflows/test.yml/badge.svg)](https://github.com/vectorize-io/hindsight/actions/workflows/test.yml)
7+
[![CI](https://github.com/vectorize-io/hindsight/actions/workflows/release.yml/badge.svg)](https://github.com/vectorize-io/hindsight/actions/workflows/release.yml)
88
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
9+
[![PyPI - hindsight-api](https://img.shields.io/pypi/v/hindsight-api?label=hindsight-api)](https://pypi.org/project/hindsight-api/)
910
[![PyPI - hindsight-client](https://img.shields.io/pypi/v/hindsight-client?label=hindsight-client)](https://pypi.org/project/hindsight-client/)
10-
[![npm](https://img.shields.io/npm/v/@vectorize-io/hindsight-client)](https://www.npmjs.com/package/@vectorize-io/hindsight-client)
11+
[![npm - @vectorize-io/hindsight-client](https://img.shields.io/npm/v/@vectorize-io/hindsight-client)](https://www.npmjs.com/package/@vectorize-io/hindsight-client)
1112
[![Slack Community](https://img.shields.io/badge/Slack-Join%20Community-4A154B?logo=slack)](https://join.slack.com/t/hindsight-space/shared_invite/zt-3klo21kua-VUCC_zHP5rIcXFB1_5yw6A)
1213

1314

@@ -53,12 +54,10 @@ Memories in Hindsight are stored in banks (e.g. memory banks). When memories are
5354
```bash
5455
export OPENAI_API_KEY=your-key
5556

56-
docker run -p 8888:8888 -p 9999:9999 \
57-
-e HINDSIGHT_API_LLM_PROVIDER=openai \
57+
docker run --rm -it --pull always -p 8888:8888 -p 9999:9999 \
5858
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
59-
-e HINDSIGHT_API_LLM_MODEL=gpt-4o-mini \
6059
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
61-
ghcr.io/vectorize-io/hindsight
60+
ghcr.io/vectorize-io/hindsight:latest
6261
```
6362

6463
API: http://localhost:8888
@@ -208,29 +207,18 @@ client.reflect(bank_id="my-bank", query="What should I know about Alice?")
208207

209208
![Retain Operation](hindsight-docs/static/img/reflect-operation.webp)
210209

211-
## Integrations
212-
213-
### Examples
214-
215-
[Examples Repo]([./examples](https://github.com/vectorize-io/hindsight-cookbook)) includes:
216-
217-
- Basic usage
218-
- Multi-session conversations
219-
- Temporal queries
220-
- Entity reasoning
221-
- Opinion tracking
222-
- Production setup (Docker Compose + monitoring)
223-
224210
---
225211

226212
## Resources
227213

228-
**Documentation:** [vectorize-io.github.io/hindsight](https://vectorize-io.github.io/hindsight)
214+
**Documentation:**
215+
- [https://hindsight.vectorize.io](https://hindsight.vectorize.io)
229216

230217
**Clients:**
231218
- [Python](http://hindsight.vectorize.io/sdks/python)
232219
- [Node.js](http://hindsight.vectorize.io/sdks/nodejs)
233-
- [REST API](http://hindsight.vectorize.io/api-reference)
220+
- [REST API](https://hindsight.vectorize.io/api-reference)
221+
- [CLI](https://hindsight.vectorize.io/sdks/cli)
234222

235223
**Community:**
236224
- [Slack](https://join.slack.com/t/hindsight-space/shared_invite/zt-3klo21kua-VUCC_zHP5rIcXFB1_5yw6A)

docker/standalone/start-all.sh

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
#!/bin/bash
22
set -e
33

4-
echo "🚀 Starting Hindsight..."
5-
echo ""
6-
74
# Service flags (default to true if not set)
85
ENABLE_API="${HINDSIGHT_ENABLE_API:-true}"
96
ENABLE_CP="${HINDSIGHT_ENABLE_CP:-true}"
@@ -31,16 +28,14 @@ if [ "$ENABLE_API" = "true" ]; then
3128
PIDS+=($API_PID)
3229

3330
# Wait for API to be ready
34-
echo "⏳ Waiting for API..."
3531
for i in {1..60}; do
3632
if curl -sf http://localhost:8888/health &>/dev/null; then
37-
echo "✅ API is ready"
3833
break
3934
fi
4035
sleep 1
4136
done
4237
else
43-
echo "⏭️ API disabled (HINDSIGHT_ENABLE_API=false)"
38+
echo "API disabled (HINDSIGHT_ENABLE_API=false)"
4439
fi
4540

4641
# Start Control Plane if enabled
@@ -51,7 +46,7 @@ if [ "$ENABLE_CP" = "true" ]; then
5146
CP_PID=$!
5247
PIDS+=($CP_PID)
5348
else
54-
echo "⏭️ Control Plane disabled (HINDSIGHT_ENABLE_CP=false)"
49+
echo "Control Plane disabled (HINDSIGHT_ENABLE_CP=false)"
5550
fi
5651

5752
# Print status

hindsight-api/hindsight_api/api/http.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -672,11 +672,15 @@ class DeleteResponse(BaseModel):
672672
"""Response model for delete operations."""
673673
model_config = ConfigDict(json_schema_extra={
674674
"example": {
675-
"success": True
675+
"success": True,
676+
"message": "Deleted successfully",
677+
"deleted_count": 10
676678
}
677679
})
678680

679681
success: bool
682+
message: Optional[str] = None
683+
deleted_count: Optional[int] = None
680684

681685

682686
def create_app(memory: MemoryEngine, initialize_memory: bool = True) -> FastAPI:
@@ -1696,6 +1700,31 @@ async def api_create_or_update_bank(bank_id: str,
16961700
raise HTTPException(status_code=500, detail=str(e))
16971701

16981702

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+
16991728
@app.post(
17001729
"/v1/default/banks/{bank_id}/memories",
17011730
response_model=RetainResponse,
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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/hindsight_api/config.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@
3232

3333
# Default values
3434
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"
3737

3838
DEFAULT_EMBEDDINGS_PROVIDER = "local"
3939
DEFAULT_EMBEDDINGS_LOCAL_MODEL = "BAAI/bge-small-en-v1.5"

hindsight-api/hindsight_api/engine/llm_wrapper.py

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,12 +91,35 @@ def __init__(
9191
self._client = AsyncOpenAI(api_key="ollama", base_url=self.base_url, max_retries=0)
9292
self._gemini_client = None
9393
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)
9599
self._gemini_client = None
96100

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
100123

101124
async def call(
102125
self,
@@ -149,7 +172,12 @@ async def call(
149172

150173
if max_completion_tokens is not None:
151174
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:
153181
call_params["temperature"] = temperature
154182

155183
# Provider-specific parameters
@@ -216,7 +244,8 @@ async def call(
216244
except APIConnectionError as e:
217245
last_exception = e
218246
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}")
220249
backoff = min(initial_backoff * (2 ** attempt), max_backoff)
221250
await asyncio.sleep(backoff)
222251
continue

hindsight-api/hindsight_api/engine/memory_engine.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -453,12 +453,17 @@ async def init_query_analyzer():
453453
# Query analyzer load is sync and CPU-bound
454454
await loop.run_in_executor(None, self.query_analyzer.load)
455455

456+
async def verify_llm():
457+
"""Verify LLM connection is working."""
458+
await self._llm_config.verify_connection()
459+
456460
# Run pg0 and all model initializations in parallel
457461
await asyncio.gather(
458462
start_pg0(),
459463
init_embeddings(),
460464
init_cross_encoder(),
461465
init_query_analyzer(),
466+
verify_llm(),
462467
)
463468

464469
# Run database migrations if enabled
@@ -1791,10 +1796,14 @@ async def delete_bank(self, bank_id: str, fact_type: Optional[str] = None) -> Di
17911796
# Delete entities (cascades to unit_entities, entity_cooccurrences, memory_links with entity_id)
17921797
await conn.execute("DELETE FROM entities WHERE bank_id = $1", bank_id)
17931798

1799+
# Delete the bank profile itself
1800+
await conn.execute("DELETE FROM banks WHERE bank_id = $1", bank_id)
1801+
17941802
return {
17951803
"memory_units_deleted": units_count,
17961804
"entities_deleted": entities_count,
1797-
"documents_deleted": documents_count
1805+
"documents_deleted": documents_count,
1806+
"bank_deleted": True
17981807
}
17991808

18001809
except Exception as e:
@@ -1839,10 +1848,11 @@ async def get_graph_data(self, bank_id: Optional[str] = None, fact_type: Optiona
18391848
""", *query_params)
18401849

18411850
# Get links, filtering to only include links between units of the selected agent
1851+
# Use DISTINCT ON with LEAST/GREATEST to deduplicate bidirectional links
18421852
unit_ids = [row['id'] for row in units]
18431853
if unit_ids:
18441854
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))
18461856
ml.from_unit_id,
18471857
ml.to_unit_id,
18481858
ml.link_type,
@@ -1851,7 +1861,7 @@ async def get_graph_data(self, bank_id: Optional[str] = None, fact_type: Optiona
18511861
FROM memory_links ml
18521862
LEFT JOIN entities e ON ml.entity_id = e.id
18531863
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
18551865
""", unit_ids)
18561866
else:
18571867
links = []

0 commit comments

Comments
 (0)