Compare commits

...
40 changed files with 2543 additions and 5901 deletions
+2
View File
@@ -27,7 +27,9 @@ jobs:
node-version: 20
cache: npm
cache-dependency-path: package-lock.json
- uses: astral-sh/setup-uv@v4
- run: npm ci --workspace=hindsight-docs
- run: uv run generate-llms-full
- run: npm run build --workspace=hindsight-docs
- uses: actions/upload-pages-artifact@v3
with:
+3
View File
@@ -32,6 +32,9 @@ logs/
.DS_Store
# Generated docs files
hindsight-docs/static/llms-full.txt
hindsight-dev/benchmarks/locomo/results/
hindsight-dev/benchmarks/longmemeval/results/
+2 -2
View File
@@ -60,8 +60,8 @@ WORKDIR /app
COPY package.json package-lock.json ./
COPY hindsight-clients/typescript/ ./hindsight-clients/typescript/
# Install and build SDK using workspace
RUN npm ci -w @vectorize-io/hindsight-client
# Install and build SDK using workspace (--ignore-scripts skips git hooks setup)
RUN npm ci --ignore-scripts -w @vectorize-io/hindsight-client
RUN npm run build -w @vectorize-io/hindsight-client
# =============================================================================
+14 -6
View File
@@ -6,7 +6,7 @@ Shows the logo and tagline with gradient colors.
# Gradient colors: #0074d9 -> #009296
GRADIENT_START = (0, 116, 217) # #0074d9
GRADIENT_END = (0, 146, 150) # #009296
GRADIENT_END = (0, 146, 150) # #009296
# Pre-generated logo (generated by test-logo.py)
LOGO = """\
@@ -28,11 +28,12 @@ def _interpolate_color(start: tuple, end: tuple, t: float) -> tuple:
def gradient_text(text: str, start: tuple = GRADIENT_START, end: tuple = GRADIENT_END) -> str:
"""Render text with a gradient color effect."""
result = []
length = len(text)
for i, char in enumerate(text):
if char == ' ':
result.append(' ')
if char == " ":
result.append(" ")
else:
t = i / max(length - 1, 1)
r, g, b = _interpolate_color(start, end, t)
@@ -74,9 +75,16 @@ def dim(text: str) -> str:
return f"\033[38;2;128;128;128m{text}\033[0m"
def print_startup_info(host: str, port: int, database_url: str, llm_provider: str,
llm_model: str, embeddings_provider: str, reranker_provider: str,
mcp_enabled: bool = False):
def print_startup_info(
host: str,
port: int,
database_url: str,
llm_provider: str,
llm_model: str,
embeddings_provider: str,
reranker_provider: str,
mcp_enabled: bool = False,
):
"""Print styled startup information."""
print(color_start("Starting Hindsight API..."))
print(f" {dim('URL:')} {color(f'http://{host}:{port}', 0.2)}")
+1 -1
View File
@@ -44,7 +44,7 @@ class EmbeddedPostgres:
self._pg0 = Pg0(**kwargs)
return self._pg0
async def start(self, max_retries: int = 3, retry_delay: float = 2.0) -> str:
async def start(self, max_retries: int = 5, retry_delay: float = 4.0) -> str:
"""Start the PostgreSQL server with retry logic."""
port_info = f"port={self.port}" if self.port else "port=auto"
logger.info(f"Starting embedded PostgreSQL (name={self.name}, {port_info})...")
+24 -1
View File
@@ -29,7 +29,7 @@ dependencies = [
"tiktoken>=0.12.0",
"httpx>=0.27.0",
"fastmcp>=2.3.0",
"pg0-embedded>=0.1.0",
"pg0-embedded>=0.11.0",
"python-dateutil>=2.8.0",
"opentelemetry-api>=1.20.0",
"opentelemetry-sdk>=1.20.0",
@@ -91,4 +91,27 @@ dev = [
"pytest-xdist>=3.8.0",
"python-dotenv>=1.2.1",
"filelock>=3.0.0",
"ruff>=0.8.0",
]
[tool.ruff]
line-length = 120
target-version = "py311"
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # Pyflakes
"I", # isort
"B", # flake8-bugbear
"UP", # pyupgrade
]
ignore = [
"E501", # line too long (handled by formatter)
"B008", # do not perform function calls in argument defaults
]
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
+7
View File
@@ -0,0 +1,7 @@
{
"semi": true,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 100
}
+3
View File
@@ -48,5 +48,8 @@
"tailwindcss-animate": "^1.0.7",
"three": "^0.182.0",
"typescript": "^5.9.3"
},
"devDependencies": {
"prettier": "^3.7.4"
}
}
@@ -0,0 +1,156 @@
#!/usr/bin/env python3
"""
Generates llms-full.txt by concatenating all documentation markdown files.
This file is used by LLMs to understand the full documentation.
Usage: generate-llms-full (after installing hindsight-dev)
Output: hindsight-docs/static/llms-full.txt (served at /llms-full.txt)
"""
import re
from datetime import datetime, timezone
from pathlib import Path
# Order matters - more important docs first
DOC_ORDER = [
"developer/index.md",
"developer/api/quickstart.md",
"developer/api/main-methods.md",
"developer/retain.md",
"developer/retrieval.md",
"developer/reflect.md",
"developer/api/retain.md",
"developer/api/recall.md",
"developer/api/reflect.md",
"developer/api/memory-banks.md",
"developer/api/entities.md",
"developer/api/documents.md",
"developer/api/operations.md",
"developer/installation.md",
"developer/configuration.md",
"developer/models.md",
"developer/rag-vs-hindsight.md",
"sdks/python.md",
"sdks/nodejs.md",
"sdks/cli.md",
"sdks/mcp.md",
"cookbook/index.mdx",
"cookbook/recipes/quickstart.md",
"cookbook/recipes/per-user-memory.md",
"cookbook/recipes/support-agent-shared-knowledge.md",
"cookbook/applications/openai-fitness-coach.md",
]
def get_docs_dir() -> Path:
"""Find the hindsight-docs directory relative to this script."""
script_dir = Path(__file__).parent
return script_dir.parent.parent / "hindsight-docs" / "docs"
def get_output_file() -> Path:
"""Get output file path."""
script_dir = Path(__file__).parent
return script_dir.parent.parent / "hindsight-docs" / "static" / "llms-full.txt"
def get_all_markdown_files(docs_dir: Path) -> list[str]:
"""Recursively find all markdown files."""
files = []
for path in docs_dir.rglob("*"):
if path.suffix in (".md", ".mdx"):
files.append(str(path.relative_to(docs_dir)))
return files
def strip_frontmatter(content: str) -> str:
"""Remove YAML frontmatter (between --- markers)."""
return re.sub(r"^---\n[\s\S]*?\n---\n", "", content)
def clean_markdown(content: str) -> str:
"""Clean markdown content for LLM consumption."""
cleaned = strip_frontmatter(content)
# Remove import statements
cleaned = re.sub(r"^import\s+.*$", "", cleaned, flags=re.MULTILINE)
# Remove JSX components (like <RecipeCarousel ... />)
cleaned = re.sub(r"<[A-Z][a-zA-Z]*\s+[^>]*/>", "", cleaned)
cleaned = re.sub(r"<[A-Z][a-zA-Z]*[^>]*>[\s\S]*?</[A-Z][a-zA-Z]*>", "", cleaned)
# Remove empty lines at start
cleaned = re.sub(r"^\n+", "", cleaned)
return cleaned
def main():
"""Generate llms-full.txt."""
print("Generating llms-full.txt...")
docs_dir = get_docs_dir()
output_file = get_output_file()
# Get all markdown files
all_files = get_all_markdown_files(docs_dir)
# Create ordered list: prioritized files first, then remaining files
ordered_files = []
remaining_files = set(all_files)
# Add prioritized files in order
for file in DOC_ORDER:
if file in remaining_files:
ordered_files.append(file)
remaining_files.discard(file)
# Add remaining files (sorted alphabetically)
ordered_files.extend(sorted(remaining_files))
# Build the output
sections = []
# Header
timestamp = datetime.now(timezone.utc).isoformat()
sections.append(f"""# Hindsight Documentation
> Agent Memory that Works Like Human Memory
This file contains the complete Hindsight documentation for LLM consumption.
Generated: {timestamp}
---
""")
# Process each file
for file in ordered_files:
file_path = docs_dir / file
if not file_path.exists():
print(f" Warning: {file} not found, skipping")
continue
content = file_path.read_text()
cleaned_content = clean_markdown(content)
if cleaned_content.strip():
sections.append(f"\n## File: {file}\n")
sections.append(cleaned_content)
sections.append("\n---\n")
print(f" Added: {file}")
# Write output
output = "\n".join(sections)
output_file.write_text(output)
size_kb = output_file.stat().st_size / 1024
print(f"\nGenerated: {output_file}")
print(f"Size: {size_kb:.1f} KB")
print(f"Files included: {len(ordered_files)}")
if __name__ == "__main__":
main()
@@ -0,0 +1,514 @@
#!/usr/bin/env python3
"""
Syncs content from the hindsight-cookbook repository.
- Clones the cookbook repo to a temp directory
- Converts notebooks/*.ipynb → docs/cookbook/recipes/*.md
- Converts app directories (with README.md) → docs/cookbook/applications/*.md
- Updates sidebars.ts with the new entries
Usage: sync-cookbook (after installing hindsight-dev)
Conventions in cookbook repo:
- notebooks/*.ipynb → Recipes (use cases, tutorials)
- Directories with README.md at root → Applications (complete apps)
- Notebook title extracted from first # heading in first markdown cell
- App title extracted from first # heading in README.md
"""
import json
import os
import re
import shutil
import subprocess
import tempfile
from pathlib import Path
COOKBOOK_REPO = "https://github.com/vectorize-io/hindsight-cookbook.git"
IGNORE_DIRS = {".git", "notebooks", "node_modules", "__pycache__", ".venv", "venv"}
def get_docs_dir() -> Path:
"""Find the hindsight-docs directory relative to this script."""
# Navigate from hindsight-dev to hindsight-docs
script_dir = Path(__file__).parent
docs_dir = script_dir.parent.parent / "hindsight-docs" / "docs" / "cookbook"
return docs_dir
def get_sidebars_file() -> Path:
script_dir = Path(__file__).parent
return script_dir.parent.parent / "hindsight-docs" / "sidebars.ts"
def slugify(filename: str) -> str:
"""Convert filename to slug. e.g., '01-quickstart.ipynb''quickstart'"""
slug = re.sub(r"\.ipynb$", "", filename)
slug = re.sub(r"\.md$", "", slug)
slug = re.sub(r"^\d+-", "", slug)
return slug
def extract_title_from_notebook(notebook_path: Path) -> str:
"""Extract title from first markdown cell's # heading."""
try:
content = json.loads(notebook_path.read_text())
for cell in content.get("cells", []):
if cell.get("cell_type") == "markdown":
source = cell.get("source", [])
if isinstance(source, list):
source = "".join(source)
match = re.search(r"^#\s+(.+)$", source, re.MULTILINE)
if match:
return match.group(1).strip()
except Exception as e:
print(f" Warning: Could not parse notebook {notebook_path}: {e}")
# Fallback to filename
slug = slugify(notebook_path.name)
return " ".join(word.capitalize() for word in slug.split("-"))
def extract_description_from_notebook(notebook_path: Path) -> str | None:
"""Extract first paragraph after title from notebook."""
try:
content = json.loads(notebook_path.read_text())
for cell in content.get("cells", []):
if cell.get("cell_type") == "markdown":
source = cell.get("source", [])
if isinstance(source, list):
source = "".join(source)
lines = source.split("\n")
found_title = False
description = []
for line in lines:
if line.startswith("#"):
found_title = True
continue
if found_title and line.strip():
if line.startswith("#"):
break
description.append(line.strip())
if line.strip().endswith("."):
break
if description:
return " ".join(description)[:200]
except Exception:
pass
return None
def extract_title_from_readme(readme_path: Path) -> str | None:
"""Extract title from README's first # heading."""
try:
content = readme_path.read_text()
match = re.search(r"^#\s+(.+)$", content, re.MULTILINE)
if match:
return match.group(1).strip()
except Exception as e:
print(f" Warning: Could not read {readme_path}: {e}")
return None
def convert_notebook_to_markdown(notebook_path: Path) -> str:
"""Convert Jupyter notebook to markdown.
Uses nbconvert with --no-input to exclude outputs (which often contain
characters that break MDX parsing).
"""
# Try nbconvert first
try:
with tempfile.TemporaryDirectory() as tmpdir:
subprocess.run(
[
"jupyter",
"nbconvert",
"--to",
"markdown",
"--TemplateExporter.exclude_output=True", # Exclude cell outputs
str(notebook_path),
"--output-dir",
tmpdir,
],
capture_output=True,
check=True,
)
md_file = Path(tmpdir) / notebook_path.with_suffix(".md").name
if md_file.exists():
return md_file.read_text()
except Exception as e:
print(f" Warning: nbconvert failed ({e}), using fallback parser")
# Fallback: manual conversion
return convert_notebook_manually(notebook_path)
def convert_notebook_manually(notebook_path: Path) -> str:
"""Manually convert notebook to markdown.
Note: We skip cell outputs to avoid MDX parsing issues (outputs often contain
characters like < and > that get interpreted as JSX tags).
"""
content = json.loads(notebook_path.read_text())
parts = []
lang = content.get("metadata", {}).get("kernelspec", {}).get("language", "python")
for cell in content.get("cells", []):
source = cell.get("source", [])
if isinstance(source, list):
source = "".join(source)
if cell.get("cell_type") == "markdown":
parts.append(source)
elif cell.get("cell_type") == "code":
parts.append(f"```{lang}\n{source}\n```")
# Skip outputs - they often contain characters that break MDX parsing
return "\n\n".join(parts)
def process_notebooks(cookbook_dir: Path, recipes_dir: Path) -> list[dict]:
"""Process all notebooks and convert to recipe markdown files."""
notebooks_dir = cookbook_dir / "notebooks"
recipes = []
if not notebooks_dir.exists():
print(" No notebooks directory found")
return recipes
files = sorted(f for f in notebooks_dir.iterdir() if f.suffix == ".ipynb")
print(f" Found {len(files)} notebooks")
for i, notebook_path in enumerate(files):
slug = slugify(notebook_path.name)
title = extract_title_from_notebook(notebook_path)
description = extract_description_from_notebook(notebook_path)
print(f" Processing: {notebook_path.name}{slug}.md")
# Convert notebook to markdown
md_content = convert_notebook_to_markdown(notebook_path)
# Create recipe page with frontmatter
notebook_url = f"https://github.com/vectorize-io/hindsight-cookbook/blob/main/notebooks/{notebook_path.name}"
frontmatter = f"""---
sidebar_position: {i + 1}
---
"""
callout = f"""
:::tip Run this notebook
This recipe is available as an interactive Jupyter notebook.
[**Open in GitHub →**]({notebook_url})
:::
"""
# Insert callout after first heading
first_heading_match = re.search(r"^(#\s+.+\n)", md_content, re.MULTILINE)
if first_heading_match:
idx = md_content.index(first_heading_match.group(0)) + len(
first_heading_match.group(0)
)
final_content = md_content[:idx] + "\n" + callout + "\n" + md_content[idx:]
else:
final_content = callout + "\n" + md_content
output_path = recipes_dir / f"{slug}.md"
output_path.write_text(frontmatter + final_content)
recipes.append(
{
"slug": slug,
"title": title,
"description": description,
"id": f"cookbook/recipes/{slug}",
}
)
return recipes
def process_applications(cookbook_dir: Path, apps_dir: Path) -> list[dict]:
"""Process application directories with README.md."""
apps = []
for entry in sorted(cookbook_dir.iterdir()):
if not entry.is_dir() or entry.name in IGNORE_DIRS:
continue
readme_path = entry / "README.md"
if not readme_path.exists():
continue
slug = entry.name
title = extract_title_from_readme(readme_path) or " ".join(
word.capitalize() for word in slug.split("-")
)
print(f" Processing app: {entry.name}{slug}.md")
# Read README content
readme_content = readme_path.read_text()
# Create application page with frontmatter
app_url = f"https://github.com/vectorize-io/hindsight-cookbook/tree/main/{entry.name}"
frontmatter = f"""---
sidebar_position: {len(apps) + 1}
---
"""
callout = f"""
:::info Complete Application
This is a complete, runnable application demonstrating Hindsight integration.
[**View source on GitHub →**]({app_url})
:::
"""
# Insert callout after first heading
first_heading_match = re.search(r"^(#\s+.+\n)", readme_content, re.MULTILINE)
if first_heading_match:
idx = readme_content.index(first_heading_match.group(0)) + len(
first_heading_match.group(0)
)
final_content = (
readme_content[:idx] + "\n" + callout + "\n" + readme_content[idx:]
)
else:
final_content = callout + "\n" + readme_content
output_path = apps_dir / f"{slug}.md"
output_path.write_text(frontmatter + final_content)
apps.append(
{
"slug": slug,
"title": title,
"id": f"cookbook/applications/{slug}",
}
)
return apps
def update_sidebars(recipes: list[dict], apps: list[dict], sidebars_file: Path):
"""Update sidebars.ts with new recipe and app entries."""
content = sidebars_file.read_text()
# Build recipe items
recipe_item_list = []
for r in recipes:
label = r["title"].replace("'", "\\'")
recipe_item_list.append(
f""" {{
type: 'doc',
id: '{r["id"]}',
label: '{label}',
}}"""
)
recipe_items = ",\n".join(recipe_item_list)
# Build app items
app_item_list = []
for a in apps:
label = a["title"].replace("'", "\\'")
app_item_list.append(
f""" {{
type: 'doc',
id: '{a["id"]}',
label: '{label}',
}}"""
)
app_items = ",\n".join(app_item_list)
new_cookbook_sidebar = f"""cookbookSidebar: [
{{
type: 'doc',
id: 'cookbook/index',
label: 'Overview',
}},
{{
type: 'category',
label: 'Recipes',
collapsible: false,
items: [
{recipe_items}
],
}},
{{
type: 'category',
label: 'Applications',
collapsible: false,
items: [
{app_items}
],
}},
]"""
# Replace existing cookbookSidebar - match the full sidebar array including nested structures
# We need to match balanced brackets
start = content.find("cookbookSidebar:")
if start == -1:
raise ValueError("cookbookSidebar not found in sidebars.ts")
# Find the opening bracket
bracket_start = content.find("[", start)
if bracket_start == -1:
raise ValueError("Could not find opening bracket for cookbookSidebar")
# Find matching closing bracket by counting brackets
depth = 0
end = bracket_start
for i, char in enumerate(content[bracket_start:], bracket_start):
if char == "[":
depth += 1
elif char == "]":
depth -= 1
if depth == 0:
end = i + 1
break
# Include trailing comma if present
if end < len(content) and content[end] == ",":
end += 1
content = content[:start] + new_cookbook_sidebar + "," + content[end:]
sidebars_file.write_text(content)
print("\nUpdated sidebars.ts")
def clean_description(desc: str) -> str:
"""Clean description for display in carousel cards."""
if not desc:
return ""
# Remove markdown formatting
desc = re.sub(r"\*\*([^*]+)\*\*", r"\1", desc) # Bold
desc = re.sub(r"\*([^*]+)\*", r"\1", desc) # Italic
desc = re.sub(r"`([^`]+)`", r"\1", desc) # Code
desc = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", desc) # Links
desc = re.sub(r"^[-*]\s+", "", desc) # List items
desc = re.sub(r"\s+", " ", desc).strip() # Normalize whitespace
# Truncate at sentence boundary or max length
if len(desc) > 120:
# Try to cut at sentence
period_idx = desc.rfind(".", 0, 120)
if period_idx > 60:
desc = desc[: period_idx + 1]
else:
desc = desc[:117] + "..."
return desc
def update_cookbook_index(
recipes: list[dict], apps: list[dict], docs_dir: Path
):
"""Update cookbook/index.mdx with recipe and app carousels."""
# Build recipe items for the carousel
recipe_items = []
for r in recipes:
title = r["title"].replace('"', '\\"')
recipe_items.append(
f' {{ title: "{title}", href: "/cookbook/recipes/{r["slug"]}" }}'
)
recipes_json = ",\n".join(recipe_items)
# Build app items for the carousel
app_items = []
for a in apps:
title = a["title"].replace('"', '\\"')
app_items.append(
f' {{ title: "{title}", href: "/cookbook/applications/{a["slug"]}" }}'
)
apps_json = ",\n".join(app_items)
content = f"""---
sidebar_position: 1
---
import RecipeCarousel from '@site/src/components/RecipeCarousel';
# Cookbook
Practical patterns, recipes, and complete applications for building with Hindsight.
<RecipeCarousel
title="Recipes"
items={{[
{recipes_json}
]}}
/>
<RecipeCarousel
title="Applications"
items={{[
{apps_json}
]}}
/>
"""
index_path = docs_dir / "index.mdx"
index_path.write_text(content)
# Remove old .md if exists
old_index = docs_dir / "index.md"
if old_index.exists():
old_index.unlink()
print("Updated cookbook/index.mdx")
def main():
"""Main entry point."""
print("Syncing hindsight-cookbook...\n")
docs_dir = get_docs_dir()
sidebars_file = get_sidebars_file()
recipes_dir = docs_dir / "recipes"
apps_dir = docs_dir / "applications"
# Create temp directory and clone
with tempfile.TemporaryDirectory() as tmpdir:
cookbook_dir = Path(tmpdir) / "cookbook"
print(f"Cloning {COOKBOOK_REPO}...")
subprocess.run(
["git", "clone", "--depth", "1", COOKBOOK_REPO, str(cookbook_dir)],
capture_output=True,
check=True,
)
print("Cloned successfully\n")
# Clean and recreate output directories
if recipes_dir.exists():
shutil.rmtree(recipes_dir)
if apps_dir.exists():
shutil.rmtree(apps_dir)
recipes_dir.mkdir(parents=True, exist_ok=True)
apps_dir.mkdir(parents=True, exist_ok=True)
# Process notebooks → Recipes
print("Processing notebooks...")
recipes = process_notebooks(cookbook_dir, recipes_dir)
# Process app directories → Applications
print("\nProcessing applications...")
apps = process_applications(cookbook_dir, apps_dir)
# Update sidebars.ts and index
if recipes or apps:
update_sidebars(recipes, apps, sidebars_file)
update_cookbook_index(recipes, apps, docs_dir)
print(f"\nDone! Generated {len(recipes)} recipes and {len(apps)} applications")
if __name__ == "__main__":
main()
+2
View File
@@ -25,3 +25,5 @@ hindsight-api = { workspace = true }
[project.scripts]
generate-openapi = "hindsight_dev.generate_openapi:generate_openapi_spec"
generate-changelog = "hindsight_dev.generate_changelog:main"
sync-cookbook = "hindsight_dev.sync_cookbook:main"
generate-llms-full = "hindsight_dev.generate_llms_full:main"
@@ -0,0 +1,315 @@
---
sidebar_position: 1
---
# OpenAI Agent + Hindsight Memory Integration
:::info Complete Application
This is a complete, runnable application demonstrating Hindsight integration.
[**View source on GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/tree/main/openai-fitness-coach)
:::
A fitness coach example demonstrating how to use **OpenAI Agents** with **Hindsight as a memory backend**.
## What This Demonstrates
This example showcases:
- **OpenAI Assistants** handling conversation logic
- **Hindsight** providing sophisticated memory storage & retrieval
- **Function calling** to bridge them together
- **Streaming responses** for real-time interaction (enabled by default)
- **Bidirectional memory** - both user data AND coach observations stored
- **System-level post-processing** - automatic opinion storage for reliability
- **Temporal-semantic memory** queries via function tools
- **Enhanced preference learning** - coach learns and respects user likes/dislikes
- **Real-world integration pattern** for adding memory to AI agents
## Architecture
```
User: "I ran 5K today, don't like tempo runs"
|
OpenAI Assistant
|
Function Call: store_memory(workout + preference)
|
Hindsight API (stores as world/agent)
|
OpenAI Assistant: "What should I focus on?"
|
Function Call: retrieve_memories("workouts and preferences")
|
Hindsight API (returns workouts + preferences)
|
OpenAI Assistant (analyzes, gives advice)
|
Function Call: store_memory(advice as opinion)
|
Hindsight API (stores coach's observation)
|
Personalized Answer
```
## Key Difference from Standard Demo
| Component | Standard Demo | OpenAI Integration |
|-----------|---------------|-------------------|
| **Conversation** | Hindsight `/think` endpoint | OpenAI Assistant API |
| **Memory** | Hindsight (built-in) | Hindsight (via function calling) |
| **LLM** | Configured in Hindsight | OpenAI GPT-4 |
| **Opinion Formation** | Automatic in `/think` | Explicit via `store_memory(type="opinion")` |
| **Best For** | Hindsight-native apps | Integrating memory into existing OpenAI agents |
## Quick Start
### Prerequisites
1. **OpenAI API Key**
```bash
export OPENAI_API_KEY=your_openai_api_key
```
2. **Hindsight API running**
```bash
# Follow Hindsight setup instructions to start the API
# Default: http://localhost:8888
```
3. **Install dependencies**
```bash
pip install openai requests
```
### Run the Conversational Demo
```bash
cd openai-fitness-coach
export OPENAI_API_KEY=your_key_here
python demo_conversational.py
```
The demo showcases:
1. **Natural language workout logging** - Tell the coach what you did conversationally
2. **Preference learning** - Express likes/dislikes and watch the coach adapt
3. **Goal tracking** - Set goals, track progress, achieve milestones
4. **Bidirectional memory** - Both your activities AND coach's advice are stored
5. **Streaming responses** - See responses appear in real-time
6. **7 interactive phases** - From goal setting to achievement recognition
The demo uses a separate agent (`fitness-coach-demo`) to avoid mixing with real data.
## Usage
### Chat with Your Coach
**Interactive mode:**
```bash
python openai_coach.py
```
**Single question:**
```bash
python openai_coach.py "What did I do for training this week?"
```
## How It Works
### 1. Memory Tools (`memory_tools.py`)
Defines function tools that the OpenAI Agent can call:
```python
retrieve_memories(query, fact_types, top_k)
search_workouts(after_date, before_date, workout_type)
get_nutrition_summary(after_date, before_date)
get_user_goals()
get_coach_opinions(about)
```
Each function makes API calls to Hindsight to fetch relevant memories.
### 2. OpenAI Agent (`openai_coach.py`)
Creates an OpenAI Assistant with:
- Fitness coaching instructions
- Access to memory function tools
- Conversation management
When you ask a question:
1. User message is sent to OpenAI Assistant
2. Assistant decides which memory functions to call
3. Functions fetch data from Hindsight
4. Assistant generates response using retrieved context
### 3. Function Calling Flow
```python
# User asks: "What did I run this week?"
# OpenAI Assistant decides to call:
search_workouts(
after_date="2024-11-18",
workout_type="running"
)
# Function retrieves from Hindsight:
{
"results": [
{"text": "User completed 45-minute cardio workout: running..."},
{"text": "User completed 60-minute cardio workout: running..."}
]
}
# OpenAI Assistant generates response:
"This week you've done two runs: a 45-minute run on Monday
and a longer 60-minute run on Wednesday. Great consistency!"
```
## Example Questions
Try asking:
```bash
python openai_coach.py "What does my training look like this week?"
python openai_coach.py "Based on my workouts, should I rest today?"
python openai_coach.py "How is my nutrition supporting my goals?"
python openai_coach.py "What's my progress toward my goal?"
python openai_coach.py "Compare my training this month to last month"
```
The agent will automatically:
1. Identify what memories it needs
2. Call the appropriate function tools
3. Retrieve data from Hindsight
4. Generate a personalized response
## Memory Types Retrieved
The OpenAI Agent can retrieve different memory types from Hindsight:
- **World Facts** (`fact_type: "world"`): Workouts, meals, activities
- **Agent Facts** (`fact_type: "agent"`): Goals, intentions
- **Opinions** (`fact_type: "opinion"`): Coach's observations about patterns
## Customization
### Add New Function Tools
Edit `memory_tools.py` to add new capabilities:
```python
def get_weekly_summary(week_offset: int = 0):
"""Get a summary of a specific week."""
# Implementation
pass
# Add to MEMORY_TOOLS list
MEMORY_TOOLS.append({
"type": "function",
"function": {
"name": "get_weekly_summary",
"description": "Get training summary for a specific week",
# ... parameters
}
})
# Add to FUNCTION_MAP
FUNCTION_MAP["get_weekly_summary"] = get_weekly_summary
```
### Modify Assistant Instructions
Edit `openai_coach.py` to change the coach's personality or behavior:
```python
assistant = client.beta.assistants.create(
name="Your Custom Coach",
instructions="Your custom instructions here...",
model="gpt-4o-mini",
tools=MEMORY_TOOLS
)
```
## Use Cases
This pattern works for any application that needs memory:
1. **Customer Support Agents** - Remember past conversations and issues
2. **Personal Assistants** - Remember preferences, schedules, past decisions
3. **Educational Tutors** - Track learning progress over time
4. **Health Coaches** - Monitor habits, progress, goals (like this example)
5. **Sales Assistants** - Remember customer interactions and preferences
## Integration Pattern
**To add Hindsight memory to your own OpenAI Agent:**
1. Define function tools that call Hindsight API
2. Register them with your OpenAI Assistant
3. Implement function handlers to execute Hindsight queries
4. Let OpenAI Assistant decide when to retrieve memories
The key benefit: **Separation of concerns**
- OpenAI = Conversation logic
- Hindsight = Memory storage, retrieval, temporal queries, entity linking
## When to Use This vs. Standard Hindsight
**Use OpenAI + Hindsight (this example) when:**
- You want OpenAI's conversation capabilities
- You're already using OpenAI Agents
- You want explicit control over when to retrieve memories
- You want to combine Hindsight with other OpenAI features
**Use Hindsight directly when:**
- You want a complete memory-first solution
- You want automatic memory retrieval and opinion formation
- You want to use different LLM providers (not just OpenAI)
- You want the `/think` endpoint's integrated approach
## Learning Points
After running this demo, you'll understand:
1. How to add sophisticated memory to any OpenAI Agent
2. How function calling bridges LLMs and memory systems
3. How temporal-semantic queries work via function tools
4. Real-world pattern for LLM + memory integration
## Core Files
- `demo_conversational.py` - Conversational demo showcasing preference learning and goal tracking
- `openai_coach.py` - OpenAI Assistant wrapper with streaming and memory integration
- `memory_tools.py` - Function calling tools that bridge to Hindsight API
- `.openai_assistant_id` - Saved assistant ID (auto-generated, gitignored)
## Common Issues
**"OPENAI_API_KEY not set"**
```bash
export OPENAI_API_KEY=your_api_key_here
```
**"Agent not found"**
- Make sure the Hindsight fitness-coach agent exists
**"Connection refused"**
- Make sure Hindsight API is running on localhost:8888
## Next Steps
1. Run the demo to see it in action
2. Try chatting with the coach: `python openai_coach.py`
3. Log your own workouts and meals
4. Experiment with different questions
5. Add custom function tools for your use case
---
**Built with:**
- OpenAI Assistants API
- Hindsight (temporal-semantic memory)
- Function calling for integration
-21
View File
@@ -1,21 +0,0 @@
---
sidebar_position: 1
---
# Cookbook
Practical patterns and recipes for building with Hindsight.
## Use Cases
### [Per-User Memory](/cookbook/per-user-memory)
The simplest pattern: give your agent persistent memory for each user. The agent remembers past conversations, preferences, and context across sessions.
**Use when:** Building chatbots, personal assistants, or any 1:1 user-to-agent interaction.
### [Support Agent with Shared Knowledge](/cookbook/support-agent-with-shared-knowledge)
Build a support agent that combines per-user memory with shared product documentation. Users get personalized support while you index docs only once.
**Use when:** Building multi-tenant support agents, RAG + memory applications, or any scenario needing user isolation with shared reference data.
+27
View File
@@ -0,0 +1,27 @@
---
sidebar_position: 1
---
import RecipeCarousel from '@site/src/components/RecipeCarousel';
# Cookbook
Practical patterns, recipes, and complete applications for building with Hindsight.
<RecipeCarousel
title="Recipes"
items={[
{ title: "Hindsight Quickstart", href: "/cookbook/recipes/quickstart" },
{ title: "Per-User Memory", href: "/cookbook/recipes/per-user-memory" },
{ title: "Support Agent with Shared Knowledge", href: "/cookbook/recipes/support-agent-shared-knowledge" },
{ title: "Hindsight Memory Demo with LiteLLM", href: "/cookbook/recipes/litellm-memory-demo" },
{ title: "Hindsight Tool Learning Demo", href: "/cookbook/recipes/tool-learning-demo" }
]}
/>
<RecipeCarousel
title="Applications"
items={[
{ title: "OpenAI Agent + Hindsight Memory Integration", href: "/cookbook/applications/openai-fitness-coach" }
]}
/>
@@ -0,0 +1,187 @@
---
sidebar_position: 4
---
# Hindsight Memory Demo with LiteLLM
:::tip Run this notebook
This recipe is available as an interactive Jupyter notebook.
[**Open in GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/blob/main/notebooks/04-litellm-memory-demo.ipynb)
:::
This notebook demonstrates how to add persistent memory to any LLM app using the `hindsight-litellm` package. Memory storage and injection happen automatically via LiteLLM callbacks - no manual memory management needed!
**Key features demonstrated:**
1. `configure()` + `enable()` - Set up automatic memory integration
2. Automatic storage - Conversations are stored after each LLM call
3. Automatic injection - Relevant memories are injected into prompts
The `hindsight-litellm` package hooks into LiteLLM's callback system to:
- Store each conversation after successful LLM responses
- Inject relevant memories into the system prompt before LLM calls
## Prerequisites
Make sure you have Hindsight running:
```bash
export OPENAI_API_KEY=your-key
docker run --rm -it --pull always -p 8888:8888 -p 9999:9999 \
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
-e HINDSIGHT_API_LLM_MODEL=o3-mini \
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
ghcr.io/vectorize-io/hindsight:latest
```
- API: http://localhost:8888
- UI: http://localhost:9999
## Installation
```python
!pip install hindsight-litellm litellm nest_asyncio python-dotenv -U -q
```
## Setup
```python
import os
import uuid
import time
import logging
import nest_asyncio
from dotenv import load_dotenv
# Apply nest_asyncio for Jupyter compatibility
nest_asyncio.apply()
# Load environment variables
load_dotenv()
# Configure logging
logging.basicConfig(level=logging.INFO)
logging.getLogger("LiteLLM").setLevel(logging.WARNING)
logging.getLogger("LiteLLM Router").setLevel(logging.WARNING)
logging.getLogger("LiteLLM Proxy").setLevel(logging.WARNING)
# Import hindsight_litellm
import hindsight_litellm
# Configuration
HINDSIGHT_API_URL = os.getenv("HINDSIGHT_API_URL", "http://localhost:8888")
# Check for API key
if not os.getenv("OPENAI_API_KEY"):
print("Warning: OPENAI_API_KEY not set")
```
## Configure and Enable Automatic Memory
This is all you need! After this, all LiteLLM calls will automatically:
- Have relevant memories injected into the prompt
- Store conversations to Hindsight after the response
```python
# Generate a unique bank_id for this demo session
bank_id = f"demo-{uuid.uuid4().hex[:8]}"
print(f"Using bank_id: {bank_id}")
# Configure and enable hindsight
hindsight_litellm.configure(
hindsight_api_url=HINDSIGHT_API_URL,
bank_id=bank_id,
store_conversations=True, # Automatically store conversations
inject_memories=True, # Automatically inject relevant memories
verbose=True, # Enable logging to debug memory operations
)
hindsight_litellm.enable()
print("Hindsight memory integration enabled!")
```
## Conversation 1: User Introduces Themselves
In this first conversation, the user shares some information about themselves. This will be automatically stored to Hindsight memory.
```python
user_message_1 = "Hi! I'm Alex and I work at Google as a software engineer. I love Python and machine learning."
print(f"User: {user_message_1}\n")
# Use hindsight_litellm.completion() directly
response_1 = hindsight_litellm.completion(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": user_message_1}
],
)
assistant_response_1 = response_1.choices[0].message.content
print(f"Assistant: {assistant_response_1}")
print("\n(Conversation automatically stored to Hindsight)")
```
## Wait for Memory Processing
Hindsight needs a few seconds to process and extract facts from the conversation.
```python
print("Waiting 12 seconds for memory processing...")
time.sleep(12)
print("Done!")
```
## Conversation 2: Test Memory-Augmented Response
Now we start a fresh conversation and ask what the assistant remembers. The memories from the previous conversation will be automatically injected into the prompt!
```python
user_message_2 = "What do you know about me? What programming language should I use for my next project?"
print(f"User: {user_message_2}\n")
# Memories are automatically injected before this call!
response_2 = hindsight_litellm.completion(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": user_message_2}
],
)
print(f"Assistant: {response_2.choices[0].message.content}")
```
## Summary
The assistant should have remembered that Alex:
- Works at Google as a software engineer
- Loves Python and machine learning
And it should have recommended Python based on that knowledge!
```python
print(f"Memories stored in bank: {bank_id}")
print(f"View in UI: http://localhost:9999/banks/{bank_id}")
```
## Cleanup
```python
hindsight_litellm.cleanup()
# Optional: delete the bank
import requests
response = requests.delete(f"{HINDSIGHT_API_URL}/v1/default/banks/{bank_id}")
print(f"Deleted bank: {response.json()}")
```
@@ -1,9 +1,16 @@
---
sidebar_position: 1
sidebar_position: 2
---
# Per-User Memory
:::tip Run this notebook
This recipe is available as an interactive Jupyter notebook.
[**Open in GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/blob/main/notebooks/02-per-user-memory.ipynb)
:::
The simplest pattern: give your agent persistent memory for each user. The agent remembers past conversations, user preferences, and context across sessions.
## The Problem
@@ -31,28 +38,54 @@ Session 2: "What's my preferred language?" → Agent doesn't know
Each user gets their own memory bank. Complete isolation, simple mental model.
## Implementation
### 1. Create a Bank When User Signs Up
```python
from hindsight import HindsightClient
!pip install hindsight-client nest_asyncio openai python-dotenv -U
```
client = HindsightClient()
## 1. Create a Bank When User Signs Up
```python
# Jupyter notebooks already run an asyncio event loop. The hindsight client
# uses loop.run_until_complete() internally, but Python doesn't allow nested
# event loops by default. nest_asyncio patches this to allow nesting.
import nest_asyncio
nest_asyncio.apply()
import os
from dotenv import load_dotenv
from openai import OpenAI as OpenAIClient
# Load environment variables from .env file
# Copy .env.example to .env and fill in your values
load_dotenv()
# Configuration (override with env vars if set)
HINDSIGHT_API_URL = os.getenv("HINDSIGHT_API_URL", "http://localhost:8888")
HINDSIGHT_UI_URL = os.getenv("HINDSIGHT_UI_URL", "http://localhost:9999")
from hindsight_client import Hindsight
client = Hindsight(base_url=HINDSIGHT_API_URL)
llm = OpenAIClient() # Uses OPENAI_API_KEY from .env
def on_user_signup(user_id: str):
client.create_bank(
bank_id=f"user-{user_id}",
name=f"Memory for {user_id}"
)
print(f"View bank: {HINDSIGHT_UI_URL}/banks/user-{user_id}?view=documents")
```
### 2. Manage Conversation Sessions
## 2. Manage Conversation Sessions
Use `document_id` to group messages belonging to the same conversation. When you retain with the same `document_id`, Hindsight replaces the previous version (upsert behavior), keeping the memory up-to-date as the conversation evolves.
```python
import uuid
import json
class ConversationSession:
def __init__(self, user_id: str):
@@ -63,74 +96,101 @@ class ConversationSession:
def add_message(self, role: str, content: str):
self.messages.append({"role": role, "content": content})
async def save(self, client: HindsightClient):
def save(self, client: Hindsight):
"""Save the entire conversation. Replaces previous version if session_id exists."""
await client.retain(
# Convert messages to string format for retain
content = "\n".join([f"{m['role']}: {m['content']}" for m in self.messages])
client.retain(
bank_id=f"user-{self.user_id}",
content=self.messages,
content=content,
document_id=self.session_id # Same ID = upsert (replace old version)
)
```
### 3. Recall Context Before Responding
## 3. Recall Context Before Responding
```python
async def get_context(user_id: str, query: str):
result = await client.recall(
def get_context(user_id: str, query: str):
result = client.recall(
bank_id=f"user-{user_id}",
query=query
)
return result.results
```
### 4. Complete Agent Loop
## 4. Complete Agent Loop
```python
async def handle_message(session: ConversationSession, user_message: str):
def format_results(results):
"""Format recall results for the prompt."""
if not results:
return "No relevant memories found."
return "\n".join([f"- {r.text}" for r in results])
def format_messages(messages):
"""Format conversation messages for the prompt."""
return "\n".join([f"{m['role']}: {m['content']}" for m in messages])
def handle_message(session: ConversationSession, user_message: str):
# 1. Add user message to session
session.add_message("user", user_message)
# 2. Recall relevant context from past conversations
context = await client.recall(
context = client.recall(
bank_id=f"user-{session.user_id}",
query=user_message
)
# 3. Build prompt with memory
prompt = f"""You are a helpful assistant with memory of past conversations.
# 3. Build system prompt with memory
system_prompt = f"""You are a helpful assistant with memory of past conversations.
## What you remember about this user
{format_results(context.results)}
## Current conversation
{format_messages(session.messages)}
"""
Respond helpfully and reference relevant memories when appropriate."""
# 4. Generate response
response = await llm.complete(prompt)
# 4. Generate response using OpenAI
response = llm.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": system_prompt},
*[{"role": m["role"], "content": m["content"]} for m in session.messages]
]
)
assistant_response = response.choices[0].message.content
# 5. Add assistant response to session
session.add_message("assistant", response)
session.add_message("assistant", assistant_response)
# 6. Save the updated conversation (upserts based on session_id)
await session.save(client)
session.save(client)
return response
print(f"User: {user_message}")
print(f"Assistant: {assistant_response}\n")
return assistant_response
```
### 5. Starting a New Conversation
## 5. Starting a New Conversation
```python
# Create the user's bank
on_user_signup("alice")
# Each new conversation gets a new session with a unique ID
session = ConversationSession(user_id="alice")
# Multiple exchanges in the same conversation
await handle_message(session, "Hi! I'm working on a Python project")
await handle_message(session, "Can you help me with async/await?")
handle_message(session, "Hi! I'm working on a Python project")
handle_message(session, "Can you help me with async/await?")
# Start a new conversation later (new session_id)
new_session = ConversationSession(user_id="alice")
await handle_message(new_session, "Different topic today...")
# View the stored conversation in the UI.
# Each message updates the same document (via document_id), so you'll see
# the full conversation history in a single document rather than separate entries.
print(f"\nView documents: {HINDSIGHT_UI_URL}/banks/user-alice?view=documents")
```
## How Document ID Works
@@ -171,4 +231,17 @@ You don't need to manually extract or structure this - just retain the conversat
**Consider adding shared knowledge if:**
- You have product docs or FAQs to reference
- Multiple users need access to the same information
- See [Support Agent with Shared Knowledge](./support-agent-with-shared-knowledge)
- See the Support Agent with Shared Knowledge notebook
## Cleanup
Delete the banks created during this notebook:
```python
import requests
# Delete the user-alice bank
response = requests.delete(f"{HINDSIGHT_API_URL}/v1/default/banks/user-alice")
print(f"Deleted user-alice: {response.json()}")
```
@@ -0,0 +1,162 @@
---
sidebar_position: 1
---
# Hindsight Quickstart
:::tip Run this notebook
This recipe is available as an interactive Jupyter notebook.
[**Open in GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/blob/main/notebooks/01-quickstart.ipynb)
:::
This notebook covers the basics of using Hindsight:
- **Retain**: Store information in memory
- **Recall**: Retrieve memories matching a query
- **Reflect**: Generate insights from memories
## Prerequisites
Make sure you have Hindsight running. The easiest way is via Docker:
```bash
export OPENAI_API_KEY=your-key
docker run --rm -it --pull always -p 8888:8888 -p 9999:9999 \
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
-e HINDSIGHT_API_LLM_MODEL=o3-mini \
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
ghcr.io/vectorize-io/hindsight:latest
```
- API: http://localhost:8888
- UI: http://localhost:9999
## Installation
Install the Hindsight Python client:
```python
!pip install hindsight-client nest_asyncio python-dotenv -U
```
## Connect to Hindsight
```python
# Jupyter notebooks already run an asyncio event loop. The hindsight client
# uses loop.run_until_complete() internally, but Python doesn't allow nested
# event loops by default. nest_asyncio patches this to allow nesting.
import nest_asyncio
nest_asyncio.apply()
import os
from dotenv import load_dotenv
# Load environment variables from .env file
# Copy .env.example to .env and fill in your values
load_dotenv()
# Configuration (override with env vars if set)
HINDSIGHT_API_URL = os.getenv("HINDSIGHT_API_URL", "http://localhost:8888")
HINDSIGHT_UI_URL = os.getenv("HINDSIGHT_UI_URL", "http://localhost:9999")
from hindsight_client import Hindsight
client = Hindsight(base_url=HINDSIGHT_API_URL)
```
## Retain: Store Information
The `retain` operation is used to push new memories into Hindsight. It tells Hindsight to _retain_ the information you pass in.
Behind the scenes, the retain operation uses an LLM to extract key facts, temporal data, entities, and relationships.
```python
# Simple retain
client.retain(
bank_id="my-bank",
content="Alice works at Google as a software engineer"
)
# View the stored document in the UI:
print(f"View documents: {HINDSIGHT_UI_URL}/banks/my-bank?view=documents")
```
```python
# Retain with context and timestamp
client.retain(
bank_id="my-bank",
content="Alice got promoted to senior engineer",
context="career update",
timestamp="2025-06-15T10:00:00Z"
)
```
## Recall: Retrieve Memories
The `recall` operation retrieves memories matching a query. It performs 4 retrieval strategies in parallel:
- **Semantic**: Vector similarity
- **Keyword**: BM25 exact matching
- **Graph**: Entity/temporal/causal links
- **Temporal**: Time range filtering
```python
# Simple recall
results = client.recall(bank_id="my-bank", query="What does Alice do?")
print("Memories:")
for r in results.results:
print(f" - {r.text}")
```
```python
# Temporal recall
results = client.recall(bank_id="my-bank", query="What happened in June?")
print("Memories:")
for r in results.results:
print(f" - {r.text}")
```
## Reflect: Generate Insights
The `reflect` operation performs a more thorough analysis of existing memories. This allows the agent to form new connections between memories which are then persisted as opinions and/or observations.
Example use cases:
- An AI Project Manager reflecting on what risks need to be mitigated
- A Sales Agent reflecting on why certain outreach messages have gotten responses
- A Support Agent reflecting on opportunities where customers have unanswered questions
```python
response = client.reflect(bank_id="my-bank", query="What should I know about Alice?")
print(response)
```
## Memory Types
Hindsight organizes memory into four networks to mimic human memory:
- **World**: Facts about the world ("The stove gets hot")
- **Experiences**: Agent's own experiences ("I touched the stove and it really hurt")
- **Opinion**: Beliefs with confidence scores ("I shouldn't touch the stove again" - .99 confidence)
- **Observation**: Complex mental models derived by reflecting on facts and experiences
## Cleanup
Delete the bank created during this notebook:
```python
import requests
response = requests.delete(f"{HINDSIGHT_API_URL}/v1/default/banks/my-bank")
print(f"Deleted my-bank: {response.json()}")
```
@@ -4,6 +4,13 @@ sidebar_position: 3
# Support Agent with Shared Knowledge
:::tip Run this notebook
This recipe is available as an interactive Jupyter notebook.
[**Open in GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/blob/main/notebooks/03-support-agent-shared-knowledge.ipynb)
:::
This pattern shows how to build a support agent that combines **per-user memory** with **shared product knowledge** (RAG), giving users personalized support while leveraging a single source of truth for documentation.
## The Problem
@@ -17,8 +24,6 @@ A naive approach would index product docs into each user's memory bank, but this
## The Solution: Multi-Bank Architecture
Create separate memory banks for different concerns:
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ User A Bank │ │ User B Bank │ │ Shared Docs │
@@ -40,16 +45,39 @@ Create separate memory banks for different concerns:
- User memory is 100% isolated
- Simple mental model, no complex filtering
## Implementation
### 1. Set Up Memory Banks
```python
!pip install hindsight-client nest_asyncio openai python-dotenv -U
```
## 1. Set Up Memory Banks
Create three types of banks:
```python
from hindsight import HindsightClient
client = HindsightClient()
```python
# Jupyter notebooks already run an asyncio event loop. The hindsight client
# uses loop.run_until_complete() internally, but Python doesn't allow nested
# event loops by default. nest_asyncio patches this to allow nesting.
import nest_asyncio
nest_asyncio.apply()
import os
from dotenv import load_dotenv
from openai import OpenAI as OpenAIClient
# Load environment variables from .env file
# Copy .env.example to .env and fill in your values
load_dotenv()
# Configuration (override with env vars if set)
HINDSIGHT_API_URL = os.getenv("HINDSIGHT_API_URL", "http://localhost:8888")
HINDSIGHT_UI_URL = os.getenv("HINDSIGHT_UI_URL", "http://localhost:9999")
from hindsight_client import Hindsight
client = Hindsight(base_url=HINDSIGHT_API_URL)
llm = OpenAIClient() # Uses OPENAI_API_KEY from .env
# Shared knowledge bank (created once)
shared_bank = client.create_bank(
@@ -65,55 +93,57 @@ def create_user_bank(user_id: str):
)
```
### 2. Index Product Documentation
## 2. Index Product Documentation
Index your product docs into the shared bank (do this once, or on doc updates):
```python
# Index product documentation
# Index product documentation - retain each doc separately
client.retain(
bank_id="product-docs",
content=[
{
"role": "document",
"content": "# Pricing Tiers\n\nBasic: $10/mo...",
"metadata": {"source": "pricing.md"}
},
{
"role": "document",
"content": "# Getting Started\n\nTo set up...",
"metadata": {"source": "quickstart.md"}
}
]
content="# Pricing Tiers\n\nBasic: $10/mo, Pro: $25/mo, Enterprise: Contact us"
)
client.retain(
bank_id="product-docs",
content="# Getting Started\n\nTo set up your account, visit the dashboard and click 'New Project'"
)
# View the stored documents in the UI:
print(f"View documents: {HINDSIGHT_UI_URL}/banks/product-docs?view=documents")
```
### 3. Store User Conversations
## 3. Store User Conversations
After each support interaction, retain it in the user's bank:
```python
def save_conversation(user_id: str, messages: list):
# Convert messages to string format
content = "\n".join([f"{m['role']}: {m['content']}" for m in messages])
client.retain(
bank_id=f"user-{user_id}",
content=messages # [{"role": "user", "content": "..."}, ...]
content=content
)
```
### 4. Query Multiple Banks at Support Time
## 4. Query Multiple Banks at Support Time
When handling a user query, retrieve context from both banks:
```python
async def get_support_context(user_id: str, query: str):
def get_support_context(user_id: str, query: str):
# Get user's personal context
user_context = await client.recall(
user_context = client.recall(
bank_id=f"user-{user_id}",
query=query
)
# Get relevant product documentation
docs_context = await client.recall(
docs_context = client.recall(
bank_id="product-docs",
query=query
)
@@ -124,11 +154,18 @@ async def get_support_context(user_id: str, query: str):
}
```
### 5. Build the Agent Prompt
## 5. Build the Agent Prompt
Combine both contexts in your agent's prompt:
```python
def format_results(results):
"""Format recall results for the prompt."""
if not results:
return "No relevant information found."
return "\n".join([f"- {r.text}" for r in results])
def build_prompt(query: str, context: dict) -> str:
return f"""You are a helpful support agent.
@@ -167,6 +204,7 @@ When the agent discovers a solution that's not in the docs, you can optionally p
all three banks
```
```python
# Optional: Create a curated learnings bank
learnings_bank = client.create_bank(
@@ -178,71 +216,77 @@ learnings_bank = client.create_bank(
def promote_learning(insight: str):
client.retain(
bank_id="support-learnings",
content=[{
"role": "system",
"content": insight,
"metadata": {"type": "verified_solution"}
}]
content=insight
)
```
Then query three banks: user + docs + learnings.
## Complete Example
```python
from hindsight import HindsightClient
def format_results(results):
if not results:
return "No relevant information found."
return "\n".join([f"- {r.text}" for r in results])
client = HindsightClient()
async def handle_support_request(user_id: str, query: str):
def handle_support_request(user_id: str, query: str):
# 1. Recall from user's memory
user_recall = await client.recall(
user_recall = client.recall(
bank_id=f"user-{user_id}",
query=query
)
# 2. Recall from shared docs
docs_recall = await client.recall(
docs_recall = client.recall(
bank_id="product-docs",
query=query
)
# 3. Recall from learnings (optional)
learnings_recall = await client.recall(
learnings_recall = client.recall(
bank_id="support-learnings",
query=query
)
# 4. Build context for LLM
context = f"""
User History:
# 4. Build system prompt with context
system_prompt = f"""You are a helpful support agent. Use the context below to answer the user's question.
## User's History
{format_results(user_recall.results)}
Product Docs:
## Product Documentation
{format_results(docs_recall.results)}
Known Solutions:
## Known Solutions
{format_results(learnings_recall.results)}
"""
# 5. Generate response with your LLM
response = await llm.complete(
system="You are a support agent...",
context=context,
query=query
)
Provide helpful, accurate responses based on the documentation. Reference the user's history when relevant."""
# 6. Save the conversation to user's memory
await client.retain(
bank_id=f"user-{user_id}",
content=[
{"role": "user", "content": query},
{"role": "assistant", "content": response}
# 5. Generate response using OpenAI
response = llm.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": query}
]
)
assistant_response = response.choices[0].message.content
return response
# 6. Save the conversation to user's memory
conversation = f"user: {query}\nassistant: {assistant_response}"
client.retain(
bank_id=f"user-{user_id}",
content=conversation
)
return assistant_response
# Test the function
create_user_bank("bob")
print("User: How do I get started?")
result = handle_support_request("bob", "How do I get started?")
print(f"Assistant: {result}")
print(f"\nView user memory: {HINDSIGHT_UI_URL}/banks/user-bob?view=documents")
```
## When to Use This Pattern
@@ -256,3 +300,16 @@ Known Solutions:
- You need cross-user learning (users benefiting from other users' solutions)
- Entity relationships must span across users and docs
## Cleanup
Delete the banks created during this notebook:
```python
import requests
# Delete all banks created in this notebook
for bank_id in ["product-docs", "support-learnings", "user-bob"]:
response = requests.delete(f"{HINDSIGHT_API_URL}/v1/default/banks/{bank_id}")
print(f"Deleted {bank_id}: {response.json()}")
```
@@ -0,0 +1,372 @@
---
sidebar_position: 5
---
# Hindsight Tool Learning Demo
:::tip Run this notebook
This recipe is available as an interactive Jupyter notebook.
[**Open in GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/blob/main/notebooks/05-tool-learning-demo.ipynb)
:::
This notebook demonstrates how Hindsight helps an LLM learn which tool to use when tool names are ambiguous. Without memory, the LLM might randomly select between similarly-named tools. With Hindsight, it learns from past interactions and consistently makes the correct choice.
## The Scenario
We have a task routing system with two tools:
- `route_to_channel_alpha` - Routes to processing channel Alpha
- `route_to_channel_omega` - Routes to processing channel Omega
The tool names and descriptions are **intentionally vague**. In reality:
- Channel Alpha handles **FINANCIAL/PAYMENT** tasks (refunds, billing, etc.)
- Channel Omega handles **TECHNICAL/SUPPORT** tasks (bugs, features, etc.)
**Without Hindsight:** The LLM guesses randomly based on vague descriptions
**With Hindsight:** The LLM learns from feedback which channel handles what
## Prerequisites
Make sure you have Hindsight running:
```bash
export OPENAI_API_KEY=your-key
docker run --rm -it --pull always -p 8888:8888 -p 9999:9999 \
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
-e HINDSIGHT_API_LLM_MODEL=o3-mini \
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
ghcr.io/vectorize-io/hindsight:latest
```
## Installation
```python
!pip install hindsight-litellm hindsight-client litellm nest_asyncio python-dotenv -U -q
```
## Setup
```python
import os
import json
import uuid
import time
import logging
import nest_asyncio
from typing import Optional
from dotenv import load_dotenv
nest_asyncio.apply()
load_dotenv()
logging.basicConfig(level=logging.INFO)
logging.getLogger("LiteLLM").setLevel(logging.WARNING)
logging.getLogger("LiteLLM Router").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
import litellm
import hindsight_litellm
from hindsight_client import Hindsight
HINDSIGHT_API_URL = os.getenv("HINDSIGHT_API_URL", "http://localhost:8888")
if not os.getenv("OPENAI_API_KEY"):
print("Warning: OPENAI_API_KEY not set")
```
## Define Tools
These tool definitions are **intentionally ambiguous** - the descriptions don't reveal which channel handles what type of request.
```python
TOOLS = [
{
"type": "function",
"function": {
"name": "route_to_channel_alpha",
"description": "Routes the customer request to processing channel Alpha. Use this channel for appropriate request types.",
"parameters": {
"type": "object",
"properties": {
"request_summary": {
"type": "string",
"description": "A brief summary of the customer's request"
},
"priority": {
"type": "string",
"enum": ["low", "medium", "high"],
"description": "Priority level of the request"
}
},
"required": ["request_summary"]
}
}
},
{
"type": "function",
"function": {
"name": "route_to_channel_omega",
"description": "Routes the customer request to processing channel Omega. Use this channel for appropriate request types.",
"parameters": {
"type": "object",
"properties": {
"request_summary": {
"type": "string",
"description": "A brief summary of the customer's request"
},
"priority": {
"type": "string",
"enum": ["low", "medium", "high"],
"description": "Priority level of the request"
}
},
"required": ["request_summary"]
}
}
}
]
```
## Test Scenarios
A mix of financial and technical requests to test routing accuracy.
```python
TEST_SCENARIOS = [
{
"type": "financial",
"request": "I was charged twice for my subscription last month. I need a refund for the duplicate charge.",
"correct_tool": "route_to_channel_alpha"
},
{
"type": "technical",
"request": "The app keeps crashing when I try to upload a file larger than 10MB. This bug is blocking my work.",
"correct_tool": "route_to_channel_omega"
},
{
"type": "financial",
"request": "My invoice shows an incorrect amount. The billing department needs to fix this.",
"correct_tool": "route_to_channel_alpha"
},
{
"type": "technical",
"request": "I'd like to request a new feature: the ability to export reports as PDF.",
"correct_tool": "route_to_channel_omega"
},
{
"type": "financial",
"request": "I need to update my payment method and understand why my last payment failed.",
"correct_tool": "route_to_channel_alpha"
},
]
```
## Helper Functions
```python
SYSTEM_PROMPT = """You are a customer service routing agent. Your job is to route customer requests to the appropriate processing channel.
You have access to two routing channels:
- route_to_channel_alpha: Routes to channel Alpha
- route_to_channel_omega: Routes to channel Omega
Analyze the customer's request and route it to the most appropriate channel. You must call one of the routing functions to process the request.
Important: Base your routing decision on what you know about each channel's purpose. If you have learned from previous interactions which channel handles specific types of requests, use that knowledge."""
def make_routing_request(user_request: str, use_hindsight: bool, bank_id: Optional[str] = None):
"""Make a routing request and return the tool called."""
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": f"Customer Request: {user_request}"}
]
if use_hindsight and bank_id:
response = hindsight_litellm.completion(
model="gpt-4o-mini",
messages=messages,
tools=TOOLS,
tool_choice="required",
temperature=0.0,
)
else:
response = litellm.completion(
model="gpt-4o-mini",
messages=messages,
tools=TOOLS,
tool_choice="required",
temperature=0.7,
)
if response.choices[0].message.tool_calls:
tool_call = response.choices[0].message.tool_calls[0]
return tool_call.function.name
return None
def store_feedback(bank_id: str, request: str, correct_tool: str, request_type: str):
"""Store feedback about which tool was correct for a request type."""
client = Hindsight(base_url=HINDSIGHT_API_URL, timeout=60.0)
feedback_content = f"""ROUTING FEEDBACK:
Request type: {request_type}
Customer request: "{request}"
Correct routing: {correct_tool}
LEARNED RULE: {request_type.upper()} requests (like refunds, billing, payments, charges, invoices) should ALWAYS be routed to {correct_tool}.
This is important institutional knowledge for routing decisions."""
client.retain(
bank_id=bank_id,
content=feedback_content,
context=f"routing:feedback:{request_type}",
metadata={"request_type": request_type, "correct_tool": correct_tool}
)
```
## Phase 1: Without Hindsight (No Memory)
The LLM has no prior knowledge about which channel handles what. With ambiguous tool descriptions, it may route incorrectly.
```python
print("=" * 60)
print("PHASE 1: WITHOUT HINDSIGHT (No Memory)")
print("=" * 60)
phase1_results = []
for i, scenario in enumerate(TEST_SCENARIOS[:3], 1):
print(f"\n--- Test {i}: {scenario['type'].upper()} Request ---")
print(f"Request: \"{scenario['request'][:60]}...\"")
tool_name = make_routing_request(scenario['request'], use_hindsight=False)
is_correct = tool_name == scenario['correct_tool']
phase1_results.append(is_correct)
print(f"LLM chose: {tool_name}")
print(f"Correct tool: {scenario['correct_tool']}")
print(f"Result: {'✓ CORRECT' if is_correct else '✗ INCORRECT'}")
phase1_accuracy = sum(phase1_results) / len(phase1_results) * 100
print(f"\n>>> Phase 1 Accuracy: {phase1_accuracy:.0f}% ({sum(phase1_results)}/{len(phase1_results)})")
```
## Phase 2: Teaching Phase
Now we provide feedback about correct routing to build memory. This simulates a human supervisor correcting the AI's routing decisions.
```python
bank_id = f"tool-learning-{uuid.uuid4().hex[:8]}"
print(f"Using bank_id: {bank_id}")
# Configure and enable Hindsight
hindsight_litellm.configure(
hindsight_api_url=HINDSIGHT_API_URL,
bank_id=bank_id,
store_conversations=True,
inject_memories=True,
max_memories=10,
recall_budget="high",
verbose=False,
)
hindsight_litellm.enable()
print("\nStoring routing feedback...")
feedback_examples = [
("I need a refund for an incorrect charge on my account.", "route_to_channel_alpha", "financial"),
("There's a bug in the system causing data loss.", "route_to_channel_omega", "technical"),
("My billing statement has errors that need correction.", "route_to_channel_alpha", "financial"),
("I want to request a new feature for the dashboard.", "route_to_channel_omega", "technical"),
]
for request, correct_tool, req_type in feedback_examples:
print(f" Storing: {req_type.upper()}{correct_tool}")
store_feedback(bank_id, request, correct_tool, req_type)
print("\nWaiting 15 seconds for Hindsight to process memories...")
time.sleep(15)
print("Done!")
```
## Phase 3: With Hindsight (Memory-Augmented)
The LLM now has access to learned routing knowledge via Hindsight. It should route requests correctly based on past feedback.
```python
print("=" * 60)
print("PHASE 3: WITH HINDSIGHT (Memory-Augmented)")
print("=" * 60)
phase3_results = []
for i, scenario in enumerate(TEST_SCENARIOS, 1):
print(f"\n--- Test {i}: {scenario['type'].upper()} Request ---")
print(f"Request: \"{scenario['request'][:60]}...\"")
tool_name = make_routing_request(
scenario['request'],
use_hindsight=True,
bank_id=bank_id
)
is_correct = tool_name == scenario['correct_tool']
phase3_results.append(is_correct)
print(f"LLM chose: {tool_name}")
print(f"Correct tool: {scenario['correct_tool']}")
print(f"Result: {'✓ CORRECT' if is_correct else '✗ INCORRECT'}")
phase3_accuracy = sum(phase3_results) / len(phase3_results) * 100
print(f"\n>>> Phase 3 Accuracy: {phase3_accuracy:.0f}% ({sum(phase3_results)}/{len(phase3_results)})")
```
## Summary
```python
print("=" * 60)
print("SUMMARY")
print("=" * 60)
print(f"\nPhase 1 (No Memory): {phase1_accuracy:.0f}% accuracy")
print(f"Phase 3 (With Hindsight): {phase3_accuracy:.0f}% accuracy")
improvement = phase3_accuracy - phase1_accuracy
if improvement > 0:
print(f"\n🎉 Improvement: +{improvement:.0f}% accuracy with Hindsight!")
elif improvement == 0:
print(f"\nNote: Results may vary. Run again to see learning effect.")
else:
print(f"\nNote: Phase 1 got lucky! Run again to see typical behavior.")
print(f"\nMemories stored in bank: {bank_id}")
print(f"View in UI: http://localhost:9999/banks/{bank_id}")
print("\n" + "=" * 60)
print("KEY INSIGHT")
print("=" * 60)
print("Hindsight allows the LLM to learn from experience which tool")
print("to use, even when tool names/descriptions are ambiguous.")
```
## Cleanup
```python
hindsight_litellm.cleanup()
# Optional: delete the bank
import requests
response = requests.delete(f"{HINDSIGHT_API_URL}/v1/default/banks/{bank_id}")
print(f"Deleted bank: {response.json()}")
```
@@ -22,7 +22,7 @@ export HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY
hindsight-api
```
API available at http://localhost:8888
API available at [http://localhost:8888](http://localhost:8888/docs)
</TabItem>
<TabItem value="docker" label="Docker (Full Experience)">
+86 -122
View File
@@ -1,127 +1,81 @@
# Configuration
Complete reference for configuring Hindsight server through environment variables and configuration files.
Complete reference for configuring Hindsight services through environment variables.
## Environment Variables
Hindsight has two services, each with its own configuration prefix:
Hindsight is configured entirely through environment variables, making it easy to deploy across different environments and container orchestration platforms.
| Service | Prefix | Description |
|---------|--------|-------------|
| **API Service** | `HINDSIGHT_API_*` | Core memory engine |
| **Control Plane** | `HINDSIGHT_CP_*` | Web UI |
All environment variable names and defaults are defined in `hindsight_api.config`. You can use `MemoryEngine.from_env()` to create a MemoryEngine instance configured from environment variables:
---
```python
from hindsight_api import MemoryEngine
## API Service
# Create from environment variables
memory = MemoryEngine.from_env()
await memory.initialize()
```
The API service handles all memory operations (retain, recall, reflect).
### LLM Provider Configuration
### Database
Configure the LLM provider used for fact extraction, entity resolution, and reasoning operations.
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_DATABASE_URL` | PostgreSQL connection string | `pg0` (embedded) |
#### Common LLM Settings
If not provided, the server uses embedded `pg0` — convenient for development but not recommended for production.
| Variable | Description | Default | Required |
|----------|-------------|---------|----------|
| `HINDSIGHT_API_LLM_PROVIDER` | LLM provider: `groq`, `openai`, `gemini`, `ollama` | `groq` | Yes |
| `HINDSIGHT_API_LLM_API_KEY` | API key for LLM provider | - | Yes (except ollama) |
| `HINDSIGHT_API_LLM_MODEL` | Model name | Provider-specific | No |
| `HINDSIGHT_API_LLM_BASE_URL` | Custom LLM endpoint | Provider default | No |
### LLM Provider
#### Provider-Specific Examples
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_LLM_PROVIDER` | Provider: `groq`, `openai`, `gemini`, `ollama` | `openai` |
| `HINDSIGHT_API_LLM_API_KEY` | API key for LLM provider | - |
| `HINDSIGHT_API_LLM_MODEL` | Model name | `gpt-5-mini` |
| `HINDSIGHT_API_LLM_BASE_URL` | Custom LLM endpoint | Provider default |
**Groq (Recommended for Fast Inference)**
**Provider Examples**
```bash
# Groq (recommended for fast inference)
export HINDSIGHT_API_LLM_PROVIDER=groq
export HINDSIGHT_API_LLM_API_KEY=gsk_xxxxxxxxxxxx
export HINDSIGHT_API_LLM_MODEL=openai/gpt-oss-20b
```
**OpenAI**
```bash
# OpenAI
export HINDSIGHT_API_LLM_PROVIDER=openai
export HINDSIGHT_API_LLM_API_KEY=sk-xxxxxxxxxxxx
export HINDSIGHT_API_LLM_MODEL=gpt-4o
```
**Gemini**
```bash
# Gemini
export HINDSIGHT_API_LLM_PROVIDER=gemini
export HINDSIGHT_API_LLM_API_KEY=xxxxxxxxxxxx
export HINDSIGHT_API_LLM_MODEL=gemini-2.0-flash
```
**Ollama (Local, No API Key)**
```bash
# Ollama (local, no API key)
export HINDSIGHT_API_LLM_PROVIDER=ollama
export HINDSIGHT_API_LLM_BASE_URL=http://localhost:11434/v1
export HINDSIGHT_API_LLM_MODEL=llama3.1
```
**OpenAI-Compatible Endpoints**
```bash
# OpenAI-compatible endpoint
export HINDSIGHT_API_LLM_PROVIDER=openai
export HINDSIGHT_API_LLM_BASE_URL=https://your-endpoint.com/v1
export HINDSIGHT_API_LLM_API_KEY=your-api-key
export HINDSIGHT_API_LLM_MODEL=your-model-name
```
### Database Configuration
### Embeddings
Configure the PostgreSQL database connection and behavior.
| Variable | Description | Default | Required |
|----------|-------------|---------|----------|
| `HINDSIGHT_API_DATABASE_URL` | PostgreSQL connection string | - | Yes* |
**\*Note**: If `DATABASE_URL` is not provided, the server will use embedded `pg0` (embedded PostGRE).
### MCP Server Configuration
Configure the Model Context Protocol (MCP) server for AI assistant integrations.
| Variable | Description | Default | Required |
|----------|-------------|---------|----------|
| `HINDSIGHT_API_MCP_ENABLED` | Enable MCP server | `true` | No |
```bash
# Enable MCP server (default)
export HINDSIGHT_API_MCP_ENABLED=true
# Disable MCP server
export HINDSIGHT_API_MCP_ENABLED=false
```
### Embeddings Configuration
Configure the embeddings provider for semantic search. By default, uses local SentenceTransformers models.
| Variable | Description | Default | Required |
|----------|-------------|---------|----------|
| `HINDSIGHT_API_EMBEDDINGS_PROVIDER` | Provider: `local` or `tei` | `local` | No |
| `HINDSIGHT_API_EMBEDDINGS_LOCAL_MODEL` | Model name for local provider | `BAAI/bge-small-en-v1.5` | No |
| `HINDSIGHT_API_EMBEDDINGS_TEI_URL` | TEI server URL | - | Yes (if provider is `tei`) |
**Local Provider (Default)**
Uses SentenceTransformers to run embedding models locally. Good for development and smaller deployments.
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_EMBEDDINGS_PROVIDER` | Provider: `local` or `tei` | `local` |
| `HINDSIGHT_API_EMBEDDINGS_LOCAL_MODEL` | Model for local provider | `BAAI/bge-small-en-v1.5` |
| `HINDSIGHT_API_EMBEDDINGS_TEI_URL` | TEI server URL | - |
```bash
# Local (default) - uses SentenceTransformers
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=local
export HINDSIGHT_API_EMBEDDINGS_LOCAL_MODEL=BAAI/bge-small-en-v1.5
```
**TEI Provider (HuggingFace Text Embeddings Inference)**
Uses a remote [TEI server](https://github.com/huggingface/text-embeddings-inference) for high-performance inference. Recommended for production deployments.
```bash
# TEI - HuggingFace Text Embeddings Inference (recommended for production)
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=tei
export HINDSIGHT_API_EMBEDDINGS_TEI_URL=http://localhost:8080
```
@@ -130,63 +84,73 @@ export HINDSIGHT_API_EMBEDDINGS_TEI_URL=http://localhost:8080
All embedding models must produce 384-dimensional vectors to match the database schema.
:::
### Reranker Configuration
### Reranker
Configure the cross-encoder reranker for improving search result relevance. By default, uses local SentenceTransformers models.
| Variable | Description | Default | Required |
|----------|-------------|---------|----------|
| `HINDSIGHT_API_RERANKER_PROVIDER` | Provider: `local` or `tei` | `local` | No |
| `HINDSIGHT_API_RERANKER_LOCAL_MODEL` | Model name for local provider | `cross-encoder/ms-marco-MiniLM-L-6-v2` | No |
| `HINDSIGHT_API_RERANKER_TEI_URL` | TEI server URL | - | Yes (if provider is `tei`) |
**Local Provider (Default)**
Uses SentenceTransformers CrossEncoder to run reranking locally.
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_RERANKER_PROVIDER` | Provider: `local` or `tei` | `local` |
| `HINDSIGHT_API_RERANKER_LOCAL_MODEL` | Model for local provider | `cross-encoder/ms-marco-MiniLM-L-6-v2` |
| `HINDSIGHT_API_RERANKER_TEI_URL` | TEI server URL | - |
```bash
# Local (default) - uses SentenceTransformers CrossEncoder
export HINDSIGHT_API_RERANKER_PROVIDER=local
export HINDSIGHT_API_RERANKER_LOCAL_MODEL=cross-encoder/ms-marco-MiniLM-L-6-v2
```
**TEI Provider (HuggingFace Text Embeddings Inference)**
Uses a remote [TEI server](https://github.com/huggingface/text-embeddings-inference) with a reranker model.
```bash
# TEI - for high-performance inference
export HINDSIGHT_API_RERANKER_PROVIDER=tei
export HINDSIGHT_API_RERANKER_TEI_URL=http://localhost:8081
```
:::tip
When using TEI, you can run separate servers for embeddings and reranking, or use a single server if it supports both operations with your chosen model.
:::
### Server
## Configuration Files
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_HOST` | Bind address | `0.0.0.0` |
| `HINDSIGHT_API_PORT` | Server port | `8888` |
| `HINDSIGHT_API_LOG_LEVEL` | Log level: `debug`, `info`, `warning`, `error` | `info` |
| `HINDSIGHT_API_MCP_ENABLED` | Enable MCP server | `true` |
### .env File
### Programmatic Configuration
The Hindsight API will look for a `.env` file:
You can also configure the API programmatically using `MemoryEngine.from_env()`:
```bash
# .env
```python
from hindsight_api import MemoryEngine
# Database
HINDSIGHT_API_DATABASE_URL=postgresql://hindsight:hindsight_dev@localhost:5432/hindsight
# LLM
HINDSIGHT_API_LLM_PROVIDER=groq
HINDSIGHT_API_LLM_API_KEY=gsk_xxxxxxxxxxxx
# Embeddings (optional, defaults to local)
# HINDSIGHT_API_EMBEDDINGS_PROVIDER=local
# HINDSIGHT_API_EMBEDDINGS_LOCAL_MODEL=BAAI/bge-small-en-v1.5
# Reranker (optional, defaults to local)
# HINDSIGHT_API_RERANKER_PROVIDER=local
# HINDSIGHT_API_RERANKER_LOCAL_MODEL=cross-encoder/ms-marco-MiniLM-L-6-v2
memory = MemoryEngine.from_env()
await memory.initialize()
```
---
For configuration issues not covered here, please [open an issue](https://github.com/your-repo/hindsight/issues) on GitHub.
## Control Plane
The Control Plane is the web UI for managing memory banks.
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_CP_DATAPLANE_API_URL` | URL of the API service | `http://localhost:8888` |
```bash
# Point Control Plane to a remote API service
export HINDSIGHT_CP_DATAPLANE_API_URL=http://api.example.com:8888
```
---
## Example .env File
```bash
# API Service
HINDSIGHT_API_DATABASE_URL=postgresql://hindsight:hindsight_dev@localhost:5432/hindsight
HINDSIGHT_API_LLM_PROVIDER=groq
HINDSIGHT_API_LLM_API_KEY=gsk_xxxxxxxxxxxx
# Control Plane
HINDSIGHT_CP_DATAPLANE_API_URL=http://localhost:8888
```
---
For configuration issues not covered here, please [open an issue](https://github.com/vectorize-io/hindsight/issues) on GitHub.
@@ -126,7 +126,6 @@ hindsight-api
hindsight-api --port 9000 # Custom port (default: 8888)
hindsight-api --host 127.0.0.1 # Bind to localhost only
hindsight-api --workers 4 # Multiple worker processes
hindsight-api --mcp # Enable MCP server
hindsight-api --log-level debug # Verbose logging
```
+19 -29
View File
@@ -25,10 +25,10 @@ This means **Recall (search) operations are blazingly fast** because all the hea
### Performance Comparison
| Operation | Typical Latency | Primary Bottleneck | Optimization Strategy |
|-----------|----------------|-------------------|----------------------|
| **Recall** | 100-600ms | Vector search, graph traversal | ✅ Already optimized |
| **Reflect** | 800-3000ms | LLM generation + search | Reduce search budget, use faster LLM |
| Operation | Typical Latency | Primary Bottleneck | Optimization Strategy |
|-----------|----------------|-------------------|----------------------------------|
| **Recall** | 100-600ms | Re-ranker (on CPU) | Use GPU for re-ranking, or reduce budget |
| **Reflect** | 800-3000ms | LLM generation | Use faster LLM |
| **Retain** | 500ms-2000ms per batch | **LLM fact extraction** | Use high-throughput LLM provider |
Hindsight is designed to ensure your **application's read path (recall/reflect) is always fast**, even if it means spending more time upfront during writes. This is the right trade-off for memory systems where:
@@ -50,8 +50,8 @@ The fact extraction process is structured and well-defined, so smaller, faster m
To maximize retention throughput:
1. **Use high-throughput LLM providers**: Choose providers with high requests-per-minute (RPM) limits and low latency
- **Fast**: [Groq](https://groq.com) with `gpt-oss-20b` or other openai-oss models, self-hosted models on GPU clusters (vLLM, TGI)
- ⚠️ **Slower**: Standard cloud LLM providers with rate limits
- **Fast**: [Groq](https://groq.com) with `gpt-oss-20b` or other openai-oss models, self-hosted models on GPU clusters (vLLM, TGI)
- **Slow**: Standard cloud LLM providers with rate limits
2. **Batch your operations**: Group related content into batch requests. The only limit is the HTTP payload size — Hindsight automatically splits large batches into smaller, optimized chunks under the hood, so you don't have to worry about it.
@@ -61,15 +61,7 @@ To maximize retention throughput:
### Throughput
Typical ingestion performance:
| Mode | Items/second | Use Case |
|------|--------------|----------|
| Synchronous | ~50-100 | Real-time updates, small batches |
| Async (batched) | ~500-1000 | Bulk imports, background processing |
| Parallel async | ~2000-5000 | Large-scale data migration |
**Factors affecting throughput:**
Factors affecting throughput:
- Document size and complexity
- LLM provider rate limits (for fact extraction)
- Database write performance
@@ -83,13 +75,13 @@ Typical ingestion performance:
The `budget` parameter controls the search depth and quality. Choose based on query complexity — comprehensive questions that need thorough analysis benefit from higher budgets:
| Budget | Latency | Memory Activation | Use Case |
|--------|---------|-------------------|----------|
| `low` | 100-300ms | ~10-50 facts | Quick lookups, real-time chat |
| `mid` | 300-600ms | ~50-200 facts | Standard queries, balanced performance |
| `high` | 500-1500ms | ~200-500 facts | Comprehensive questions, thorough analysis |
| Budget | Use Case |
|--------|----------|
| `low` | Quick lookups, real-time chat |
| `mid` | Standard queries, balanced performance |
| `high` | Comprehensive questions, thorough analysis |
### Search Optimization
### Optimization
1. **Appropriate budgets**: Use lower budgets for simple queries, higher for comprehensive reasoning
2. **Limit result tokens**: Set `max_tokens` to control response size (default: 4096)
@@ -107,18 +99,16 @@ Hindsight uses PostgreSQL with pgvector for efficient vector search:
### Performance Characteristics
| Component | Latency | Description |
|-----------|---------|-------------|
| Memory search | 300-1000ms | Based on budget (low/mid/high) |
| LLM generation | 500-2000ms | Depends on provider and response length |
| **Total** | **800-3000ms** | Typical end-to-end latency |
| Component | Latency | Description |
|-----------|----------------|-------------|
| Memory search | 100-600ms | Based on budget (low/mid/high) |
| LLM generation | 500-2000ms | Depends on provider and response length |
| **Total** | **600-2600ms** | Typical end-to-end latency |
### Optimization Strategies
1. **Budget selection**: Use lower budgets when context is sufficient
2. **Context provision**: Provide relevant `context` to reduce search requirements
3. **Streaming responses**: Use streaming APIs (when available) for faster time-to-first-token
4. **Caching**: Cache frequent queries at the application level
2. **Context provision**: Provide relevant `context` to reduce recall requirements and steer towards more focused answers
## Best Practices
+1 -1
View File
@@ -62,9 +62,9 @@ Hindsight distinguishes between **world** facts (about others) and **experience*
| **world** | Facts about people, places, things | "Alice works at Google" |
| **experience** | Conversations and events | "I recommended Python to Alice" |
This separation is important for `reflect()` — the bank can reason about what it knows versus what happened in conversations.
**Note:** Opinions aren't created during `retain()` — only during `reflect()` when the bank forms beliefs.
This separation is important for `reflect()` — the bank can reason about what it knows versus what happened in conversations.
---
+53 -29
View File
@@ -108,6 +108,19 @@ After the four strategies run, results are **fused together**:
---
## Why Multiple Strategies?
Consider the query: **"What did Alice think about Python last spring?"**
- **Semantic** finds facts about Alice's opinions on programming
- **Keyword** ensures "Python" is actually mentioned
- **Graph** connects Alice → opinions → programming languages
- **Temporal** filters to "last spring" timeframe
The **fusion** of all four gives you exactly what you're looking for, even though no single strategy would suffice.
---
## Token Budget Management
Hindsight is built for AI agents, not humans. Traditional search systems return "top-k" results, but agents don't think in terms of result counts—they think in tokens. An agent's context window is measured in tokens, and that's exactly how Hindsight measures results.
@@ -119,19 +132,43 @@ Hindsight is built for AI agents, not humans. Traditional search systems return
**Parameters you control:**
- `max_tokens`: How much memory content to return (default: 4096 tokens)
- `budget`: Budget level for graph traversal (low, mid, high)
- `budget`: Search depth level (low, mid, high)
- `fact_type`: Filter by world, experience, opinion, or all
### Additional Context: Chunks and Entity Observations
### Expanding Context: Chunks and Entity Observations
For the most relevant memories, you can optionally retrieve additional context—each with its own token budget:
Memories are distilled facts—concise but sometimes missing nuance. When your agent needs deeper context, you can optionally retrieve the source material and related knowledge:
| Option | Parameters | Description |
| Option | Parameters | When to Use |
|--------|------------|-------------|
| **Chunks** | `include_chunks`, `max_chunk_tokens` | Raw text chunks that generated the memories |
| **Entity Observations** | `include_entities`, `max_entity_tokens` | Related observations about entities mentioned in results |
| **Chunks** | `include_chunks`, `max_chunk_tokens` | Need exact quotes, original phrasing, or surrounding context |
| **Entity Observations** | `include_entities`, `max_entity_tokens` | Need broader knowledge about people/things mentioned in results |
This gives your agent richer context while maintaining precise control over total token consumption.
**Chunks** return the raw text that generated each memory—useful when the distilled fact loses important nuance:
```
Memory: "Alice prefers Python over JavaScript"
Chunk: "Alice mentioned she prefers Python over JavaScript, mainly because
of its data science ecosystem, though she admits JS is better for
frontend work and she's been learning TypeScript lately."
```
**Entity Observations** pull in related facts about entities mentioned in your results. If a memory mentions "Alice", you automatically get her role, skills, and other relevant context—without needing a separate query:
```
Query: "What programming languages does Alice like?"
Memory: "Alice prefers Python over JavaScript"
Entity Observations (Alice):
- "Alice is a senior data scientist at Google"
- "Alice specializes in machine learning"
- "Alice has been learning TypeScript"
```
**When to include them:**
- **Chunks**: When generating responses that need verbatim quotes or when context matters (e.g., "What exactly did Alice say about the project?")
- **Entity Observations**: When building complete profiles or when the conversation might reference multiple aspects of an entity (e.g., "Tell me about Alice's work")
Each has its own token budget, giving you precise control over total context size.
---
@@ -139,17 +176,17 @@ This gives your agent richer context while maintaining precise control over tota
Different use cases require different trade-offs between **recall quality** and **response speed**. Two parameters control this:
### Budget: Graph Exploration Depth
### Budget: Search Depth
Controls how many nodes to explore when traversing the knowledge graph:
Controls how thoroughly Hindsight explores the memory bank—affecting graph traversal depth, candidate pool size, and cross-encoder re-ranking:
| Budget | Nodes Explored | Best For | Trade-off |
|--------|----------------|----------|-----------|
| **low** | 100 nodes | Quick lookups, simple queries | Fast, may miss distant connections |
| **mid** | 300 nodes | Most queries, balanced | Good coverage, reasonable speed |
| **high** | 600 nodes | Complex multi-hop queries | Thorough, slower |
| Budget | Best For | Trade-off |
|--------|----------|-----------|
| **low** | Quick lookups, simple queries | Fast, may miss indirect connections |
| **mid** | Most queries, balanced | Good coverage, reasonable speed |
| **high** | Complex queries requiring deep exploration | Thorough, slower |
**Example:** "What did Alice's manager's team work on?" benefits from high budget to traverse Alice → manager → team → projects.
**Example:** "What did Alice's manager's team work on?" benefits from high budget to traverse multiple hops (Alice → manager → team → projects) and evaluate more candidates.
### Max Tokens: Context Window Size
@@ -169,7 +206,7 @@ Budget and max_tokens control different aspects of recall:
| Parameter | What it controls | Latency impact | Example |
|-----------|------------------|----------------|---------|
| **Budget** | How deep to explore the graph | Search time | High budget finds Alice → manager → team → projects |
| **Budget** | How thoroughly to explore memories | Search time | High budget finds Alice → manager → team → projects |
| **Max Tokens** | How much context to return | LLM processing time | High tokens returns more memories to the agent |
**They're independent.** Common combinations:
@@ -192,19 +229,6 @@ Budget and max_tokens control different aspects of recall:
---
## Why Multiple Strategies?
Consider the query: **"What did Alice think about Python last spring?"**
- **Semantic** finds facts about Alice's opinions on programming
- **Keyword** ensures "Python" is actually mentioned
- **Graph** connects Alice → opinions → programming languages
- **Temporal** filters to "last spring" timeframe
The **fusion** of all four gives you exactly what you're looking for, even though no single strategy would suffice.
---
## Next Steps
- [**Retain**](./retain) — How memories are stored with rich context
+42
View File
@@ -0,0 +1,42 @@
# Services
Hindsight consists of two services that can run together or separately depending on your deployment needs.
## API Service
The core memory engine. Handles all memory operations:
- **Retain**: Ingests content, extracts facts, builds knowledge graph
- **Recall**: Semantic search across memories
- **Reflect**: Disposition-aware answer generation
```
hindsight-api # Default port: 8888
```
The API service is stateless and can be horizontally scaled behind a load balancer. All state is stored in PostgreSQL.
## Control Plane
Web UI for managing and exploring your memory banks:
- Browse agents and memory banks
- Explore entities and relationships
- View ingestion history and operations
- Test recall queries interactively
```
hindsight-control-plane # Default port: 9999
```
The Control Plane connects to the API service and provides a visual interface for development and debugging.
## Deployment Options
| Deployment | Services | Use Case |
|------------|----------|----------|
| **Docker (single container)** | Both bundled | Development, quick start |
| **Helm / Kubernetes** | Separate pods | Production, scaling |
| **Bare metal** | Run independently | Custom deployments |
In the Docker quickstart, both services run in a single container. For production Kubernetes deployments, they run as separate pods with independent scaling.
+3 -4
View File
@@ -52,10 +52,9 @@ pg0 is a single binary containing:
### Behavior
When no `DATABASE_URL` is configured, Hindsight:
1. Downloads the pg0 binary for the current platform (macOS ARM, Linux x86_64/ARM64, Windows)
2. Starts an embedded PostgreSQL instance on port 5555
3. Initializes the schema
4. Stores data in `~/.hindsight/pg0/`
1. Starts an embedded PostgreSQL instance on port 5555
2. Initializes the schema
3. Stores data in `~/.hindsight/pg0/`
### Environments
+13
View File
@@ -2,6 +2,10 @@ import {themes as prismThemes} from 'prism-react-renderer';
import type {Config} from '@docusaurus/types';
import type * as Preset from '@docusaurus/preset-classic';
// Announcement bar - supports HTML for links
// Set to empty string '' to hide the bar
const ANNOUNCEMENT_BAR = 'HINDSIGHT is State-of-the-Art on Memory for AI Agents | <a href="https://arxiv.org/abs/2512.12818" target="_blank">Read the paper →</a>';
const config: Config = {
title: 'Hindsight',
tagline: 'Entity-Aware Memory System for AI Agents',
@@ -115,6 +119,15 @@ const config: Config = {
themes: ['@docusaurus/theme-mermaid'],
themeConfig: {
...(ANNOUNCEMENT_BAR && {
announcementBar: {
id: 'announcement',
content: ANNOUNCEMENT_BAR,
backgroundColor: '#0074d9',
textColor: '#ffffff',
isCloseable: true,
},
}),
image: 'img/hindsight-social-card.jpg',
colorMode: {
defaultMode: 'dark',
+2 -3
View File
@@ -4,9 +4,8 @@
"private": true,
"scripts": {
"docusaurus": "docusaurus",
"generate-llms": "node scripts/generate-llms-full.js",
"start": "npm run generate-llms && docusaurus start",
"build": "npm run generate-llms && docusaurus build",
"start": "docusaurus start",
"build": "docusaurus build",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"clear": "docusaurus clear",
@@ -1,150 +0,0 @@
#!/usr/bin/env node
/**
* Generates llms-full.txt by concatenating all documentation markdown files.
* This file is used by LLMs to understand the full documentation.
*
* Usage: node scripts/generate-llms-full.js
*
* Output: static/llms-full.txt (served at /llms-full.txt)
*/
const fs = require('fs');
const path = require('path');
const DOCS_DIR = path.join(__dirname, '..', 'docs');
const OUTPUT_FILE = path.join(__dirname, '..', 'static', 'llms-full.txt');
// Order matters - more important docs first
const DOC_ORDER = [
'developer/index.md',
'developer/api/quickstart.md',
'developer/api/main-methods.md',
'developer/retain.md',
'developer/retrieval.md',
'developer/reflect.md',
'developer/api/retain.md',
'developer/api/recall.md',
'developer/api/reflect.md',
'developer/api/memory-banks.md',
'developer/api/entities.md',
'developer/api/documents.md',
'developer/api/operations.md',
'developer/installation.md',
'developer/configuration.md',
'developer/models.md',
'developer/rag-vs-hindsight.md',
'sdks/python.md',
'sdks/nodejs.md',
'sdks/cli.md',
'sdks/mcp.md',
'cookbook/index.md',
'cookbook/per-user-memory.md',
'cookbook/support-agent-with-shared-knowledge.md',
];
function getAllMarkdownFiles(dir, baseDir = dir) {
const files = [];
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...getAllMarkdownFiles(fullPath, baseDir));
} else if (entry.name.endsWith('.md') || entry.name.endsWith('.mdx')) {
const relativePath = path.relative(baseDir, fullPath);
files.push(relativePath);
}
}
return files;
}
function stripFrontmatter(content) {
// Remove YAML frontmatter (between --- markers)
const frontmatterRegex = /^---\n[\s\S]*?\n---\n/;
return content.replace(frontmatterRegex, '');
}
function cleanMarkdown(content) {
let cleaned = stripFrontmatter(content);
// Remove import statements
cleaned = cleaned.replace(/^import\s+.*$/gm, '');
// Remove empty lines at start
cleaned = cleaned.replace(/^\n+/, '');
return cleaned;
}
function generateLlmsFullTxt() {
console.log('Generating llms-full.txt...');
// Get all markdown files
const allFiles = getAllMarkdownFiles(DOCS_DIR);
// Create ordered list: prioritized files first, then remaining files
const orderedFiles = [];
const remainingFiles = new Set(allFiles);
// Add prioritized files in order
for (const file of DOC_ORDER) {
if (remainingFiles.has(file)) {
orderedFiles.push(file);
remainingFiles.delete(file);
}
}
// Add remaining files (sorted alphabetically)
const sortedRemaining = Array.from(remainingFiles).sort();
orderedFiles.push(...sortedRemaining);
// Build the output
const sections = [];
// Header
sections.push(`# Hindsight Documentation
> Agent Memory that Works Like Human Memory
This file contains the complete Hindsight documentation for LLM consumption.
Generated: ${new Date().toISOString()}
---
`);
// Process each file
for (const file of orderedFiles) {
const filePath = path.join(DOCS_DIR, file);
if (!fs.existsSync(filePath)) {
console.warn(` Warning: ${file} not found, skipping`);
continue;
}
const content = fs.readFileSync(filePath, 'utf-8');
const cleanedContent = cleanMarkdown(content);
if (cleanedContent.trim()) {
// Add file path as context
sections.push(`\n## File: ${file}\n`);
sections.push(cleanedContent);
sections.push('\n---\n');
console.log(` Added: ${file}`);
}
}
// Write output
const output = sections.join('\n');
fs.writeFileSync(OUTPUT_FILE, output);
const stats = fs.statSync(OUTPUT_FILE);
const sizeKb = (stats.size / 1024).toFixed(1);
console.log(`\nGenerated: ${OUTPUT_FILE}`);
console.log(`Size: ${sizeKb} KB`);
console.log(`Files included: ${orderedFiles.length}`);
}
generateLlmsFullTxt();
+36 -4
View File
@@ -101,6 +101,11 @@ const sidebars: SidebarsConfig = {
id: 'developer/installation',
label: 'Installation',
},
{
type: 'doc',
id: 'developer/services',
label: 'Services',
},
{
type: 'doc',
id: 'developer/configuration',
@@ -173,19 +178,46 @@ const sidebars: SidebarsConfig = {
},
{
type: 'category',
label: 'Use Cases',
label: 'Recipes',
collapsible: false,
items: [
{
type: 'doc',
id: 'cookbook/per-user-memory',
id: 'cookbook/recipes/quickstart',
label: 'Hindsight Quickstart',
},
{
type: 'doc',
id: 'cookbook/recipes/per-user-memory',
label: 'Per-User Memory',
},
{
type: 'doc',
id: 'cookbook/support-agent-with-shared-knowledge',
label: 'Support Agent + Shared Knowledge',
id: 'cookbook/recipes/support-agent-shared-knowledge',
label: 'Support Agent with Shared Knowledge',
},
{
type: 'doc',
id: 'cookbook/recipes/litellm-memory-demo',
label: 'Hindsight Memory Demo with LiteLLM',
},
{
type: 'doc',
id: 'cookbook/recipes/tool-learning-demo',
label: 'Hindsight Tool Learning Demo',
}
],
},
{
type: 'category',
label: 'Applications',
collapsible: false,
items: [
{
type: 'doc',
id: 'cookbook/applications/openai-fitness-coach',
label: 'OpenAI Agent + Hindsight Memory Integration',
}
],
},
],
@@ -0,0 +1,80 @@
.carouselSection {
margin: 2rem 0;
}
.sectionTitle {
font-size: 1.5rem;
margin-bottom: 1rem;
font-weight: 600;
}
.carousel {
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: thin;
scrollbar-color: var(--ifm-color-emphasis-400) transparent;
padding-bottom: 0.5rem;
margin: 0 -1rem;
padding: 0 1rem;
}
.carousel::-webkit-scrollbar {
height: 6px;
}
.carousel::-webkit-scrollbar-track {
background: transparent;
}
.carousel::-webkit-scrollbar-thumb {
background-color: var(--ifm-color-emphasis-400);
border-radius: 3px;
}
.carouselTrack {
display: flex;
gap: 1rem;
padding-bottom: 0.5rem;
}
.card {
flex: 0 0 auto;
padding: 0.75rem 1rem;
border-radius: 8px;
border: 1px solid var(--ifm-color-emphasis-300);
background: var(--ifm-background-surface-color);
text-decoration: none;
color: inherit;
display: flex;
align-items: center;
gap: 0.5rem;
transition: all 0.2s ease;
white-space: nowrap;
}
.card:hover {
text-decoration: none;
border-color: var(--ifm-color-primary);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
[data-theme='dark'] .card {
background: var(--ifm-background-color);
border-color: var(--ifm-color-emphasis-400);
}
[data-theme='dark'] .card:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
border-color: var(--ifm-color-primary);
}
.cardTitle {
font-size: 0.95rem;
font-weight: 500;
color: var(--ifm-font-color-base);
}
.cardLink {
color: var(--ifm-color-primary);
font-weight: 500;
}
@@ -0,0 +1,31 @@
import React from 'react';
import Link from '@docusaurus/Link';
import styles from './RecipeCarousel.module.css';
export interface RecipeCard {
title: string;
href: string;
}
interface RecipeCarouselProps {
title: string;
items: RecipeCard[];
}
export default function RecipeCarousel({ title, items }: RecipeCarouselProps): React.ReactElement {
return (
<div className={styles.carouselSection}>
<h2 className={styles.sectionTitle}>{title}</h2>
<div className={styles.carousel}>
<div className={styles.carouselTrack}>
{items.map((item, index) => (
<Link key={index} to={item.href} className={styles.card}>
<span className={styles.cardTitle}>{item.title}</span>
<span className={styles.cardLink}></span>
</Link>
))}
</div>
</div>
</div>
);
}
File diff suppressed because it is too large Load Diff
+21 -2
View File
@@ -13,7 +13,7 @@
},
"hindsight-clients/typescript": {
"name": "@vectorize-io/hindsight-client",
"version": "0.1.4",
"version": "0.1.5",
"license": "MIT",
"devDependencies": {
"@hey-api/openapi-ts": "^0.88.0",
@@ -25,7 +25,7 @@
}
},
"hindsight-control-plane": {
"version": "0.1.4",
"version": "0.1.5",
"license": "ISC",
"dependencies": {
"@radix-ui/react-checkbox": "^1.3.3",
@@ -63,6 +63,9 @@
"tailwindcss-animate": "^1.0.7",
"three": "^0.182.0",
"typescript": "^5.9.3"
},
"devDependencies": {
"prettier": "^3.7.4"
}
},
"hindsight-control-plane/node_modules/@types/node": {
@@ -23922,6 +23925,22 @@
"node": ">= 0.8.0"
}
},
"node_modules/prettier": {
"version": "3.7.4",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz",
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/pretty-error": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz",
+46
View File
@@ -0,0 +1,46 @@
#!/bin/bash
# Lint and format Node/TypeScript code (hindsight-control-plane only)
set -e
REPO_ROOT="$(git rev-parse --show-toplevel)"
# Get staged JS/TS files in control-plane only
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '^hindsight-control-plane/.*\.(js|jsx|ts|tsx)$' || true)
if [ -z "$STAGED_FILES" ]; then
echo " No Node/TS files to lint"
exit 0
fi
cd "$REPO_ROOT/hindsight-control-plane"
# Check if node_modules exists
if [ ! -d "node_modules" ]; then
echo " node_modules not found, skipping Node lint"
exit 0
fi
echo " Linting and formatting Node/TS files..."
# Convert to relative paths
RELATIVE_FILES=""
for file in $STAGED_FILES; do
RELATIVE_FILES="$RELATIVE_FILES ${file#hindsight-control-plane/}"
done
# Run ESLint with --fix
npx eslint --fix $RELATIVE_FILES || true
# Run Prettier for formatting
npx prettier --write $RELATIVE_FILES || true
# Re-add fixed files to staging
cd "$REPO_ROOT"
for file in $STAGED_FILES; do
if [ -f "$file" ]; then
git add "$file"
fi
done
echo " Node lint complete"
+46
View File
@@ -0,0 +1,46 @@
#!/bin/bash
# Lint Python code with Ruff (hindsight-api and hindsight packages only)
set -e
REPO_ROOT="$(git rev-parse --show-toplevel)"
# Get staged Python files in hindsight-api or hindsight directories only
STAGED_PY_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '^(hindsight-api|hindsight)/.*\.py$' || true)
if [ -z "$STAGED_PY_FILES" ]; then
echo " No Python files to lint"
exit 0
fi
cd "$REPO_ROOT/hindsight-api"
# Check if ruff is available
if ! uv run ruff --version &> /dev/null; then
echo " Ruff not installed, skipping Python lint"
exit 0
fi
echo " Linting Python files with Ruff..."
# Convert to absolute paths
ABSOLUTE_FILES=""
for file in $STAGED_PY_FILES; do
ABSOLUTE_FILES="$ABSOLUTE_FILES $REPO_ROOT/$file"
done
# Run ruff check with fix
uv run ruff check --fix $ABSOLUTE_FILES
# Run ruff format
uv run ruff format $ABSOLUTE_FILES
# Re-add fixed files to staging
cd "$REPO_ROOT"
for file in $STAGED_PY_FILES; do
if [ -f "$file" ]; then
git add "$file"
fi
done
echo " Python lint complete"
-35
View File
@@ -1,35 +0,0 @@
#!/bin/bash
# Regenerate llms-full.txt when docs change
LOG_PREFIX=" "
# Check if any docs files are staged
DOCS_CHANGED=$(git diff --cached --name-only -- 'hindsight-docs/docs/**/*.md' 'hindsight-docs/docs/**/*.mdx' 2>/dev/null || true)
if [ -z "$DOCS_CHANGED" ]; then
echo "${LOG_PREFIX}No docs changes, skipping"
exit 0
fi
echo "${LOG_PREFIX}Docs changed, regenerating llms-full.txt..."
# Check if npm is available and hindsight-docs exists
if [ ! -d "hindsight-docs" ] || ! command -v npm &> /dev/null; then
echo "${LOG_PREFIX}Warning: Cannot regenerate llms-full.txt (missing hindsight-docs or npm)"
exit 0
fi
cd hindsight-docs
# Run the generate script
if npm run generate-llms --silent 2>/dev/null; then
# Check if llms-full.txt changed
if [ -n "$(git diff --name-only -- static/llms-full.txt 2>/dev/null)" ]; then
echo "${LOG_PREFIX}llms-full.txt updated, staging..."
git add static/llms-full.txt
else
echo "${LOG_PREFIX}llms-full.txt unchanged"
fi
else
echo "${LOG_PREFIX}Warning: Failed to regenerate llms-full.txt"
fi
+13
View File
@@ -0,0 +1,13 @@
#!/bin/bash
# Sync cookbook content from hindsight-cookbook repository
# Converts notebooks to markdown and updates the docs
set -e
cd "$(dirname "$0")/.."
echo "Syncing cookbook..."
uv run sync-cookbook
echo ""
echo "Done! Run 'npm run serve' to preview."
Generated
+35 -7
View File
@@ -1215,6 +1215,7 @@ dev = [
{ name = "pytest-timeout" },
{ name = "pytest-xdist" },
{ name = "python-dotenv" },
{ name = "ruff" },
]
[package.metadata]
@@ -1234,7 +1235,7 @@ requires-dist = [
{ name = "opentelemetry-exporter-prometheus", specifier = ">=0.41b0" },
{ name = "opentelemetry-instrumentation-fastapi", specifier = ">=0.41b0" },
{ name = "opentelemetry-sdk", specifier = ">=1.20.0" },
{ name = "pg0-embedded", specifier = ">=0.1.0" },
{ name = "pg0-embedded", specifier = ">=0.11.0" },
{ name = "pgvector", specifier = ">=0.4.1" },
{ name = "psycopg2-binary", specifier = ">=2.9.11" },
{ name = "pydantic", specifier = ">=2.0.0" },
@@ -1263,6 +1264,7 @@ dev = [
{ name = "pytest-timeout", specifier = ">=2.4.0" },
{ name = "pytest-xdist", specifier = ">=3.8.0" },
{ name = "python-dotenv", specifier = ">=1.2.1" },
{ name = "ruff", specifier = ">=0.8.0" },
]
[[package]]
@@ -2506,14 +2508,14 @@ wheels = [
[[package]]
name = "pg0-embedded"
version = "0.10.1"
version = "0.11.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5d/2a/26aed143a5bc4396321c5016c3e7574d596b94122e4de555ec21ebd1f135/pg0_embedded-0.10.1.tar.gz", hash = "sha256:afbfa9e050bec48587d55410e2a93694390c8fb50e1bbab2ac22a36a7eec146d", size = 17619 }
sdist = { url = "https://files.pythonhosted.org/packages/ad/6c/ed900aeea802f6217d6979a16084903fb454d3d149b3f4dbe7ff019407db/pg0_embedded-0.11.0.tar.gz", hash = "sha256:f086e1980e142fddf540b9eabef156ffab432b41a2673e5e1e0c2fb97b83bfba", size = 17692 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/08/61/03f089d9d812e782db200f330e7cf35903834d7f106a2d650bb92c73c8d7/pg0_embedded-0.10.1-py3-none-macosx_14_0_arm64.whl", hash = "sha256:913010ad1a2321367f47cdc907a1639537af36597cd9b61549a341d8da3f249b", size = 13073670 },
{ url = "https://files.pythonhosted.org/packages/72/03/d6e64310c59da880cda4931216f328f72a014c19722fa754b1a0d6422cbb/pg0_embedded-0.10.1-py3-none-manylinux_2_35_aarch64.whl", hash = "sha256:b6f2fc089e844a67dbc1b16899f582ca857744bdd7842b4166a6eecce807a5af", size = 14785516 },
{ url = "https://files.pythonhosted.org/packages/7b/25/a2f84a1c142b48c2a41f14765721650076e2be56762b5ad7cd72ae32e5e4/pg0_embedded-0.10.1-py3-none-manylinux_2_35_x86_64.whl", hash = "sha256:f2ae4ed1ce0aa42a310f20b1ea47dd6091f0f74106b7ded39e9c089e7f80ab25", size = 15224456 },
{ url = "https://files.pythonhosted.org/packages/d0/a8/64963aef0d6ae720b88068441ebdede95b727218c26927e0b8c17d91cf2f/pg0_embedded-0.10.1-py3-none-win_amd64.whl", hash = "sha256:39516c952edc050fbb9e24c35d28c9e013f108879b9c8e91091325231cbdb5a1", size = 54977766 },
{ url = "https://files.pythonhosted.org/packages/65/07/ee9cd32ec3a81c1fca01a83dcd8e92ff98dd5b21e9c3bee85d688ec18be2/pg0_embedded-0.11.0-py3-none-macosx_14_0_arm64.whl", hash = "sha256:4fb4a6ba596d84b19bebfcd1ccff0a531b6b716cf57209e1a016b3cbf1c04454", size = 13077882 },
{ url = "https://files.pythonhosted.org/packages/e6/88/444f74d883c6838420dc26f833146c4acd2bf52f029a41c66e7107fc94d8/pg0_embedded-0.11.0-py3-none-manylinux_2_35_aarch64.whl", hash = "sha256:cdb68aadb47938bc7e4cf3794ddcd36f3278a26aa2863ea6008288658b7d545a", size = 14788819 },
{ url = "https://files.pythonhosted.org/packages/9e/cb/f7f023942957f98e89e6d170bfe6fdeee92c4df6c93ba57b7d301b4c5226/pg0_embedded-0.11.0-py3-none-manylinux_2_35_x86_64.whl", hash = "sha256:c93871b38f0ae2e69e3ce1d58f3f30c5245e2b40b2dc8b9bf367e5942492f041", size = 15230448 },
{ url = "https://files.pythonhosted.org/packages/12/3c/81b7f01a2d008c0b842919ffbfab3bd582c55fed060ff07ef39a9a027742/pg0_embedded-0.11.0-py3-none-win_amd64.whl", hash = "sha256:b67639f4dd280936492c3ba409dff1ace6911ecad9f4a5a4139d11e4fd0dd913", size = 54980453 },
]
[[package]]
@@ -3673,6 +3675,32 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696 },
]
[[package]]
name = "ruff"
version = "0.14.9"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f6/1b/ab712a9d5044435be8e9a2beb17cbfa4c241aa9b5e4413febac2a8b79ef2/ruff-0.14.9.tar.gz", hash = "sha256:35f85b25dd586381c0cc053f48826109384c81c00ad7ef1bd977bfcc28119d5b", size = 5809165 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b8/1c/d1b1bba22cffec02351c78ab9ed4f7d7391876e12720298448b29b7229c1/ruff-0.14.9-py3-none-linux_armv6l.whl", hash = "sha256:f1ec5de1ce150ca6e43691f4a9ef5c04574ad9ca35c8b3b0e18877314aba7e75", size = 13576541 },
{ url = "https://files.pythonhosted.org/packages/94/ab/ffe580e6ea1fca67f6337b0af59fc7e683344a43642d2d55d251ff83ceae/ruff-0.14.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ed9d7417a299fc6030b4f26333bf1117ed82a61ea91238558c0268c14e00d0c2", size = 13779363 },
{ url = "https://files.pythonhosted.org/packages/7d/f8/2be49047f929d6965401855461e697ab185e1a6a683d914c5c19c7962d9e/ruff-0.14.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d5dc3473c3f0e4a1008d0ef1d75cee24a48e254c8bed3a7afdd2b4392657ed2c", size = 12925292 },
{ url = "https://files.pythonhosted.org/packages/9e/e9/08840ff5127916bb989c86f18924fd568938b06f58b60e206176f327c0fe/ruff-0.14.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84bf7c698fc8f3cb8278830fb6b5a47f9bcc1ed8cb4f689b9dd02698fa840697", size = 13362894 },
{ url = "https://files.pythonhosted.org/packages/31/1c/5b4e8e7750613ef43390bb58658eaf1d862c0cc3352d139cd718a2cea164/ruff-0.14.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aa733093d1f9d88a5d98988d8834ef5d6f9828d03743bf5e338bf980a19fce27", size = 13311482 },
{ url = "https://files.pythonhosted.org/packages/5b/3a/459dce7a8cb35ba1ea3e9c88f19077667a7977234f3b5ab197fad240b404/ruff-0.14.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a1cfb04eda979b20c8c19550c8b5f498df64ff8da151283311ce3199e8b3648", size = 14016100 },
{ url = "https://files.pythonhosted.org/packages/a6/31/f064f4ec32524f9956a0890fc6a944e5cf06c63c554e39957d208c0ffc45/ruff-0.14.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1e5cb521e5ccf0008bd74d5595a4580313844a42b9103b7388eca5a12c970743", size = 15477729 },
{ url = "https://files.pythonhosted.org/packages/7a/6d/f364252aad36ccd443494bc5f02e41bf677f964b58902a17c0b16c53d890/ruff-0.14.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd429a8926be6bba4befa8cdcf3f4dd2591c413ea5066b1e99155ed245ae42bb", size = 15122386 },
{ url = "https://files.pythonhosted.org/packages/20/02/e848787912d16209aba2799a4d5a1775660b6a3d0ab3944a4ccc13e64a02/ruff-0.14.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab208c1b7a492e37caeaf290b1378148f75e13c2225af5d44628b95fd7834273", size = 14497124 },
{ url = "https://files.pythonhosted.org/packages/f3/51/0489a6a5595b7760b5dbac0dd82852b510326e7d88d51dbffcd2e07e3ff3/ruff-0.14.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72034534e5b11e8a593f517b2f2f2b273eb68a30978c6a2d40473ad0aaa4cb4a", size = 14195343 },
{ url = "https://files.pythonhosted.org/packages/f6/53/3bb8d2fa73e4c2f80acc65213ee0830fa0c49c6479313f7a68a00f39e208/ruff-0.14.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:712ff04f44663f1b90a1195f51525836e3413c8a773574a7b7775554269c30ed", size = 14346425 },
{ url = "https://files.pythonhosted.org/packages/ad/04/bdb1d0ab876372da3e983896481760867fc84f969c5c09d428e8f01b557f/ruff-0.14.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a111fee1db6f1d5d5810245295527cda1d367c5aa8f42e0fca9a78ede9b4498b", size = 13258768 },
{ url = "https://files.pythonhosted.org/packages/40/d9/8bf8e1e41a311afd2abc8ad12be1b6c6c8b925506d9069b67bb5e9a04af3/ruff-0.14.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8769efc71558fecc25eb295ddec7d1030d41a51e9dcf127cbd63ec517f22d567", size = 13326939 },
{ url = "https://files.pythonhosted.org/packages/f4/56/a213fa9edb6dd849f1cfbc236206ead10913693c72a67fb7ddc1833bf95d/ruff-0.14.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:347e3bf16197e8a2de17940cd75fd6491e25c0aa7edf7d61aa03f146a1aa885a", size = 13578888 },
{ url = "https://files.pythonhosted.org/packages/33/09/6a4a67ffa4abae6bf44c972a4521337ffce9cbc7808faadede754ef7a79c/ruff-0.14.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7715d14e5bccf5b660f54516558aa94781d3eb0838f8e706fb60e3ff6eff03a8", size = 14314473 },
{ url = "https://files.pythonhosted.org/packages/12/0d/15cc82da5d83f27a3c6b04f3a232d61bc8c50d38a6cd8da79228e5f8b8d6/ruff-0.14.9-py3-none-win32.whl", hash = "sha256:df0937f30aaabe83da172adaf8937003ff28172f59ca9f17883b4213783df197", size = 13202651 },
{ url = "https://files.pythonhosted.org/packages/32/f7/c78b060388eefe0304d9d42e68fab8cffd049128ec466456cef9b8d4f06f/ruff-0.14.9-py3-none-win_amd64.whl", hash = "sha256:c0b53a10e61df15a42ed711ec0bda0c582039cf6c754c49c020084c55b5b0bc2", size = 14702079 },
{ url = "https://files.pythonhosted.org/packages/26/09/7a9520315decd2334afa65ed258fed438f070e31f05a2e43dd480a5e5911/ruff-0.14.9-py3-none-win_arm64.whl", hash = "sha256:8e821c366517a074046d92f0e9213ed1c13dbc5b37a7fc20b07f79b64d62cc84", size = 13744730 },
]
[[package]]
name = "safetensors"
version = "0.6.2"