Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be45b4367d | ||
|
|
8905461d99 | ||
|
|
4b2772c47a | ||
|
|
122d16569c | ||
|
|
f6da55c5e6 | ||
|
|
3e5efc314d | ||
|
|
ceae079234 | ||
|
|
4e85510ad2 |
@@ -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:
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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
|
||||
|
||||
# =============================================================================
|
||||
|
||||
@@ -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)}")
|
||||
|
||||
@@ -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})...")
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5",
|
||||
"printWidth": 100
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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()}")
|
||||
```
|
||||
+106
-33
@@ -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()}")
|
||||
```
|
||||
+118
-61
@@ -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)">
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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();
|
||||
@@ -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
Generated
+21
-2
@@ -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",
|
||||
|
||||
Executable
+46
@@ -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"
|
||||
Executable
+46
@@ -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"
|
||||
@@ -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
|
||||
Executable
+13
@@ -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."
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user