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
|
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
|
||||||
|
|
|
||||||
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)
|
### 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();
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
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:
|
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:
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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