doc: prepare doc for 0.4.10 (#325)

* doc: prepare doc for 0.4.10

* fixe

* ci
This commit is contained in:
Nicolò Boschi 2026-02-09 11:42:37 +01:00 committed by GitHub
parent c2607d7699
commit a3a9d7b37d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 383 additions and 389 deletions

View file

@ -334,8 +334,9 @@ jobs:
push: false push: false
load: ${{ matrix.variant == 'slim' }} load: ${{ matrix.variant == 'slim' }}
tags: hindsight-${{ matrix.name }}:test tags: hindsight-${{ matrix.name }}:test
cache-from: type=gha,scope=${{ matrix.name }} # Removed GitHub Actions cache (type=gha) - it frequently returns 502 errors
cache-to: type=gha,mode=max,scope=${{ matrix.name }} # causing buildx to fail with "failed to parse error response 502"
# Build will be slower but more reliable
# Only test slim variants to save disk space (they're much smaller) # Only test slim variants to save disk space (they're much smaller)
# Slim variants require external embedding providers # Slim variants require external embedding providers

View file

@ -48,40 +48,35 @@ If you need more control over how and when your agent stores and recalls memorie
### Docker (recommended) ### Docker (recommended)
```bash ```bash
export OPENAI_API_KEY=your-key export OPENAI_API_KEY=sk-xxx
docker run --rm -it --pull always -p 8888:8888 -p 9999:9999 \ 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_API_KEY=$OPENAI_API_KEY \
-e HINDSIGHT_API_LLM_MODEL=o3-mini \
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \ -v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
ghcr.io/vectorize-io/hindsight:latest ghcr.io/vectorize-io/hindsight:latest
``` ```
>API: http://localhost:8888
>UI: http://localhost:9999
You can modify the LLM provider by setting `HINDSIGHT_API_LLM_PROVIDER`. Valid options are `openai`, `anthropic`, `gemini`, `groq`, `ollama`, and `lmstudio`. The documentation provides more details on [supported models](https://hindsight.vectorize.io/developer/models). You can modify the LLM provider by setting `HINDSIGHT_API_LLM_PROVIDER`. Valid options are `openai`, `anthropic`, `gemini`, `groq`, `ollama`, and `lmstudio`. The documentation provides more details on [supported models](https://hindsight.vectorize.io/developer/models).
### Docker compose
### Docker (external PostgreSQL)
```bash ```bash
export OPENAI_API_KEY=sk-xxx
export HINDSIGHT_DB_PASSWORD=choose-a-password
cd docker/docker-compose cd docker/docker-compose
docker compose up
# edit the docker compose file with your favorite editor
nano docker-compose.yaml
# start hindsight with an external PostgeSQL
docker compose up -d
```
```bash
# stop and cleanup the pg volume with the optional parameter -v
docker compose down -v
``` ```
>API: http://localhost:8888
>UI: http://localhost:9999
API: http://localhost:8888 ### Client
UI: http://localhost:9999
Install client:
```bash ```bash
pip install hindsight-client -U pip install hindsight-client -U
@ -89,7 +84,7 @@ pip install hindsight-client -U
npm install @vectorize-io/hindsight-client npm install @vectorize-io/hindsight-client
``` ```
Python example: #### Python
```python ```python
from hindsight_client import Hindsight from hindsight_client import Hindsight
@ -106,7 +101,29 @@ client.recall(bank_id="my-bank", query="What does Alice do?")
client.reflect(bank_id="my-bank", query="Tell me about Alice") client.reflect(bank_id="my-bank", query="Tell me about Alice")
``` ```
### Python (embedded, no Docker) #### Node.js / TypeScript
```bash
npm install @vectorize-io/hindsight-client
```
```javascript
const { HindsightClient } = require('@vectorize-io/hindsight-client');
const main = async () => {
const client = new HindsightClient({ baseUrl: 'http://localhost:8888' });
await client.retain('my-bank', 'Alice loves hiking in Yosemite');
const results = await client.recall('my-bank', 'What does Alice like?');
console.log(results);
}
main();
```
### Python Embedded (no server required)
```bash ```bash
pip install hindsight-all -U pip install hindsight-all -U
@ -126,26 +143,6 @@ with HindsightServer(
results = client.recall(bank_id="my-bank", query="Where does Alice work?") results = client.recall(bank_id="my-bank", query="Where does Alice work?")
``` ```
### Node.js / TypeScript
```bash
npm install @vectorize-io/hindsight-client
```
```javascript
const { HindsightClient } = require('@vectorize-io/hindsight-client');
const example = async () => {
const client = new HindsightClient({ baseUrl: 'http://localhost:8888' });
await client.retain('my-bank', 'Alice loves hiking in Yosemite');
const results = await client.recall('my-bank', 'What does Alice like?');
console.log(results);
}
example();
```
--- ---

View file

@ -1,135 +0,0 @@
# Docker Testing
Scripts for testing Hindsight Docker images locally and in CI.
## Scripts
### `test-image.sh`
General-purpose Docker image test script. Starts a container and verifies it becomes healthy.
**Usage:**
```bash
./docker/test-image.sh <image> [target]
```
**Arguments:**
- `image` - Docker image to test (e.g., `hindsight:test`, `ghcr.io/vectorize-io/hindsight:latest`)
- `target` - Optional: `cp-only` for control plane, `api-only` for API, or `standalone` (default)
**Environment Variables:**
- `GROQ_API_KEY` - Required for API/standalone images
- `HINDSIGHT_API_LLM_PROVIDER` - LLM provider (default: `groq`)
- `HINDSIGHT_API_LLM_MODEL` - LLM model (default: `llama-3.3-70b-versatile`)
- `HINDSIGHT_API_EMBEDDINGS_PROVIDER` - Embeddings provider (for slim images)
- `HINDSIGHT_API_EMBEDDINGS_OPENAI_API_KEY` - OpenAI API key for embeddings
- `HINDSIGHT_API_RERANKER_PROVIDER` - Reranker provider (for slim images)
- `HINDSIGHT_API_COHERE_API_KEY` - Cohere API key for reranking
- `SMOKE_TEST_TIMEOUT` - Timeout in seconds (default: 120)
**Examples:**
Test a full image (with local ML models):
```bash
export GROQ_API_KEY=gsk_xxx
./docker/test-image.sh hindsight:test
```
Test a slim image (with external providers):
```bash
export GROQ_API_KEY=gsk_xxx
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=openai
export HINDSIGHT_API_EMBEDDINGS_OPENAI_API_KEY=sk-xxx
export HINDSIGHT_API_RERANKER_PROVIDER=cohere
export HINDSIGHT_API_COHERE_API_KEY=xxx
./docker/test-image.sh hindsight-slim:test
```
### `test-slim-local.sh`
Convenience wrapper for testing slim images locally. Automatically configures external providers.
**Usage:**
```bash
# Set API keys
export GROQ_API_KEY=gsk_xxx
export OPENAI_API_KEY=sk-xxx
export COHERE_API_KEY=xxx
# Run test
./docker/test-slim-local.sh [image]
```
**Or inline:**
```bash
GROQ_API_KEY=gsk_xxx \
OPENAI_API_KEY=sk-xxx \
COHERE_API_KEY=xxx \
./docker/test-slim-local.sh hindsight-slim:test
```
This script:
- ✅ Validates API keys are set
- ✅ Configures OpenAI embeddings automatically
- ✅ Configures Cohere reranking automatically
- ✅ Calls `test-image.sh` with the right configuration
## Building and Testing Locally
### Build a slim image
```bash
docker build \
--build-arg INCLUDE_LOCAL_MODELS=false \
--build-arg PRELOAD_ML_MODELS=false \
--target standalone \
-t hindsight-slim:test \
-f docker/standalone/Dockerfile \
.
```
### Test the slim image
```bash
# With API keys
export GROQ_API_KEY=gsk_xxx
export OPENAI_API_KEY=sk-xxx
export COHERE_API_KEY=xxx
# Run test
./docker/test-slim-local.sh hindsight-slim:test
```
## Expected Output
**Successful test:**
```
Starting smoke test for: hindsight-slim:test
Target: standalone
Health endpoint: http://localhost:8888/health
Timeout: 120s
Starting container...
Waiting for health endpoint at http://localhost:8888/health...
Still waiting... (10s)
Still waiting... (20s)
Container is healthy after 25s
=== Health Response ===
{
"status": "healthy",
"database": "connected"
}
Smoke test PASSED
```
## CI Integration
These scripts are used in CI to validate Docker images on every PR:
- `.github/workflows/test.yml` - Runs `test-image.sh` for slim variants with OpenAI/Cohere
- `.github/workflows/release.yml` - Can optionally run smoke tests during release
See the workflows for the exact configuration.

View file

@ -35,44 +35,12 @@ services:
hindsight: hindsight:
image: ghcr.io/vectorize-io/hindsight:${HINDSIGHT_VERSION:-latest} image: ghcr.io/vectorize-io/hindsight:${HINDSIGHT_VERSION:-latest}
container_name: hindsight-app container_name: hindsight-app
pull_policy: always
ports: ports:
- "8888:8888" - "8888:8888"
- "9999:9999" - "9999:9999"
environment: environment:
# LLM-configuration for Grog - HINDSIGHT_API_LLM_API_KEY=${OPENAI_API_KEY?Please set the OPENAI_API_KEY env variable}
# - HINDSIGHT_API_LLM_PROVIDER=groq
# - HINDSIGHT_API_LLM_API_KEY=${GROG_API_KEY?Please set the GROG_API_KEY env variable}
# - HINDSIGHT_API_LLM_MODEL=${HINDSIGHT_API_LLM_MODEL:-openai/gpt-oss-20b}
# LLM-configuration for OpenAI
# - HINDSIGHT_API_LLM_PROVIDER=openai
# - HINDSIGHT_API_LLM_API_KEY=${OPENAI_API_KEY?Please set the OPENAI_API_KEY env variable}
# - HINDSIGHT_API_LLM_MODEL=${HINDSIGHT_API_LLM_MODEL:-gpt-4o}
# Gemini
# - HINDSIGHT_API_LLM_PROVIDER=gemini
# - HINDSIGHT_API_LLM_API_KEY=${GEMINI_API_KEY?Please set the GEMINI_API_KEY env variable}
# - HINDSIGHT_API_LLM_MODEL=${HINDSIGHT_API_LLM_MODEL:-gemini-2.0-flash}
# Anthropic
# - HINDSIGHT_API_LLM_PROVIDER=anthropic
# - HINDSIGHT_API_LLM_API_KEY=${ANTHROPIC_API_KEY?Please set the ANTHROPIC_API_KEY env variable}
# - HINDSIGHT_API_LLM_MODEL=${HINDSIGHT_API_LLM_MODEL:-claude-sonnet-4-20250514}
# LLM-configuration for Ollama (local, no API key)
# - HINDSIGHT_API_LLM_PROVIDER=ollama
# - HINDSIGHT_API_LLM_BASE_URL=${HINDSIGHT_API_LLM_BASE_URL:-http://127.0.0.1:11434/v1}
# - HINDSIGHT_API_LLM_MODEL=${HINDSIGHT_API_LLM_MODEL:-llama3.2}
# Configuration for the external Postgres database
- HINDSIGHT_API_DATABASE_URL=postgresql://${HINDSIGHT_DB_USER:-hindsight_user}:${HINDSIGHT_DB_PASSWORD:?Please set the HINDSIGHT_DB_PASSWORD env variable}@db:5432/${HINDSIGHT_DB_NAME:-hindsight_db} - HINDSIGHT_API_DATABASE_URL=postgresql://${HINDSIGHT_DB_USER:-hindsight_user}:${HINDSIGHT_DB_PASSWORD:?Please set the HINDSIGHT_DB_PASSWORD env variable}@db:5432/${HINDSIGHT_DB_NAME:-hindsight_db}
# use public schema, otherwise the app start fails (2026-02-06)
- HINDSIGHT_API_DATABASE_SCHEMA=public
# disable if you don't want automatic migrations on startup
- HINDSIGHT_API_RUN_MIGRATIONS_ON_STARTUP=true
depends_on: depends_on:
- db - db
networks: networks:

View file

@ -650,19 +650,34 @@ class MemoryEngine(MemoryEngineInterface):
generated_content = reflect_result.text or "No content generated" generated_content = reflect_result.text or "No content generated"
# Build reflect_response payload to store # Build reflect_response payload to store
reflect_response = { # based_on contains MemoryFact objects for most types, but plain dicts for directives
"text": reflect_result.text, based_on_serialized: dict[str, list[dict[str, Any]]] = {}
"based_on": { for fact_type, facts in reflect_result.based_on.items():
fact_type: [ serialized_facts = []
for fact in facts:
if isinstance(fact, dict):
# Plain dict (e.g., directives with id, name, content)
serialized_facts.append(
{
"id": str(fact["id"]),
"text": fact.get("text", fact.get("content", fact.get("name", ""))),
"type": fact_type,
}
)
else:
# MemoryFact object with .id and .text attributes
serialized_facts.append(
{ {
"id": str(fact.id), "id": str(fact.id),
"text": fact.text, "text": fact.text,
"type": fact_type, "type": fact_type,
} }
for fact in facts )
] based_on_serialized[fact_type] = serialized_facts
for fact_type, facts in reflect_result.based_on.items()
}, reflect_response = {
"text": reflect_result.text,
"based_on": based_on_serialized,
} }
# Update the mental model with the generated content and reflect_response # Update the mental model with the generated content and reflect_response
@ -4740,20 +4755,36 @@ class MemoryEngine(MemoryEngineInterface):
) )
# Build reflect_response payload to store # Build reflect_response payload to store
reflect_response_payload = { # based_on contains MemoryFact objects for most types, but plain dicts for directives
"text": reflect_result.text, based_on_serialized_payload: dict[str, list[dict[str, Any]]] = {}
"based_on": { for fact_type, facts in reflect_result.based_on.items():
fact_type: [ serialized_facts = []
for fact in facts:
if isinstance(fact, dict):
# Plain dict (e.g., directives with id, name, content)
serialized_facts.append(
{
"id": str(fact["id"]),
"text": fact.get("text", fact.get("content", fact.get("name", ""))),
"type": fact_type,
"context": fact.get("context", None),
}
)
else:
# MemoryFact object with .id, .text, .context attributes
serialized_facts.append(
{ {
"id": str(fact.id), "id": str(fact.id),
"text": fact.text, "text": fact.text,
"type": fact_type, "type": fact_type,
"context": fact.context, # Include context to distinguish directives from mental models in UI "context": fact.context,
} }
for fact in facts )
] based_on_serialized_payload[fact_type] = serialized_facts
for fact_type, facts in reflect_result.based_on.items()
}, reflect_response_payload = {
"text": reflect_result.text,
"based_on": based_on_serialized_payload,
"mental_models": [], # Mental models are included in based_on["mental-models"] "mental_models": [], # Mental models are included in based_on["mental-models"]
} }

View file

@ -8,6 +8,16 @@ This extension enables multi-tenant memory isolation for applications using
Supabase Auth - each authenticated user's memories are stored in a separate Supabase Auth - each authenticated user's memories are stored in a separate
schema, ensuring complete data isolation. schema, ensuring complete data isolation.
Features:
- Local JWT Verification: Validates tokens locally using JWKS public keys
(no network call per request)
- Automatic Schema Isolation: Each user gets {prefix}_{user_id} schema
- Zero User Management: Leverages your existing Supabase Auth setup
- Production Ready: Includes health checks, timeouts, key rotation handling,
and error handling
- Built-in: Ships with Hindsight, no extra installation needed
- Legacy Support: Falls back to /auth/v1/user endpoint for HS256 projects
JWT Verification Strategy: JWT Verification Strategy:
By default, JWTs are verified locally using public keys from the Supabase By default, JWTs are verified locally using public keys from the Supabase
JWKS endpoint (/auth/v1/.well-known/jwks.json). This is the Supabase-recommended JWKS endpoint (/auth/v1/.well-known/jwks.json). This is the Supabase-recommended

View file

@ -852,3 +852,61 @@ class TestMentalModelRefreshTagSecurity:
# Cleanup # Cleanup
await memory.delete_bank(bank_id, request_context=request_context) await memory.delete_bank(bank_id, request_context=request_context)
async def test_refresh_mental_model_with_directives(self, memory: MemoryEngine, request_context):
"""Test that refreshing a mental model with directives works correctly."""
bank_id = f"test-refresh-directives-{uuid.uuid4().hex[:8]}"
# Ensure bank exists
await memory.get_bank_profile(bank_id, request_context=request_context)
# Create a directive
directive = await memory.create_directive(
bank_id=bank_id,
name="Response Style",
content="Always be concise and professional",
request_context=request_context,
)
# Create a concept mental model to refresh
concept = await memory.create_mental_model(
bank_id=bank_id,
name="Team Info",
source_query="Team information summary",
content="Initial team information",
request_context=request_context,
)
# Add some memories
await memory.retain_batch_async(
bank_id=bank_id,
contents=[
{"content": "Alice is the team lead and handles project planning."},
{"content": "Bob is a senior engineer who mentors junior developers."},
],
request_context=request_context,
)
# Wait for retain to complete
await memory.wait_for_background_tasks()
# Refresh the concept mental model (this should include directive in based_on)
refreshed = await memory.refresh_mental_model(
bank_id=bank_id,
mental_model_id=concept["id"],
request_context=request_context,
)
# Wait for background tasks to complete
await memory.wait_for_background_tasks()
# Verify the refresh completed without errors
assert refreshed is not None
assert refreshed["content"] is not None
# Get the updated mental model
updated = await memory.get_mental_model(bank_id, concept["id"], request_context=request_context)
assert updated["content"] != "Initial team information"
# Cleanup
await memory.delete_bank(bank_id, request_context=request_context)

View file

@ -0,0 +1,153 @@
"use client";
import { useState, useEffect } from "react";
import { client } from "@/lib/api";
import { useBank } from "@/lib/bank-context";
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
import { Loader2 } from "lucide-react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
interface Directive {
id: string;
bank_id: string;
name: string;
content: string;
is_active: boolean;
priority: number;
tags: string[];
created_at: string;
}
interface DirectiveDetailModalProps {
directiveId: string | null;
onClose: () => void;
}
const formatDateTime = (dateStr: string) => {
const date = new Date(dateStr);
return `${date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
})} at ${date.toLocaleTimeString("en-US", {
hour: "2-digit",
minute: "2-digit",
hour12: false,
})}`;
};
export function DirectiveDetailModal({ directiveId, onClose }: DirectiveDetailModalProps) {
const { currentBank } = useBank();
const [directive, setDirective] = useState<Directive | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!directiveId || !currentBank) return;
const loadDirective = async () => {
setLoading(true);
setError(null);
setDirective(null);
try {
const data = await client.getDirective(currentBank, directiveId);
setDirective(data);
} catch (err) {
console.error("Error loading directive:", err);
setError((err as Error).message);
} finally {
setLoading(false);
}
};
loadDirective();
}, [directiveId, currentBank]);
const isOpen = directiveId !== null;
return (
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-hidden flex flex-col p-6">
<VisuallyHidden>
<DialogTitle>Directive Details</DialogTitle>
</VisuallyHidden>
{loading ? (
<div className="flex items-center justify-center py-20">
<Loader2 className="w-8 h-8 animate-spin text-muted-foreground" />
</div>
) : error ? (
<div className="flex items-center justify-center py-20">
<div className="text-center text-destructive">
<div className="text-sm">Error: {error}</div>
</div>
</div>
) : directive ? (
<div className="flex-1 overflow-y-auto space-y-6">
{/* Header */}
<div className="pb-5 border-b border-border">
<div className="flex items-center gap-2">
<h3 className="text-xl font-bold text-foreground">{directive.name}</h3>
{!directive.is_active && (
<span className="px-2 py-0.5 rounded-full bg-red-500/10 text-red-600 dark:text-red-400 text-xs font-medium">
Inactive
</span>
)}
</div>
<code className="text-xs font-mono text-muted-foreground/70">{directive.id}</code>
</div>
{/* Created / Priority */}
<div className="flex gap-8">
<div>
<div className="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-1">
Created
</div>
<div className="text-sm text-foreground">
{formatDateTime(directive.created_at)}
</div>
</div>
<div>
<div className="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-1">
Priority
</div>
<div className="text-sm text-foreground">{directive.priority}</div>
</div>
</div>
{/* Content */}
<div>
<div className="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-3">
Content
</div>
<div className="prose prose-base dark:prose-invert max-w-none">
<ReactMarkdown remarkPlugins={[remarkGfm]}>{directive.content}</ReactMarkdown>
</div>
</div>
{/* Tags */}
{directive.tags && directive.tags.length > 0 && (
<div>
<div className="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-3">
Tags
</div>
<div className="flex flex-wrap gap-1.5">
{directive.tags.map((tag: string, idx: number) => (
<span
key={idx}
className="px-2 py-0.5 bg-purple-500/10 text-purple-600 dark:text-purple-400 rounded text-xs"
>
{tag}
</span>
))}
</div>
</div>
)}
</div>
) : null}
</DialogContent>
</Dialog>
);
}

View file

@ -51,6 +51,7 @@ import {
List, List,
} from "lucide-react"; } from "lucide-react";
import { MemoryDetailModal } from "./memory-detail-modal"; import { MemoryDetailModal } from "./memory-detail-modal";
import { DirectiveDetailModal } from "./directive-detail-modal";
interface ReflectResponseBasedOnFact { interface ReflectResponseBasedOnFact {
id: string; id: string;
@ -927,6 +928,7 @@ function MentalModelDetailPanel({
const { currentBank } = useBank(); const { currentBank } = useBank();
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
const [viewMemoryId, setViewMemoryId] = useState<string | null>(null); const [viewMemoryId, setViewMemoryId] = useState<string | null>(null);
const [viewDirectiveId, setViewDirectiveId] = useState<string | null>(null);
const handleRefresh = async () => { const handleRefresh = async () => {
if (!currentBank) return; if (!currentBank) return;
@ -998,14 +1000,13 @@ function MentalModelDetailPanel({
// Helper to determine display label for fact type // Helper to determine display label for fact type
const getFactTypeDisplay = (fact: any) => { const getFactTypeDisplay = (fact: any) => {
if (fact.factType === "mental-models") { if (fact.factType === "directives") {
// Check context to distinguish directives from mental models
if (fact.context?.includes("directive")) {
return { return {
label: "directive", label: "directive",
color: "bg-purple-500/10 text-purple-600 dark:text-purple-400", color: "bg-purple-500/10 text-purple-600 dark:text-purple-400",
}; };
} }
if (fact.factType === "mental-models") {
return { return {
label: "mental model", label: "mental model",
color: "bg-indigo-500/10 text-indigo-600 dark:text-indigo-400", color: "bg-indigo-500/10 text-indigo-600 dark:text-indigo-400",
@ -1160,7 +1161,13 @@ function MentalModelDetailPanel({
variant="outline" variant="outline"
size="sm" size="sm"
className="h-6 text-xs" className="h-6 text-xs"
onClick={() => setViewMemoryId(fact.id)} onClick={() => {
if (fact.factType === "directives") {
setViewDirectiveId(fact.id);
} else {
setViewMemoryId(fact.id);
}
}}
> >
View View
</Button> </Button>
@ -1234,6 +1241,14 @@ function MentalModelDetailPanel({
{viewMemoryId && currentBank && ( {viewMemoryId && currentBank && (
<MemoryDetailModal memoryId={viewMemoryId} onClose={() => setViewMemoryId(null)} /> <MemoryDetailModal memoryId={viewMemoryId} onClose={() => setViewMemoryId(null)} />
)} )}
{/* Directive Detail Modal */}
{viewDirectiveId && currentBank && (
<DirectiveDetailModal
directiveId={viewDirectiveId}
onClose={() => setViewDirectiveId(null)}
/>
)}
</div> </div>
); );
} }

View file

@ -19,7 +19,20 @@ HINDSIGHT_API_TENANT_EXTENSION=hindsight_api.extensions.builtin.tenant:ApiKeyTen
HINDSIGHT_API_TENANT_API_KEY=your-secret-key HINDSIGHT_API_TENANT_API_KEY=your-secret-key
``` ```
For multi-tenant setups with separate schemas per tenant (e.g., JWT-based auth with per-tenant schemas), implement a custom `TenantExtension`. **Built-in: SupabaseTenantExtension**
Validates [Supabase](https://supabase.com) JWTs and provides multi-tenant memory isolation. Each authenticated user gets their own PostgreSQL schema (`{prefix}_{user_id}`), ensuring complete data separation. Performs local JWT verification using JWKS for optimal performance (no network call per request).
```bash
HINDSIGHT_API_TENANT_EXTENSION=hindsight_api.extensions.builtin.supabase_tenant:SupabaseTenantExtension
HINDSIGHT_API_TENANT_SUPABASE_URL=https://your-project.supabase.co
# Optional - only needed for legacy HS256 projects or health check
HINDSIGHT_API_TENANT_SUPABASE_SERVICE_KEY=your-service-role-key
```
See the [source code](https://github.com/vectorize-io/hindsight/blob/main/hindsight-api/hindsight_api/extensions/builtin/supabase_tenant.py) for complete configuration options and implementation details.
For other multi-tenant setups with separate schemas per tenant (e.g., custom JWT-based auth), implement a custom `TenantExtension`.
--- ---
@ -51,6 +64,18 @@ HINDSIGHT_API_OPERATION_VALIDATOR_EXTENSION=mypackage.validators:MyValidator
--- ---
### MCPExtension
Registers additional MCP (Model Context Protocol) tools on the Hindsight MCP server. Enables external packages to add custom tools without modifying core code.
**No built-in implementation** - implement your own to add custom MCP tools.
```bash
HINDSIGHT_API_MCP_EXTENSION=mypackage.mcp:MyMCPExtension
```
---
## Writing Custom Extensions ## Writing Custom Extensions
### Extension Basics ### Extension Basics
@ -161,6 +186,24 @@ class MyValidator(OperationValidatorExtension):
pass pass
``` ```
### Example: Custom MCPExtension
```python
from mcp.server.fastmcp import FastMCP
from hindsight_api.extensions import MCPExtension
from hindsight_api.engine import MemoryEngine
class MyMCPExtension(MCPExtension):
async def register_tools(self, mcp: FastMCP, memory: MemoryEngine) -> None:
@mcp.tool()
async def custom_search(query: str) -> str:
"""Custom MCP tool for specialized search."""
# Access memory engine for operations
pool = await memory._get_pool()
# ... custom logic
return f"Results for: {query}"
```
--- ---
## Deploying Custom Extensions ## Deploying Custom Extensions

View file

@ -1,147 +0,0 @@
# Supabase Tenant Extension for Hindsight
A built-in TenantExtension that validates [Supabase](https://supabase.com) JWTs and provides multi-tenant memory isolation. Each authenticated user gets their own PostgreSQL schema, ensuring complete data separation.
## Features
- **Local JWT Verification** - Validates tokens locally using JWKS public keys (no network call per request)
- **Automatic Schema Isolation** - Each user gets `{prefix}_{user_id}` schema
- **Zero User Management** - Leverages your existing Supabase Auth setup
- **Production Ready** - Includes health checks, timeouts, key rotation handling, and error handling
- **Built-in** - Ships with Hindsight, no extra installation needed
- **Legacy Support** - Falls back to `/auth/v1/user` endpoint for HS256 projects
## Configuration
The Supabase tenant extension is built into Hindsight. Just set the environment variables:
```bash
# Required
HINDSIGHT_API_TENANT_EXTENSION=hindsight_api.extensions.builtin.supabase_tenant:SupabaseTenantExtension
HINDSIGHT_API_TENANT_SUPABASE_URL=https://your-project.supabase.co
# Optional - only needed for legacy HS256 projects or startup health check
HINDSIGHT_API_TENANT_SUPABASE_SERVICE_KEY=your-service-role-key
# Optional
HINDSIGHT_API_TENANT_SCHEMA_PREFIX=user # Default: "user"
```
> **Note:** Most Supabase projects use asymmetric JWT signing (ES256/RS256) and the extension verifies tokens locally using JWKS — no service key needed. The `service_role` key is only required if your project uses legacy HS256 signing or if you want the startup health check.
## Usage
Clients pass their Supabase access token in the Authorization header:
```bash
# Get user's access token from Supabase Auth
TOKEN=$(curl -s -X POST "https://your-project.supabase.co/auth/v1/token?grant_type=password" \
-H "apikey: your-anon-key" \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com", "password": "xxx"}' | jq -r '.access_token')
# Use with Hindsight
curl -X POST "https://your-hindsight-server/v1/default/banks/my-bank/memories" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"items": [{"content": "User preference: likes dark mode"}]}'
```
## How It Works
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Your App │ │ Hindsight │ │ Supabase │
│ │ │ │ │ │
│ 1. User logs │ │ │ │ JWKS keys │
│ in via │────▶│ │ │ fetched once │
│ Supabase │ │ │ │ on startup │
│ │ │ │ │ │
│ 2. App calls │ │ 3. Extension │ │ │
│ Hindsight │────▶│ verifies │ │ │
│ with JWT │ │ JWT locally │ │ │
│ │ │ (no network │ │ │
│ │ │ call) │ │ │
│ │ │ │ │ │
│ │ │ 4. Routes to │ │ │
│ │◀────│ user's │ │ │
│ │ │ schema │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
```
1. On startup, Hindsight fetches JWKS public keys from Supabase (cached for 10 minutes)
2. User authenticates with your app via Supabase Auth
3. Your app calls Hindsight API with the user's JWT
4. Extension verifies the JWT signature locally using cached public keys
5. On success, routes request to user's isolated schema (`user_{uuid}`)
For legacy HS256 projects, the extension falls back to calling `/auth/v1/user` per request.
## Schema Isolation
Each user gets a completely isolated PostgreSQL schema:
```
Hindsight Database
├── Schema: user_abc123_def456 (User A)
│ ├── memories
│ ├── entities
│ └── ...
├── Schema: user_xyz789_... (User B)
│ ├── memories
│ ├── entities
│ └── ...
└── Schema: public (Hindsight internals)
```
User A cannot access User B's data - they're in separate schemas.
## Configuration Options
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `HINDSIGHT_API_TENANT_SUPABASE_URL` | Yes | - | Your Supabase project URL |
| `HINDSIGHT_API_TENANT_SUPABASE_SERVICE_KEY` | No | - | Supabase service_role key (only needed for HS256 projects or health check) |
| `HINDSIGHT_API_TENANT_SCHEMA_PREFIX` | No | `user` | Prefix for schema names (must be a valid Postgres identifier) |
## Deployment Examples
### Docker
```dockerfile
FROM ghcr.io/vectorize-io/hindsight:latest
ENV HINDSIGHT_API_TENANT_EXTENSION=hindsight_api.extensions.builtin.supabase_tenant:SupabaseTenantExtension
```
### Railway
```toml
# railway.toml
[build]
builder = "dockerfile"
dockerfilePath = "Dockerfile"
[deploy]
healthcheckPath = "/health"
```
Set environment variables in Railway dashboard.
## Troubleshooting
| Error | Cause | Solution |
|-------|-------|----------|
| `401 Unauthorized` | Invalid or expired JWT | Get fresh token from Supabase |
| `Missing Authorization header` | No Bearer token sent | Add `Authorization: Bearer <token>` header |
| `Unable to find signing key` | JWT signed with unknown key | Check Supabase JWT algorithm settings |
| `Authentication timeout` | Supabase slow/unreachable (legacy mode) | Check Supabase status, retry |
| `SUPABASE_SERVICE_KEY is required when JWKS is not available` | HS256 project without service key | Provide service_role key or switch to asymmetric JWT signing |
## Contributing
This extension was originally developed by [BrighterBalance](https://brighterbalance.app) for their AI advisor product.
## License
MIT