doc: prepare doc for 0.4.10 (#325)
* doc: prepare doc for 0.4.10 * fixe * ci
This commit is contained in:
parent
c2607d7699
commit
a3a9d7b37d
11 changed files with 383 additions and 389 deletions
5
.github/workflows/test.yml
vendored
5
.github/workflows/test.yml
vendored
|
|
@ -334,8 +334,9 @@ jobs:
|
|||
push: false
|
||||
load: ${{ matrix.variant == 'slim' }}
|
||||
tags: hindsight-${{ matrix.name }}:test
|
||||
cache-from: type=gha,scope=${{ matrix.name }}
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.name }}
|
||||
# Removed GitHub Actions cache (type=gha) - it frequently returns 502 errors
|
||||
# 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)
|
||||
# Slim variants require external embedding providers
|
||||
|
|
|
|||
77
README.md
77
README.md
|
|
@ -48,40 +48,35 @@ If you need more control over how and when your agent stores and recalls memorie
|
|||
### Docker (recommended)
|
||||
|
||||
```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 \
|
||||
-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
|
||||
|
||||
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
|
||||
export OPENAI_API_KEY=sk-xxx
|
||||
export HINDSIGHT_DB_PASSWORD=choose-a-password
|
||||
cd docker/docker-compose
|
||||
|
||||
# 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
|
||||
docker compose up
|
||||
```
|
||||
|
||||
|
||||
>API: http://localhost:8888
|
||||
>UI: http://localhost:9999
|
||||
|
||||
API: http://localhost:8888
|
||||
UI: http://localhost:9999
|
||||
|
||||
Install client:
|
||||
### Client
|
||||
|
||||
```bash
|
||||
pip install hindsight-client -U
|
||||
|
|
@ -89,7 +84,7 @@ pip install hindsight-client -U
|
|||
npm install @vectorize-io/hindsight-client
|
||||
```
|
||||
|
||||
Python example:
|
||||
#### Python
|
||||
|
||||
```python
|
||||
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")
|
||||
```
|
||||
|
||||
### 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
|
||||
pip install hindsight-all -U
|
||||
|
|
@ -126,26 +143,6 @@ with HindsightServer(
|
|||
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();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
135
docker/README.md
135
docker/README.md
|
|
@ -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.
|
||||
|
|
@ -35,44 +35,12 @@ services:
|
|||
hindsight:
|
||||
image: ghcr.io/vectorize-io/hindsight:${HINDSIGHT_VERSION:-latest}
|
||||
container_name: hindsight-app
|
||||
pull_policy: always
|
||||
ports:
|
||||
- "8888:8888"
|
||||
- "9999:9999"
|
||||
environment:
|
||||
# LLM-configuration for Grog
|
||||
# - 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_LLM_API_KEY=${OPENAI_API_KEY?Please set the OPENAI_API_KEY env variable}
|
||||
- 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:
|
||||
- db
|
||||
networks:
|
||||
|
|
|
|||
|
|
@ -650,19 +650,34 @@ class MemoryEngine(MemoryEngineInterface):
|
|||
generated_content = reflect_result.text or "No content generated"
|
||||
|
||||
# Build reflect_response payload to store
|
||||
reflect_response = {
|
||||
"text": reflect_result.text,
|
||||
"based_on": {
|
||||
fact_type: [
|
||||
# based_on contains MemoryFact objects for most types, but plain dicts for directives
|
||||
based_on_serialized: dict[str, list[dict[str, Any]]] = {}
|
||||
for fact_type, facts in reflect_result.based_on.items():
|
||||
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),
|
||||
"text": fact.text,
|
||||
"type": fact_type,
|
||||
}
|
||||
for fact in facts
|
||||
]
|
||||
for fact_type, facts in reflect_result.based_on.items()
|
||||
},
|
||||
)
|
||||
based_on_serialized[fact_type] = serialized_facts
|
||||
|
||||
reflect_response = {
|
||||
"text": reflect_result.text,
|
||||
"based_on": based_on_serialized,
|
||||
}
|
||||
|
||||
# Update the mental model with the generated content and reflect_response
|
||||
|
|
@ -4740,20 +4755,36 @@ class MemoryEngine(MemoryEngineInterface):
|
|||
)
|
||||
|
||||
# Build reflect_response payload to store
|
||||
reflect_response_payload = {
|
||||
"text": reflect_result.text,
|
||||
"based_on": {
|
||||
fact_type: [
|
||||
# based_on contains MemoryFact objects for most types, but plain dicts for directives
|
||||
based_on_serialized_payload: dict[str, list[dict[str, Any]]] = {}
|
||||
for fact_type, facts in reflect_result.based_on.items():
|
||||
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),
|
||||
"text": fact.text,
|
||||
"type": fact_type,
|
||||
"context": fact.context, # Include context to distinguish directives from mental models in UI
|
||||
"context": fact.context,
|
||||
}
|
||||
for fact in facts
|
||||
]
|
||||
for fact_type, facts in reflect_result.based_on.items()
|
||||
},
|
||||
)
|
||||
based_on_serialized_payload[fact_type] = serialized_facts
|
||||
|
||||
reflect_response_payload = {
|
||||
"text": reflect_result.text,
|
||||
"based_on": based_on_serialized_payload,
|
||||
"mental_models": [], # Mental models are included in based_on["mental-models"]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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:
|
||||
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
|
||||
|
|
|
|||
|
|
@ -852,3 +852,61 @@ class TestMentalModelRefreshTagSecurity:
|
|||
|
||||
# Cleanup
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -51,6 +51,7 @@ import {
|
|||
List,
|
||||
} from "lucide-react";
|
||||
import { MemoryDetailModal } from "./memory-detail-modal";
|
||||
import { DirectiveDetailModal } from "./directive-detail-modal";
|
||||
|
||||
interface ReflectResponseBasedOnFact {
|
||||
id: string;
|
||||
|
|
@ -927,6 +928,7 @@ function MentalModelDetailPanel({
|
|||
const { currentBank } = useBank();
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [viewMemoryId, setViewMemoryId] = useState<string | null>(null);
|
||||
const [viewDirectiveId, setViewDirectiveId] = useState<string | null>(null);
|
||||
|
||||
const handleRefresh = async () => {
|
||||
if (!currentBank) return;
|
||||
|
|
@ -998,14 +1000,13 @@ function MentalModelDetailPanel({
|
|||
|
||||
// Helper to determine display label for fact type
|
||||
const getFactTypeDisplay = (fact: any) => {
|
||||
if (fact.factType === "mental-models") {
|
||||
// Check context to distinguish directives from mental models
|
||||
if (fact.context?.includes("directive")) {
|
||||
if (fact.factType === "directives") {
|
||||
return {
|
||||
label: "directive",
|
||||
color: "bg-purple-500/10 text-purple-600 dark:text-purple-400",
|
||||
};
|
||||
}
|
||||
if (fact.factType === "mental-models") {
|
||||
return {
|
||||
label: "mental model",
|
||||
color: "bg-indigo-500/10 text-indigo-600 dark:text-indigo-400",
|
||||
|
|
@ -1160,7 +1161,13 @@ function MentalModelDetailPanel({
|
|||
variant="outline"
|
||||
size="sm"
|
||||
className="h-6 text-xs"
|
||||
onClick={() => setViewMemoryId(fact.id)}
|
||||
onClick={() => {
|
||||
if (fact.factType === "directives") {
|
||||
setViewDirectiveId(fact.id);
|
||||
} else {
|
||||
setViewMemoryId(fact.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
View
|
||||
</Button>
|
||||
|
|
@ -1234,6 +1241,14 @@ function MentalModelDetailPanel({
|
|||
{viewMemoryId && currentBank && (
|
||||
<MemoryDetailModal memoryId={viewMemoryId} onClose={() => setViewMemoryId(null)} />
|
||||
)}
|
||||
|
||||
{/* Directive Detail Modal */}
|
||||
{viewDirectiveId && currentBank && (
|
||||
<DirectiveDetailModal
|
||||
directiveId={viewDirectiveId}
|
||||
onClose={() => setViewDirectiveId(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,20 @@ HINDSIGHT_API_TENANT_EXTENSION=hindsight_api.extensions.builtin.tenant:ApiKeyTen
|
|||
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
|
||||
|
||||
### Extension Basics
|
||||
|
|
@ -161,6 +186,24 @@ class MyValidator(OperationValidatorExtension):
|
|||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Loading…
Reference in a new issue