From a3a9d7b37d5cfcadab0886872221509daef0c0f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Boschi?= Date: Mon, 9 Feb 2026 11:42:37 +0100 Subject: [PATCH] doc: prepare doc for 0.4.10 (#325) * doc: prepare doc for 0.4.10 * fixe * ci --- .github/workflows/test.yml | 5 +- README.md | 77 +++++---- docker/README.md | 135 ---------------- docker/docker-compose/docker-compose.yaml | 34 +--- .../hindsight_api/engine/memory_engine.py | 77 ++++++--- .../extensions/builtin/supabase_tenant.py | 10 ++ hindsight-api/tests/test_mental_models.py | 58 +++++++ .../src/components/directive-detail-modal.tsx | 153 ++++++++++++++++++ .../src/components/mental-models-view.tsx | 31 +++- hindsight-docs/docs/developer/extensions.md | 45 +++++- hindsight-integrations/supabase/README.md | 147 ----------------- 11 files changed, 383 insertions(+), 389 deletions(-) delete mode 100644 docker/README.md create mode 100644 hindsight-control-plane/src/components/directive-detail-modal.tsx delete mode 100644 hindsight-integrations/supabase/README.md diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4fb01123..87bcf7d7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 diff --git a/README.md b/README.md index 43b7a816..b633ed73 100644 --- a/README.md +++ b/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(); -``` --- diff --git a/docker/README.md b/docker/README.md deleted file mode 100644 index f0ce84d1..00000000 --- a/docker/README.md +++ /dev/null @@ -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 [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. diff --git a/docker/docker-compose/docker-compose.yaml b/docker/docker-compose/docker-compose.yaml index 3052ddd8..ddd06bba 100644 --- a/docker/docker-compose/docker-compose.yaml +++ b/docker/docker-compose/docker-compose.yaml @@ -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: diff --git a/hindsight-api/hindsight_api/engine/memory_engine.py b/hindsight-api/hindsight_api/engine/memory_engine.py index 8c1916cb..86b7cf8d 100644 --- a/hindsight-api/hindsight_api/engine/memory_engine.py +++ b/hindsight-api/hindsight_api/engine/memory_engine.py @@ -650,19 +650,34 @@ class MemoryEngine(MemoryEngineInterface): generated_content = reflect_result.text or "No content generated" # Build reflect_response payload to store + # 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, + } + ) + based_on_serialized[fact_type] = serialized_facts + reflect_response = { "text": reflect_result.text, - "based_on": { - fact_type: [ - { - "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": 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 + # 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, + } + ) + based_on_serialized_payload[fact_type] = serialized_facts + reflect_response_payload = { "text": reflect_result.text, - "based_on": { - fact_type: [ - { - "id": str(fact.id), - "text": fact.text, - "type": fact_type, - "context": fact.context, # Include context to distinguish directives from mental models in UI - } - for fact in facts - ] - for fact_type, facts in reflect_result.based_on.items() - }, + "based_on": based_on_serialized_payload, "mental_models": [], # Mental models are included in based_on["mental-models"] } diff --git a/hindsight-api/hindsight_api/extensions/builtin/supabase_tenant.py b/hindsight-api/hindsight_api/extensions/builtin/supabase_tenant.py index 94994199..01c2b1db 100644 --- a/hindsight-api/hindsight_api/extensions/builtin/supabase_tenant.py +++ b/hindsight-api/hindsight_api/extensions/builtin/supabase_tenant.py @@ -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 diff --git a/hindsight-api/tests/test_mental_models.py b/hindsight-api/tests/test_mental_models.py index e3ea5e5f..793f7ee2 100644 --- a/hindsight-api/tests/test_mental_models.py +++ b/hindsight-api/tests/test_mental_models.py @@ -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) diff --git a/hindsight-control-plane/src/components/directive-detail-modal.tsx b/hindsight-control-plane/src/components/directive-detail-modal.tsx new file mode 100644 index 00000000..ff08fc6c --- /dev/null +++ b/hindsight-control-plane/src/components/directive-detail-modal.tsx @@ -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(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(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 ( + !open && onClose()}> + + + Directive Details + + {loading ? ( +
+ +
+ ) : error ? ( +
+
+
Error: {error}
+
+
+ ) : directive ? ( +
+ {/* Header */} +
+
+

{directive.name}

+ {!directive.is_active && ( + + Inactive + + )} +
+ {directive.id} +
+ + {/* Created / Priority */} +
+
+
+ Created +
+
+ {formatDateTime(directive.created_at)} +
+
+
+
+ Priority +
+
{directive.priority}
+
+
+ + {/* Content */} +
+
+ Content +
+
+ {directive.content} +
+
+ + {/* Tags */} + {directive.tags && directive.tags.length > 0 && ( +
+
+ Tags +
+
+ {directive.tags.map((tag: string, idx: number) => ( + + {tag} + + ))} +
+
+ )} +
+ ) : null} +
+
+ ); +} diff --git a/hindsight-control-plane/src/components/mental-models-view.tsx b/hindsight-control-plane/src/components/mental-models-view.tsx index b29cee53..d08b1a05 100644 --- a/hindsight-control-plane/src/components/mental-models-view.tsx +++ b/hindsight-control-plane/src/components/mental-models-view.tsx @@ -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(null); + const [viewDirectiveId, setViewDirectiveId] = useState(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 === "directives") { + return { + label: "directive", + color: "bg-purple-500/10 text-purple-600 dark:text-purple-400", + }; + } if (fact.factType === "mental-models") { - // Check context to distinguish directives from mental models - if (fact.context?.includes("directive")) { - return { - label: "directive", - color: "bg-purple-500/10 text-purple-600 dark:text-purple-400", - }; - } 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 @@ -1234,6 +1241,14 @@ function MentalModelDetailPanel({ {viewMemoryId && currentBank && ( setViewMemoryId(null)} /> )} + + {/* Directive Detail Modal */} + {viewDirectiveId && currentBank && ( + setViewDirectiveId(null)} + /> + )} ); } diff --git a/hindsight-docs/docs/developer/extensions.md b/hindsight-docs/docs/developer/extensions.md index 181b7983..3264fe2e 100644 --- a/hindsight-docs/docs/developer/extensions.md +++ b/hindsight-docs/docs/developer/extensions.md @@ -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 diff --git a/hindsight-integrations/supabase/README.md b/hindsight-integrations/supabase/README.md deleted file mode 100644 index 16c2214d..00000000 --- a/hindsight-integrations/supabase/README.md +++ /dev/null @@ -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 ` 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