Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e429229089 | ||
|
|
8e7a402118 | ||
|
|
b553f072fa | ||
|
|
f64817814a | ||
|
|
fa4cbf7ef2 | ||
|
|
2109397028 | ||
|
|
c4ef090a20 | ||
|
|
96f487213c |
@@ -340,6 +340,7 @@ jobs:
|
||||
retention-days: 1
|
||||
|
||||
release-docker-images:
|
||||
name: Release Docker (${{ matrix.image_name }}${{ matrix.tag_suffix }})
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -349,10 +350,28 @@ jobs:
|
||||
include:
|
||||
- target: api-only
|
||||
image_name: hindsight-api
|
||||
tag_suffix: ""
|
||||
build_args: ""
|
||||
- target: api-only
|
||||
image_name: hindsight-api
|
||||
tag_suffix: "-slim"
|
||||
build_args: |
|
||||
INCLUDE_LOCAL_MODELS=false
|
||||
PRELOAD_ML_MODELS=false
|
||||
- target: cp-only
|
||||
image_name: hindsight-control-plane
|
||||
tag_suffix: ""
|
||||
build_args: ""
|
||||
- target: standalone
|
||||
image_name: hindsight
|
||||
tag_suffix: ""
|
||||
build_args: ""
|
||||
- target: standalone
|
||||
image_name: hindsight
|
||||
tag_suffix: "-slim"
|
||||
build_args: |
|
||||
INCLUDE_LOCAL_MODELS=false
|
||||
PRELOAD_ML_MODELS=false
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -390,6 +409,9 @@ jobs:
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository_owner }}/${{ matrix.image_name }}
|
||||
flavor: |
|
||||
latest=auto
|
||||
suffix=${{ matrix.tag_suffix }}
|
||||
tags: |
|
||||
type=semver,pattern={{version}},value=${{ steps.get_version.outputs.VERSION }}
|
||||
type=semver,pattern={{major}}.{{minor}},value=${{ steps.get_version.outputs.VERSION }}
|
||||
@@ -415,7 +437,7 @@ jobs:
|
||||
# - name: Smoke test - verify container starts
|
||||
# env:
|
||||
# GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
|
||||
# run: ./scripts/docker-smoke-test.sh "${{ matrix.image_name }}:test" "${{ matrix.target }}"
|
||||
# run: ./docker/test-image.sh "${{ matrix.image_name }}:test" "${{ matrix.target }}"
|
||||
|
||||
# Build multi-platform and push to release tags
|
||||
- name: Build and push release images
|
||||
@@ -424,6 +446,7 @@ jobs:
|
||||
context: .
|
||||
file: docker/standalone/Dockerfile
|
||||
target: ${{ matrix.target }}
|
||||
build-args: ${{ matrix.build_args }}
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.11', '3.12', '3.13']
|
||||
python-version: ['3.11', '3.12', '3.13', '3.14']
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -277,16 +277,35 @@ jobs:
|
||||
run: helm lint helm/hindsight
|
||||
|
||||
build-docker-images:
|
||||
name: Build Docker (${{ matrix.name }})
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- target: api-only
|
||||
name: api
|
||||
variant: full
|
||||
build_args: ""
|
||||
- target: api-only
|
||||
name: api-slim
|
||||
variant: slim
|
||||
build_args: |
|
||||
INCLUDE_LOCAL_MODELS=false
|
||||
PRELOAD_ML_MODELS=false
|
||||
- target: cp-only
|
||||
name: control-plane
|
||||
variant: full
|
||||
build_args: ""
|
||||
- target: standalone
|
||||
name: standalone
|
||||
variant: full
|
||||
build_args: ""
|
||||
- target: standalone
|
||||
name: standalone-slim
|
||||
variant: slim
|
||||
build_args: |
|
||||
INCLUDE_LOCAL_MODELS=false
|
||||
PRELOAD_ML_MODELS=false
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -305,20 +324,30 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build ${{ matrix.name }} image
|
||||
- name: Build ${{ matrix.name }} image (${{ matrix.variant }})
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/standalone/Dockerfile
|
||||
target: ${{ matrix.target }}
|
||||
build-args: ${{ matrix.build_args }}
|
||||
push: false
|
||||
load: 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 }}
|
||||
|
||||
# TODO: Re-enable smoke test when disk space issue is resolved
|
||||
# - name: Smoke test - verify container starts
|
||||
# env:
|
||||
# GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
|
||||
# run: ./scripts/docker-smoke-test.sh "hindsight-${{ matrix.name }}:test" "${{ matrix.target }}"
|
||||
# Only test slim variants to save disk space (they're much smaller)
|
||||
# Slim variants require external embedding providers
|
||||
- name: Smoke test - verify container starts
|
||||
if: matrix.variant == 'slim'
|
||||
env:
|
||||
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
|
||||
HINDSIGHT_API_EMBEDDINGS_PROVIDER: openai
|
||||
HINDSIGHT_API_EMBEDDINGS_OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
HINDSIGHT_API_RERANKER_PROVIDER: cohere
|
||||
HINDSIGHT_API_COHERE_API_KEY: ${{ secrets.COHERE_API_KEY }}
|
||||
run: ./docker/test-image.sh "hindsight-${{ matrix.name }}:test" "${{ matrix.target }}"
|
||||
|
||||
test-api:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -59,7 +59,7 @@ docker run --rm -it --pull always -p 8888:8888 -p 9999: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).
|
||||
|
||||
API: http://localhost:8888
|
||||
API: http://localhost:8888
|
||||
UI: http://localhost:9999
|
||||
|
||||
Install client:
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
# 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.
|
||||
@@ -6,28 +6,40 @@
|
||||
# Can be run locally or in CI pipelines.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/docker-smoke-test.sh <image> [target]
|
||||
# ./docker/test-image.sh <image> [target]
|
||||
#
|
||||
# Arguments:
|
||||
# image - Docker image to test (e.g., hindsight-api:test, ghcr.io/vectorize-io/hindsight:latest)
|
||||
# target - Optional: 'cp-only' for control plane, otherwise assumes API image (default: api)
|
||||
#
|
||||
# Environment variables:
|
||||
# GROQ_API_KEY - Required for API/standalone images (LLM verification)
|
||||
# HINDSIGHT_API_LLM_PROVIDER - LLM provider (default: groq)
|
||||
# HINDSIGHT_API_LLM_MODEL - LLM model (default: llama-3.3-70b-versatile)
|
||||
# SMOKE_TEST_TIMEOUT - Timeout in seconds (default: 120)
|
||||
# SMOKE_TEST_CONTAINER_NAME - Container name (default: hindsight-smoke-test)
|
||||
# GROQ_API_KEY - Required for API/standalone images (LLM verification)
|
||||
# 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 (optional, for slim images: openai, cohere, tei)
|
||||
# HINDSIGHT_API_EMBEDDINGS_OPENAI_API_KEY - OpenAI API key for embeddings (optional)
|
||||
# HINDSIGHT_API_RERANKER_PROVIDER - Reranker provider (optional, for slim images: cohere, tei)
|
||||
# HINDSIGHT_API_COHERE_API_KEY - Cohere API key for reranking (optional)
|
||||
# SMOKE_TEST_TIMEOUT - Timeout in seconds (default: 120)
|
||||
# SMOKE_TEST_CONTAINER_NAME - Container name (default: hindsight-smoke-test)
|
||||
#
|
||||
# Examples:
|
||||
# # Test a locally built image
|
||||
# ./scripts/docker-smoke-test.sh hindsight-api:test
|
||||
# # Test a locally built full image
|
||||
# ./docker/test-image.sh hindsight-api:test
|
||||
#
|
||||
# # Test a released image
|
||||
# ./scripts/docker-smoke-test.sh ghcr.io/vectorize-io/hindsight:latest
|
||||
# ./docker/test-image.sh ghcr.io/vectorize-io/hindsight:latest
|
||||
#
|
||||
# # Test control plane image
|
||||
# ./scripts/docker-smoke-test.sh hindsight-control-plane:test cp-only
|
||||
# ./docker/test-image.sh hindsight-control-plane:test cp-only
|
||||
#
|
||||
# # Test slim image with external providers
|
||||
# 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
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 - Success (container healthy)
|
||||
@@ -108,12 +120,32 @@ if [ "$TARGET" = "cp-only" ]; then
|
||||
-p "${HEALTH_PORT}:${HEALTH_PORT}" \
|
||||
"$IMAGE"
|
||||
else
|
||||
docker run -d --name "$CONTAINER_NAME" \
|
||||
-e HINDSIGHT_API_LLM_PROVIDER="$LLM_PROVIDER" \
|
||||
-e HINDSIGHT_API_LLM_API_KEY="${GROQ_API_KEY}" \
|
||||
-e HINDSIGHT_API_LLM_MODEL="$LLM_MODEL" \
|
||||
-p "${HEALTH_PORT}:${HEALTH_PORT}" \
|
||||
"$IMAGE"
|
||||
# Build docker run command with required and optional env vars
|
||||
DOCKER_CMD="docker run -d --name $CONTAINER_NAME"
|
||||
DOCKER_CMD="$DOCKER_CMD -e HINDSIGHT_API_LLM_PROVIDER=$LLM_PROVIDER"
|
||||
DOCKER_CMD="$DOCKER_CMD -e HINDSIGHT_API_LLM_API_KEY=${GROQ_API_KEY}"
|
||||
DOCKER_CMD="$DOCKER_CMD -e HINDSIGHT_API_LLM_MODEL=$LLM_MODEL"
|
||||
|
||||
# Add optional embeddings provider config
|
||||
if [ -n "${HINDSIGHT_API_EMBEDDINGS_PROVIDER:-}" ]; then
|
||||
DOCKER_CMD="$DOCKER_CMD -e HINDSIGHT_API_EMBEDDINGS_PROVIDER=${HINDSIGHT_API_EMBEDDINGS_PROVIDER}"
|
||||
fi
|
||||
if [ -n "${HINDSIGHT_API_EMBEDDINGS_OPENAI_API_KEY:-}" ]; then
|
||||
DOCKER_CMD="$DOCKER_CMD -e HINDSIGHT_API_EMBEDDINGS_OPENAI_API_KEY=${HINDSIGHT_API_EMBEDDINGS_OPENAI_API_KEY}"
|
||||
fi
|
||||
|
||||
# Add optional reranker provider config
|
||||
if [ -n "${HINDSIGHT_API_RERANKER_PROVIDER:-}" ]; then
|
||||
DOCKER_CMD="$DOCKER_CMD -e HINDSIGHT_API_RERANKER_PROVIDER=${HINDSIGHT_API_RERANKER_PROVIDER}"
|
||||
fi
|
||||
if [ -n "${HINDSIGHT_API_COHERE_API_KEY:-}" ]; then
|
||||
DOCKER_CMD="$DOCKER_CMD -e HINDSIGHT_API_COHERE_API_KEY=${HINDSIGHT_API_COHERE_API_KEY}"
|
||||
fi
|
||||
|
||||
DOCKER_CMD="$DOCKER_CMD -p ${HEALTH_PORT}:${HEALTH_PORT}"
|
||||
DOCKER_CMD="$DOCKER_CMD $IMAGE"
|
||||
|
||||
eval $DOCKER_CMD
|
||||
fi
|
||||
|
||||
# Wait for health endpoint
|
||||
Executable
+51
@@ -0,0 +1,51 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Local Test Script for Slim Docker Images
|
||||
#
|
||||
# This script makes it easy to test slim images locally with external providers.
|
||||
# It expects API keys to be set in environment variables.
|
||||
#
|
||||
# Usage:
|
||||
# export GROQ_API_KEY=gsk_xxx
|
||||
# export OPENAI_API_KEY=sk-xxx
|
||||
# export COHERE_API_KEY=xxx
|
||||
# ./docker/test-slim-local.sh
|
||||
#
|
||||
# Or inline:
|
||||
# GROQ_API_KEY=gsk_xxx OPENAI_API_KEY=sk_xxx COHERE_API_KEY=xxx ./docker/test-slim-local.sh
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Check for required API keys
|
||||
if [ -z "${GROQ_API_KEY:-}" ]; then
|
||||
echo "❌ Error: GROQ_API_KEY environment variable is required"
|
||||
echo "Set it with: export GROQ_API_KEY=gsk_xxx"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${OPENAI_API_KEY:-}" ]; then
|
||||
echo "❌ Error: OPENAI_API_KEY environment variable is required"
|
||||
echo "Set it with: export OPENAI_API_KEY=sk-xxx"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${COHERE_API_KEY:-}" ]; then
|
||||
echo "❌ Error: COHERE_API_KEY environment variable is required"
|
||||
echo "Set it with: export COHERE_API_KEY=xxx"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Configuration
|
||||
IMAGE="${1:-hindsight-slim:test}"
|
||||
echo "Testing image: $IMAGE"
|
||||
echo ""
|
||||
|
||||
# Set up external providers
|
||||
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=openai
|
||||
export HINDSIGHT_API_EMBEDDINGS_OPENAI_API_KEY=$OPENAI_API_KEY
|
||||
export HINDSIGHT_API_RERANKER_PROVIDER=cohere
|
||||
export HINDSIGHT_API_COHERE_API_KEY=$COHERE_API_KEY
|
||||
|
||||
# Run the test
|
||||
exec "$(dirname "$0")/test-image.sh" "$IMAGE" standalone
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
"""Fix mental_models primary key to be scoped per bank
|
||||
|
||||
Revision ID: w8r9s0t1u2v3
|
||||
Revises: v7q8r9s0t1u2
|
||||
Create Date: 2026-02-05
|
||||
|
||||
This migration fixes a critical bank isolation bug where mental_models.id was
|
||||
globally unique across all banks instead of being scoped per bank. This caused
|
||||
conflicts when different banks tried to use the same custom ID.
|
||||
|
||||
CRITICAL FIX: Changes primary key from (id) to (bank_id, id) to ensure proper isolation.
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import context, op
|
||||
|
||||
revision: str = "w8r9s0t1u2v3"
|
||||
down_revision: str | Sequence[str] | None = "v7q8r9s0t1u2"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def _get_schema_prefix() -> str:
|
||||
"""Get schema prefix for table names (required for multi-tenant support)."""
|
||||
schema = context.config.get_main_option("target_schema")
|
||||
return f'"{schema}".' if schema else ""
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Change mental_models primary key from (id) to (bank_id, id) for proper bank isolation."""
|
||||
schema = _get_schema_prefix()
|
||||
|
||||
# Drop the old primary key constraint (just id)
|
||||
# Note: The constraint might be named differently on different DBs
|
||||
# Try both old names (pinned_reflections_pkey from original, mental_models_pkey from rename)
|
||||
op.execute(f"ALTER TABLE {schema}mental_models DROP CONSTRAINT IF EXISTS pinned_reflections_pkey")
|
||||
op.execute(f"ALTER TABLE {schema}mental_models DROP CONSTRAINT IF EXISTS mental_models_pkey")
|
||||
|
||||
# Create the new composite primary key (bank_id, id)
|
||||
# This ensures IDs are scoped per bank, not globally
|
||||
op.execute(f"""
|
||||
ALTER TABLE {schema}mental_models
|
||||
ADD CONSTRAINT mental_models_pkey PRIMARY KEY (bank_id, id)
|
||||
""")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Revert mental_models primary key from (bank_id, id) to (id)."""
|
||||
schema = _get_schema_prefix()
|
||||
|
||||
# Drop the composite primary key
|
||||
op.execute(f"ALTER TABLE {schema}mental_models DROP CONSTRAINT IF EXISTS mental_models_pkey")
|
||||
|
||||
# Restore the old primary key (just id)
|
||||
# WARNING: This downgrade will fail if there are duplicate IDs across banks
|
||||
op.execute(f"""
|
||||
ALTER TABLE {schema}mental_models
|
||||
ADD CONSTRAINT mental_models_pkey PRIMARY KEY (id)
|
||||
""")
|
||||
@@ -523,7 +523,9 @@ class ReflectFact(BaseModel):
|
||||
)
|
||||
|
||||
id: str | None = None
|
||||
text: str
|
||||
text: str = Field(
|
||||
description="Fact text. When type='observation', this contains markdown-formatted consolidated knowledge"
|
||||
)
|
||||
type: str | None = None # fact type: world, experience, observation
|
||||
context: str | None = None
|
||||
occurred_start: str | None = None
|
||||
@@ -588,7 +590,7 @@ class ReflectResponse(BaseModel):
|
||||
model_config = ConfigDict(
|
||||
json_schema_extra={
|
||||
"example": {
|
||||
"text": "Based on my understanding, AI is a transformative technology...",
|
||||
"text": "## AI Overview\n\nBased on my understanding, AI is a **transformative technology**:\n\n- Used extensively in healthcare\n- Discussed in recent conversations\n- Continues to evolve rapidly",
|
||||
"based_on": {
|
||||
"memories": [
|
||||
{"id": "123", "text": "AI is used in healthcare", "type": "world"},
|
||||
@@ -616,7 +618,9 @@ class ReflectResponse(BaseModel):
|
||||
}
|
||||
)
|
||||
|
||||
text: str
|
||||
text: str = Field(
|
||||
description="The reflect response as well-formatted markdown (headers, lists, bold/italic, code blocks, etc.)"
|
||||
)
|
||||
based_on: ReflectBasedOn | None = Field(
|
||||
default=None,
|
||||
description="Evidence used to generate the response. Only present when include.facts is set.",
|
||||
@@ -1114,7 +1118,9 @@ class MentalModelResponse(BaseModel):
|
||||
bank_id: str
|
||||
name: str
|
||||
source_query: str
|
||||
content: str
|
||||
content: str = Field(
|
||||
description="The mental model content as well-formatted markdown (auto-generated from reflect endpoint)"
|
||||
)
|
||||
tags: list[str] = Field(default_factory=list)
|
||||
max_tokens: int = Field(default=2048)
|
||||
trigger: MentalModelTrigger = Field(default_factory=MentalModelTrigger)
|
||||
|
||||
@@ -447,6 +447,22 @@ class HindsightConfig:
|
||||
# Reflect agent settings
|
||||
reflect_max_iterations: int
|
||||
|
||||
def validate(self) -> None:
|
||||
"""Validate configuration values and raise errors for invalid combinations."""
|
||||
# RETAIN_MAX_COMPLETION_TOKENS must be greater than RETAIN_CHUNK_SIZE
|
||||
# to ensure the LLM has enough output capacity to extract facts from chunks
|
||||
if self.retain_max_completion_tokens <= self.retain_chunk_size:
|
||||
raise ValueError(
|
||||
f"Invalid configuration: HINDSIGHT_API_RETAIN_MAX_COMPLETION_TOKENS "
|
||||
f"({self.retain_max_completion_tokens}) must be greater than "
|
||||
f"HINDSIGHT_API_RETAIN_CHUNK_SIZE ({self.retain_chunk_size}). "
|
||||
f"\n\nYou have two options to fix this:"
|
||||
f"\n 1. Increase HINDSIGHT_API_RETAIN_MAX_COMPLETION_TOKENS to a value > {self.retain_chunk_size}"
|
||||
f"\n 2. Use a model that supports at least {self.retain_max_completion_tokens} output tokens"
|
||||
f"\n (current model: {self.retain_llm_model or self.llm_model}, "
|
||||
f"provider: {self.retain_llm_provider or self.llm_provider})"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_env(cls) -> "HindsightConfig":
|
||||
"""Create configuration from environment variables."""
|
||||
@@ -454,7 +470,7 @@ class HindsightConfig:
|
||||
llm_provider = os.getenv(ENV_LLM_PROVIDER, DEFAULT_LLM_PROVIDER)
|
||||
llm_model = os.getenv(ENV_LLM_MODEL) or _get_default_model_for_provider(llm_provider)
|
||||
|
||||
return cls(
|
||||
config = cls(
|
||||
# Database
|
||||
database_url=os.getenv(ENV_DATABASE_URL, DEFAULT_DATABASE_URL),
|
||||
database_schema=os.getenv(ENV_DATABASE_SCHEMA, DEFAULT_DATABASE_SCHEMA),
|
||||
@@ -631,6 +647,8 @@ class HindsightConfig:
|
||||
# Reflect agent settings
|
||||
reflect_max_iterations=int(os.getenv(ENV_REFLECT_MAX_ITERATIONS, str(DEFAULT_REFLECT_MAX_ITERATIONS))),
|
||||
)
|
||||
config.validate()
|
||||
return config
|
||||
|
||||
def get_llm_base_url(self) -> str:
|
||||
"""Get the LLM base URL, with provider-specific defaults."""
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
CONSOLIDATION_SYSTEM_PROMPT = """You are a memory consolidation system. Your job is to convert facts into durable knowledge (observations) and merge with existing knowledge when appropriate.
|
||||
|
||||
You must output ONLY valid JSON with no markdown formatting, no code blocks, and no additional text.
|
||||
You must output ONLY valid JSON with no markdown code blocks or additional text. However, the "text" field within each observation should use markdown formatting (headers, lists, bold, etc.) for clarity and readability.
|
||||
|
||||
## EXTRACT DURABLE KNOWLEDGE, NOT EPHEMERAL STATE
|
||||
Facts often describe events or actions. Extract the DURABLE KNOWLEDGE implied by the fact, not the transient state.
|
||||
@@ -71,10 +71,15 @@ Instructions:
|
||||
- New topic → CREATE new observation
|
||||
- Purely ephemeral → return []
|
||||
|
||||
Output JSON array of actions:
|
||||
Output JSON array of actions (the "text" field should use markdown formatting for structure):
|
||||
[
|
||||
{{"action": "update", "learning_id": "uuid-from-observations", "text": "updated knowledge", "reason": "..."}},
|
||||
{{"action": "create", "text": "new durable knowledge", "reason": "..."}}
|
||||
{{"action": "update", "learning_id": "uuid-from-observations", "text": "## Updated Knowledge\n\n**Key point**: details here\n\n- Supporting detail 1\n- Supporting detail 2", "reason": "..."}},
|
||||
{{"action": "create", "text": "## New Durable Knowledge\n\nDescription with **emphasis** and proper structure", "reason": "..."}}
|
||||
]
|
||||
|
||||
Return [] if fact contains no durable knowledge."""
|
||||
Return [] if fact contains no durable knowledge.
|
||||
|
||||
IMPORTANT: Format the "text" field with markdown for better readability:
|
||||
- Use headers, lists, bold/italic, tables where appropriate
|
||||
- CRITICAL: Add blank lines before and after block elements (tables, code blocks, lists)
|
||||
- Ensure proper spacing for markdown to render correctly"""
|
||||
|
||||
@@ -65,6 +65,7 @@ class MockLLM(LLMInterface):
|
||||
# Storage for test verification
|
||||
self._mock_calls: list[dict] = []
|
||||
self._mock_response: Any = None
|
||||
self._mock_exception: Exception | None = None
|
||||
|
||||
async def verify_connection(self) -> None:
|
||||
"""
|
||||
@@ -124,6 +125,10 @@ class MockLLM(LLMInterface):
|
||||
self._mock_calls.append(call_record)
|
||||
logger.debug(f"Mock LLM call recorded: scope={scope}, model={self.model}")
|
||||
|
||||
# Raise mock exception if configured
|
||||
if self._mock_exception is not None:
|
||||
raise self._mock_exception
|
||||
|
||||
# Return mock response
|
||||
if self._mock_response is not None:
|
||||
result = self._mock_response
|
||||
@@ -183,6 +188,10 @@ class MockLLM(LLMInterface):
|
||||
}
|
||||
self._mock_calls.append(call_record)
|
||||
|
||||
# Raise mock exception if configured
|
||||
if self._mock_exception is not None:
|
||||
raise self._mock_exception
|
||||
|
||||
if self._mock_response is not None:
|
||||
if isinstance(self._mock_response, LLMToolCallResult):
|
||||
return self._mock_response
|
||||
@@ -215,6 +224,16 @@ class MockLLM(LLMInterface):
|
||||
"""
|
||||
self._mock_response = response
|
||||
|
||||
def set_mock_exception(self, exception: Exception) -> None:
|
||||
"""
|
||||
Set an exception to raise from mock calls.
|
||||
|
||||
Args:
|
||||
exception: The exception to raise on the next call.
|
||||
After raising, the exception is cleared.
|
||||
"""
|
||||
self._mock_exception = exception
|
||||
|
||||
def get_mock_calls(self) -> list[dict]:
|
||||
"""
|
||||
Get the list of recorded mock calls.
|
||||
@@ -230,5 +249,6 @@ class MockLLM(LLMInterface):
|
||||
return self._mock_calls
|
||||
|
||||
def clear_mock_calls(self) -> None:
|
||||
"""Clear the recorded mock calls."""
|
||||
"""Clear the recorded mock calls and any set exception."""
|
||||
self._mock_calls = []
|
||||
self._mock_exception = None
|
||||
|
||||
@@ -31,7 +31,7 @@ class ReflectAction(BaseModel):
|
||||
default=None, description="Observation sections for done action (when output_mode=observations)"
|
||||
)
|
||||
# Plain text answer fields (for output_mode=answer)
|
||||
answer: str | None = Field(default=None, description="Plain text answer for done action (no markdown)")
|
||||
answer: str | None = Field(default=None, description="Well-formatted markdown answer for done action")
|
||||
answer_memory_ids: list[str] | None = Field(
|
||||
default=None, description="Memory IDs supporting the answer", alias="memory_ids"
|
||||
)
|
||||
|
||||
@@ -148,7 +148,15 @@ def build_system_prompt_for_tools(
|
||||
|
||||
parts = []
|
||||
|
||||
# Inject directives at the VERY START for maximum prominence
|
||||
# Anti-hallucination rule at the very top
|
||||
parts.extend(
|
||||
[
|
||||
"CRITICAL: You MUST ONLY use information from retrieved tool results. NEVER make up names, people, events, or entities.",
|
||||
"",
|
||||
]
|
||||
)
|
||||
|
||||
# Inject directives after anti-hallucination rule
|
||||
if directives:
|
||||
parts.append(build_directives_section(directives))
|
||||
|
||||
@@ -162,7 +170,7 @@ def build_system_prompt_for_tools(
|
||||
parts.extend(
|
||||
[
|
||||
"## CRITICAL RULES",
|
||||
"- You must NEVER fabricate information that has no basis in retrieved data",
|
||||
"- ONLY use information from tool results - no external knowledge or guessing",
|
||||
"- You SHOULD synthesize, infer, and reason from the retrieved memories",
|
||||
"- You MUST search before saying you don't have information",
|
||||
"",
|
||||
@@ -300,9 +308,11 @@ def build_system_prompt_for_tools(
|
||||
parts.extend(
|
||||
[
|
||||
"",
|
||||
"## Output Format: Plain Text Answer",
|
||||
"Call done() with a plain text 'answer' field.",
|
||||
"- Do NOT use markdown formatting",
|
||||
"## Output Format: Well-Formatted Markdown Answer",
|
||||
"Call done() with a well-formatted markdown 'answer' field.",
|
||||
"- USE markdown formatting for structure (headers, lists, bold, italic, code blocks, tables, etc.)",
|
||||
"- CRITICAL: Add blank lines before and after block elements (tables, code blocks, lists)",
|
||||
"- Format for clarity and readability with proper spacing and hierarchy",
|
||||
"- NEVER include memory IDs, UUIDs, or 'Memory references' in the answer text",
|
||||
"- Put IDs ONLY in the memory_ids/mental_model_ids/observation_ids arrays, not in the answer",
|
||||
]
|
||||
@@ -474,19 +484,30 @@ def build_final_prompt(
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
FINAL_SYSTEM_PROMPT = """You are a thoughtful assistant that synthesizes answers from retrieved memories.
|
||||
FINAL_SYSTEM_PROMPT = """CRITICAL: You MUST ONLY use information from retrieved tool results. NEVER make up names, people, events, or entities.
|
||||
|
||||
You are a thoughtful assistant that synthesizes answers from retrieved memories.
|
||||
|
||||
Your approach:
|
||||
- Reason over the retrieved memories to answer the question
|
||||
- Make reasonable inferences when the exact answer isn't explicitly stated
|
||||
- Connect related memories to form a complete picture
|
||||
- Be helpful - if you have related information, use it to give the best possible answer
|
||||
- ONLY use information from tool results - no external knowledge or guessing
|
||||
|
||||
Only say "I don't have information" if the retrieved data is truly unrelated to the question.
|
||||
Do NOT fabricate information that has no basis in the retrieved data.
|
||||
|
||||
FORMATTING: Use proper markdown formatting in your answer:
|
||||
- Headers (##, ###) for sections
|
||||
- Lists (bullet or numbered) for enumerations
|
||||
- Bold/italic for emphasis
|
||||
- Tables with proper syntax (ensure blank line before and after)
|
||||
- Code blocks where appropriate
|
||||
- CRITICAL: Always add blank lines before and after block elements (tables, code blocks, lists)
|
||||
- Proper spacing between sections
|
||||
|
||||
CRITICAL: Output ONLY the final synthesized answer. Do NOT include:
|
||||
- Meta-commentary about what you're doing ("I'll search...", "Let me analyze...")
|
||||
- Explanations of your reasoning process
|
||||
- Descriptions of your approach
|
||||
Just provide the direct answer."""
|
||||
Just provide the direct answer with proper markdown formatting."""
|
||||
|
||||
@@ -139,7 +139,7 @@ TOOL_DONE_ANSWER = {
|
||||
"properties": {
|
||||
"answer": {
|
||||
"type": "string",
|
||||
"description": "Your response as plain text. Do NOT use markdown formatting. NEVER include memory IDs, UUIDs, or 'Memory references' in this text - put IDs only in memory_ids array.",
|
||||
"description": "Your response as well-formatted markdown. Use headers, lists, bold/italic, and code blocks for clarity. NEVER include memory IDs, UUIDs, or 'Memory references' in this text - put IDs only in memory_ids array.",
|
||||
},
|
||||
"memory_ids": {
|
||||
"type": "array",
|
||||
@@ -190,7 +190,7 @@ def _build_done_tool_with_directives(directive_rules: list[str]) -> dict:
|
||||
"properties": {
|
||||
"answer": {
|
||||
"type": "string",
|
||||
"description": "Your response as plain text. Do NOT use markdown formatting. NEVER include memory IDs, UUIDs, or 'Memory references' in this text - put IDs only in memory_ids array.",
|
||||
"description": "Your response as well-formatted markdown. Use headers, lists, bold/italic, and code blocks for clarity. NEVER include memory IDs, UUIDs, or 'Memory references' in this text - put IDs only in memory_ids array.",
|
||||
},
|
||||
"memory_ids": {
|
||||
"type": "array",
|
||||
|
||||
@@ -542,7 +542,12 @@ Output: ONLY 2 facts (skip coffee preference - too trivial):
|
||||
QUALITY OVER QUANTITY
|
||||
══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
Ask: "Would this be useful to recall in 6 months?" If no, skip it."""
|
||||
Ask: "Would this be useful to recall in 6 months?" If no, skip it.
|
||||
|
||||
IMPORTANT: Sensory/emotional details and observations that provide meaningful context
|
||||
about experiences ARE important to remember, even if they seem small (e.g., how food
|
||||
tasted, how someone looked, how loud music was). Extract these if they characterize
|
||||
an experience or person."""
|
||||
|
||||
# Assembled concise prompt (backward compatible - exact same output as before)
|
||||
CONCISE_FACT_EXTRACTION_PROMPT = _BASE_FACT_EXTRACTION_PROMPT.format(
|
||||
@@ -641,6 +646,7 @@ For EVENTS (fact_kind="event") - MUST SET BOTH occurred_start AND occurred_end:
|
||||
- Convert relative dates → absolute using Event Date as reference
|
||||
- If Event Date is "Saturday, March 15, 2020", then "yesterday" = Friday, March 14, 2020
|
||||
- Dates mentioned in text (e.g., "in March 2020") should use THAT year, not current year
|
||||
- CRITICAL: If the content mentions an absolute date (e.g., "March 15, 2024", "2024-03-15"), you MUST extract it and set occurred_start in ISO format
|
||||
- Always include the day name (Monday, Tuesday, etc.) in the 'when' field
|
||||
- Set occurred_start AND occurred_end to WHEN IT HAPPENED (not when mentioned)
|
||||
- For single-day/point events: set occurred_end = occurred_start (same timestamp)
|
||||
@@ -1005,6 +1011,29 @@ Text:
|
||||
|
||||
except BadRequestError as e:
|
||||
last_error = e
|
||||
error_str = str(e).lower()
|
||||
|
||||
# Check if error is related to max_tokens/completion_tokens not being supported
|
||||
if any(
|
||||
keyword in error_str
|
||||
for keyword in [
|
||||
"max_tokens",
|
||||
"max_completion_tokens",
|
||||
"maximum context",
|
||||
"token limit",
|
||||
"context length",
|
||||
]
|
||||
):
|
||||
# Provide helpful error message with configuration suggestions
|
||||
raise ValueError(
|
||||
f"Model does not support the required output token limit.\n\n"
|
||||
f"The model '{llm_config.model}' (provider: {llm_config.provider}) failed with: {e}\n\n"
|
||||
f"You have two options to fix this:\n"
|
||||
f" 1. Use a different model that supports at least {config.retain_max_completion_tokens} output tokens\n"
|
||||
f" 2. Decrease HINDSIGHT_API_RETAIN_MAX_COMPLETION_TOKENS to a value your model supports\n"
|
||||
f" (current value: {config.retain_max_completion_tokens}, must be > RETAIN_CHUNK_SIZE={config.retain_chunk_size})"
|
||||
) from e
|
||||
|
||||
if "json_validate_failed" in str(e):
|
||||
logger.warning(
|
||||
f" [1.3.{chunk_index + 1}] Attempt {attempt + 1}/{max_retries} failed with JSON validation error: {e}"
|
||||
@@ -1347,28 +1376,21 @@ def _convert_causal_relations(relations_from_llm, fact_start_idx: int) -> list[C
|
||||
|
||||
def _add_temporal_offsets(facts: list[ExtractedFactType], contents: list[RetainContent]) -> None:
|
||||
"""
|
||||
Add time offsets to preserve fact ordering within each content.
|
||||
Add time offsets to preserve fact ordering across all contents.
|
||||
|
||||
This allows retrieval to distinguish between facts that happened earlier vs later
|
||||
in the same conversation, even when the base event_date is the same.
|
||||
This allows retrieval to distinguish between facts from different documents/conversations
|
||||
even when they have the same base event_date, and also between facts within the same
|
||||
conversation.
|
||||
|
||||
Uses absolute position across all facts to ensure unique timestamps.
|
||||
|
||||
Modifies facts in place.
|
||||
"""
|
||||
from .orchestrator import parse_datetime_flexible
|
||||
|
||||
# Group facts by content_index
|
||||
current_content_idx = 0
|
||||
content_fact_start = 0
|
||||
|
||||
for i, fact in enumerate(facts):
|
||||
if fact.content_index != current_content_idx:
|
||||
# Moved to next content
|
||||
current_content_idx = fact.content_index
|
||||
content_fact_start = i
|
||||
|
||||
# Calculate position within this content
|
||||
fact_position = i - content_fact_start
|
||||
offset = timedelta(seconds=fact_position * SECONDS_PER_FACT)
|
||||
# Use absolute position across all facts to ensure uniqueness across different contents
|
||||
offset = timedelta(seconds=i * SECONDS_PER_FACT)
|
||||
|
||||
# Apply offset to all temporal fields (handle both datetime objects and ISO strings)
|
||||
if fact.occurred_start:
|
||||
|
||||
@@ -188,7 +188,7 @@ def get_system_message(disposition: DispositionTraits) -> str:
|
||||
" ".join(instructions) if instructions else "Balance your disposition traits when interpreting information."
|
||||
)
|
||||
|
||||
return f"You are a person with your own thoughts, experiences, opinions, and disposition. When asked a question, respond naturally like a human would - share what you genuinely think based on what you know and have experienced. {disposition_instruction} Be direct, express your views confidently, and use 'I think', 'I believe', 'in my view', etc. Respond in plain text without markdown formatting. IMPORTANT: Detect the language of the question and respond in the SAME language. Do not translate to English if the question is in another language."
|
||||
return f"You are a person with your own thoughts, experiences, opinions, and disposition. When asked a question, respond naturally like a human would - share what you genuinely think based on what you know and have experienced. {disposition_instruction} Be direct, express your views confidently, and use 'I think', 'I believe', 'in my view', etc. Respond in plain text without markdown formatting. CRITICAL: ONLY use the facts and information provided in the prompt - do not make up names, events, or information that weren't mentioned. If you don't have enough information to answer, say so. IMPORTANT: Detect the language of the question and respond in the SAME language. Do not translate to English if the question is in another language."
|
||||
|
||||
|
||||
async def reflect(
|
||||
|
||||
@@ -88,6 +88,7 @@ def should_skip_provider(provider: str, model: str = "") -> tuple[bool, str]:
|
||||
|
||||
@pytest.mark.parametrize("provider,model", MODEL_MATRIX)
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.timeout(300) # Increase timeout for slow models like groq gpt-oss-120b
|
||||
async def test_llm_provider_api_methods(provider: str, model: str):
|
||||
"""
|
||||
Test all LLM API methods used by Hindsight at runtime.
|
||||
@@ -141,27 +142,32 @@ async def test_llm_provider_api_methods(provider: str, model: str):
|
||||
pytest.fail(f"{provider}/{model} call() plain text failed: {e}")
|
||||
|
||||
# Test 3: call() with response_format (structured output)
|
||||
try:
|
||||
from pydantic import BaseModel
|
||||
# Skip for models that don't support structured output
|
||||
skip_structured_output = (provider == "groq" and "gpt-oss-120b" in model.lower())
|
||||
if skip_structured_output:
|
||||
print(f" ⊘ call() structured output: skipped (model doesn't support response_format)")
|
||||
else:
|
||||
try:
|
||||
from pydantic import BaseModel
|
||||
|
||||
class TestResponse(BaseModel):
|
||||
answer: str
|
||||
confidence: str
|
||||
class TestResponse(BaseModel):
|
||||
answer: str
|
||||
confidence: str
|
||||
|
||||
response = await llm.call(
|
||||
messages=[
|
||||
{"role": "system", "content": "You are a math assistant."},
|
||||
{"role": "user", "content": "What is the capital of France?"},
|
||||
],
|
||||
response_format=TestResponse,
|
||||
max_completion_tokens=100,
|
||||
)
|
||||
assert isinstance(response, TestResponse), f"Expected TestResponse, got {type(response)}"
|
||||
assert hasattr(response, "answer"), "Structured output missing 'answer' field"
|
||||
assert hasattr(response, "confidence"), "Structured output missing 'confidence' field"
|
||||
print(f" ✓ call() structured output: answer={response.answer}, confidence={response.confidence}")
|
||||
except Exception as e:
|
||||
pytest.fail(f"{provider}/{model} call() structured output failed: {e}")
|
||||
response = await llm.call(
|
||||
messages=[
|
||||
{"role": "system", "content": "You are a math assistant."},
|
||||
{"role": "user", "content": "What is the capital of France?"},
|
||||
],
|
||||
response_format=TestResponse,
|
||||
max_completion_tokens=100,
|
||||
)
|
||||
assert isinstance(response, TestResponse), f"Expected TestResponse, got {type(response)}"
|
||||
assert hasattr(response, "answer"), "Structured output missing 'answer' field"
|
||||
assert hasattr(response, "confidence"), "Structured output missing 'confidence' field"
|
||||
print(f" ✓ call() structured output: answer={response.answer}, confidence={response.confidence}")
|
||||
except Exception as e:
|
||||
pytest.fail(f"{provider}/{model} call() structured output failed: {e}")
|
||||
|
||||
# Test 4: call_with_tools() (tool calling)
|
||||
try:
|
||||
@@ -189,7 +195,7 @@ async def test_llm_provider_api_methods(provider: str, model: str):
|
||||
{"role": "user", "content": "What's the weather like in Paris?"},
|
||||
],
|
||||
tools=tools,
|
||||
max_completion_tokens=200,
|
||||
max_completion_tokens=500, # Increased from 200 to give models enough space for tool calls
|
||||
)
|
||||
|
||||
assert result is not None, "call_with_tools() returned None"
|
||||
|
||||
@@ -17,7 +17,7 @@ import pprint
|
||||
import re # noqa: F401
|
||||
import json
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, StrictInt, StrictStr
|
||||
from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr
|
||||
from typing import Any, ClassVar, Dict, List, Optional
|
||||
from hindsight_client_api.models.mental_model_trigger import MentalModelTrigger
|
||||
from typing import Optional, Set
|
||||
@@ -31,7 +31,7 @@ class MentalModelResponse(BaseModel):
|
||||
bank_id: StrictStr
|
||||
name: StrictStr
|
||||
source_query: StrictStr
|
||||
content: StrictStr
|
||||
content: StrictStr = Field(description="The mental model content as well-formatted markdown (auto-generated from reflect endpoint)")
|
||||
tags: Optional[List[StrictStr]] = None
|
||||
max_tokens: Optional[StrictInt] = 2048
|
||||
trigger: Optional[MentalModelTrigger] = None
|
||||
|
||||
@@ -17,7 +17,7 @@ import pprint
|
||||
import re # noqa: F401
|
||||
import json
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, StrictStr
|
||||
from pydantic import BaseModel, ConfigDict, Field, StrictStr
|
||||
from typing import Any, ClassVar, Dict, List, Optional
|
||||
from typing import Optional, Set
|
||||
from typing_extensions import Self
|
||||
@@ -27,7 +27,7 @@ class ReflectFact(BaseModel):
|
||||
A fact used in think response.
|
||||
""" # noqa: E501
|
||||
id: Optional[StrictStr] = None
|
||||
text: StrictStr
|
||||
text: StrictStr = Field(description="Fact text. When type='observation', this contains markdown-formatted consolidated knowledge")
|
||||
type: Optional[StrictStr] = None
|
||||
context: Optional[StrictStr] = None
|
||||
occurred_start: Optional[StrictStr] = None
|
||||
|
||||
@@ -17,7 +17,7 @@ import pprint
|
||||
import re # noqa: F401
|
||||
import json
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, StrictStr
|
||||
from pydantic import BaseModel, ConfigDict, Field, StrictStr
|
||||
from typing import Any, ClassVar, Dict, List, Optional
|
||||
from hindsight_client_api.models.reflect_based_on import ReflectBasedOn
|
||||
from hindsight_client_api.models.reflect_trace import ReflectTrace
|
||||
@@ -29,7 +29,7 @@ class ReflectResponse(BaseModel):
|
||||
"""
|
||||
Response model for think endpoint.
|
||||
""" # noqa: E501
|
||||
text: StrictStr
|
||||
text: StrictStr = Field(description="The reflect response as well-formatted markdown (headers, lists, bold/italic, code blocks, etc.)")
|
||||
based_on: Optional[ReflectBasedOn] = None
|
||||
structured_output: Optional[Dict[str, Any]] = None
|
||||
usage: Optional[TokenUsage] = None
|
||||
|
||||
@@ -1034,6 +1034,8 @@ export type MentalModelResponse = {
|
||||
source_query: string;
|
||||
/**
|
||||
* Content
|
||||
*
|
||||
* The mental model content as well-formatted markdown (auto-generated from reflect endpoint)
|
||||
*/
|
||||
content: string;
|
||||
/**
|
||||
@@ -1382,6 +1384,8 @@ export type ReflectFact = {
|
||||
id?: string | null;
|
||||
/**
|
||||
* Text
|
||||
*
|
||||
* Fact text. When type='observation', this contains markdown-formatted consolidated knowledge
|
||||
*/
|
||||
text: string;
|
||||
/**
|
||||
@@ -1523,6 +1527,8 @@ export type ReflectRequest = {
|
||||
export type ReflectResponse = {
|
||||
/**
|
||||
* Text
|
||||
*
|
||||
* The reflect response as well-formatted markdown (headers, lists, bold/italic, code blocks, etc.)
|
||||
*/
|
||||
text: string;
|
||||
/**
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
"react-markdown": "^10.1.0",
|
||||
"react18-json-view": "^0.2.9",
|
||||
"recharts": "^3.5.1",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
|
||||
@@ -189,4 +189,75 @@ input[type="datetime-local"]::-webkit-calendar-picker-indicator {
|
||||
|
||||
.dark input[type="datetime-local"]::-webkit-calendar-picker-indicator {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
/* Markdown table styles - explicitly override Tailwind reset */
|
||||
.prose table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
font-size: 0.875em;
|
||||
line-height: 1.5;
|
||||
border: 2px solid rgba(0, 0, 0, 0.2) !important;
|
||||
}
|
||||
|
||||
.prose thead {
|
||||
border-bottom: 3px solid rgba(0, 0, 0, 0.3) !important;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.prose thead th {
|
||||
padding: 0.5rem 0.75rem;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
vertical-align: bottom;
|
||||
border: 1px solid rgba(0, 0, 0, 0.2) !important;
|
||||
border-bottom-width: 3px !important;
|
||||
}
|
||||
|
||||
.prose tbody tr {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.15) !important;
|
||||
}
|
||||
|
||||
.prose tbody tr:last-child {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.15) !important;
|
||||
}
|
||||
|
||||
.prose tbody td {
|
||||
padding: 0.5rem 0.75rem;
|
||||
vertical-align: top;
|
||||
border: 1px solid rgba(0, 0, 0, 0.15) !important;
|
||||
}
|
||||
|
||||
.prose tbody tr:hover {
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
/* Dark mode table styles - use white borders with transparency */
|
||||
.dark .prose table {
|
||||
color: hsl(var(--foreground));
|
||||
border: 2px solid rgba(255, 255, 255, 0.2) !important;
|
||||
}
|
||||
|
||||
.dark .prose thead {
|
||||
border-bottom: 3px solid rgba(255, 255, 255, 0.3) !important;
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.dark .prose thead th {
|
||||
border: 1px solid rgba(255, 255, 255, 0.2) !important;
|
||||
border-bottom-width: 3px !important;
|
||||
}
|
||||
|
||||
.dark .prose tbody tr {
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.15) !important;
|
||||
}
|
||||
|
||||
.dark .prose tbody td {
|
||||
border: 1px solid rgba(255, 255, 255, 0.15) !important;
|
||||
}
|
||||
|
||||
.dark .prose tbody tr:hover {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { client } from "@/lib/api";
|
||||
import { useBank } from "@/lib/bank-context";
|
||||
@@ -1343,7 +1344,7 @@ function DirectiveDetailPanel({
|
||||
Rule
|
||||
</div>
|
||||
<div className="prose prose-base dark:prose-invert max-w-none">
|
||||
<ReactMarkdown>{directive.content}</ReactMarkdown>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{directive.content}</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
||||
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
|
||||
import { Loader2, Zap } from "lucide-react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
|
||||
interface MentalModelDetailContentProps {
|
||||
mentalModel: MentalModel;
|
||||
@@ -73,7 +74,7 @@ export function MentalModelDetailContent({ mentalModel }: MentalModelDetailConte
|
||||
Content
|
||||
</div>
|
||||
<div className="prose prose-base dark:prose-invert max-w-none">
|
||||
<ReactMarkdown>{mentalModel.content}</ReactMarkdown>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{mentalModel.content}</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import { client } from "@/lib/api";
|
||||
import { useBank } from "@/lib/bank-context";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -302,8 +303,24 @@ export function MentalModelsView() {
|
||||
<p className="text-sm text-muted-foreground line-clamp-2 mb-3">
|
||||
{m.source_query}
|
||||
</p>
|
||||
<div className="text-sm text-foreground line-clamp-6 mb-3 border-t border-border pt-3">
|
||||
{m.content}
|
||||
<div className="text-sm text-foreground mb-3 border-t border-border pt-3">
|
||||
{/* Check if content has tables or complex markdown */}
|
||||
{m.content.includes("|") ||
|
||||
m.content.includes("```") ||
|
||||
m.content.includes("\n\n") ? (
|
||||
// Show plain text preview for complex content
|
||||
<div className="line-clamp-3 text-muted-foreground italic">
|
||||
{m.content.substring(0, 150)}...{" "}
|
||||
<span className="text-primary">Click to view full content</span>
|
||||
</div>
|
||||
) : (
|
||||
// Render simple markdown with line clamp
|
||||
<div className="line-clamp-6 prose prose-sm dark:prose-invert max-w-none">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||
{m.content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-xs border-t border-border pt-3">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -1115,7 +1132,7 @@ function MentalModelDetailPanel({
|
||||
Content
|
||||
</div>
|
||||
<div className="prose prose-base dark:prose-invert max-w-none">
|
||||
<ReactMarkdown>{mentalModel.content}</ReactMarkdown>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{mentalModel.content}</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -32,6 +32,8 @@ import JsonView from "react18-json-view";
|
||||
import "react18-json-view/src/style.css";
|
||||
import { MemoryDetailModal } from "./memory-detail-modal";
|
||||
import { MentalModelDetailModal } from "./mental-model-detail-modal";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
|
||||
type TagsMatch = "any" | "all" | "any_strict" | "all_strict";
|
||||
type ViewMode = "answer" | "trace" | "json";
|
||||
@@ -364,7 +366,9 @@ export function ThinkView() {
|
||||
<CardTitle>Answer</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-base leading-relaxed whitespace-pre-wrap">{result.text}</div>
|
||||
<div className="prose prose-sm max-w-none dark:prose-invert">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{result.text}</ReactMarkdown>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -967,9 +971,11 @@ export function ThinkView() {
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-muted-foreground">Text</h3>
|
||||
<p className="mt-1 font-medium">
|
||||
{fullObservation?.text || selectedObservation.text}
|
||||
</p>
|
||||
<div className="mt-1 prose prose-sm max-w-none dark:prose-invert">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||
{fullObservation?.text || selectedObservation.text}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
{fullObservation?.tags && fullObservation.tags.length > 0 && (
|
||||
<div>
|
||||
|
||||
@@ -50,6 +50,81 @@ docker run --rm -it --pull always -p 8888:8888 -p 9999:9999 \
|
||||
- **API Server**: http://localhost:8888
|
||||
- **Control Plane** (Web UI): http://localhost:9999
|
||||
|
||||
### Docker Image Variants
|
||||
|
||||
Hindsight provides two image variants with different size/capability tradeoffs:
|
||||
|
||||
| Variant | Size (AMD64) | Size (ARM64) | Use Case |
|
||||
|---------|--------------|--------------|----------|
|
||||
| **Full** (`latest`) | ~9 GB | ~3.7 GB | Includes local ML models (embeddings, reranking) |
|
||||
| **Slim** (`slim`) | ~500 MB | ~500 MB | Requires external embedding/reranking providers |
|
||||
|
||||
**Full image** (default):
|
||||
```bash
|
||||
docker run --rm -it -p 8888:8888 \
|
||||
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
|
||||
ghcr.io/vectorize-io/hindsight:latest
|
||||
```
|
||||
- ✅ Works out of the box with local ML models
|
||||
- ✅ No additional services needed
|
||||
- ❌ Larger image size (AMD64 includes CUDA libraries for GPU support)
|
||||
|
||||
**Slim image**:
|
||||
```bash
|
||||
docker run --rm -it -p 8888:8888 \
|
||||
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
|
||||
-e HINDSIGHT_API_EMBEDDINGS_PROVIDER=openai \
|
||||
-e HINDSIGHT_API_RERANKER_PROVIDER=cohere \
|
||||
-e HINDSIGHT_API_COHERE_API_KEY=$COHERE_API_KEY \
|
||||
ghcr.io/vectorize-io/hindsight:slim
|
||||
```
|
||||
- ✅ Dramatically smaller image (~95% reduction on AMD64)
|
||||
- ✅ Faster pull/deploy times
|
||||
- ✅ Lower memory footprint
|
||||
- ❌ Requires external embedding/reranking services (OpenAI, Cohere, TEI)
|
||||
|
||||
**When to use slim:**
|
||||
- Cloud deployments where image size matters
|
||||
- Using managed embedding services (OpenAI, Cohere)
|
||||
- Running on Text Embeddings Inference (TEI) infrastructure
|
||||
- Kubernetes environments with fast pull requirements
|
||||
|
||||
:::warning Slim Image Requires External Providers
|
||||
If you run the slim image **without** setting external embedding providers, you'll see this error:
|
||||
|
||||
```
|
||||
ImportError: sentence-transformers is required for LocalSTEmbeddings.
|
||||
Install it with: pip install sentence-transformers
|
||||
```
|
||||
|
||||
**Fix:** Always set embedding and reranking providers when using slim images:
|
||||
```bash
|
||||
-e HINDSIGHT_API_EMBEDDINGS_PROVIDER=openai
|
||||
-e HINDSIGHT_API_EMBEDDINGS_OPENAI_API_KEY=sk-xxx
|
||||
-e HINDSIGHT_API_RERANKER_PROVIDER=cohere
|
||||
-e HINDSIGHT_API_COHERE_API_KEY=xxx
|
||||
```
|
||||
:::
|
||||
|
||||
See [Configuration](./configuration#embeddings-and-reranking) for all embedding provider options.
|
||||
|
||||
### Available Tags
|
||||
|
||||
```bash
|
||||
# Standalone (API + Control Plane)
|
||||
ghcr.io/vectorize-io/hindsight:latest # Full, latest release
|
||||
ghcr.io/vectorize-io/hindsight:slim # Slim, latest release
|
||||
ghcr.io/vectorize-io/hindsight:0.4.9 # Full, specific version
|
||||
ghcr.io/vectorize-io/hindsight:0.4.9-slim # Slim, specific version
|
||||
|
||||
# API only
|
||||
ghcr.io/vectorize-io/hindsight-api:latest
|
||||
ghcr.io/vectorize-io/hindsight-api:slim
|
||||
|
||||
# Control Plane only
|
||||
ghcr.io/vectorize-io/hindsight-control-plane:latest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Helm / Kubernetes
|
||||
|
||||
@@ -91,6 +91,20 @@ export HINDSIGHT_API_RETAIN_LLM_PROVIDER=anthropic
|
||||
|
||||
Other LLM models not listed above may work with Hindsight, but they must support **at least 65,000 output tokens** to ensure reliable fact extraction. If you need support for a specific model that doesn't meet this requirement, please [open an issue](https://github.com/hindsight-ai/hindsight/issues) to request an exception.
|
||||
|
||||
:::tip Models with Limited Output Tokens
|
||||
If your model only supports 32k or fewer output tokens (e.g., some older models), you can reduce the retain completion token limit:
|
||||
|
||||
```bash
|
||||
# For models that support 32k output tokens
|
||||
export HINDSIGHT_API_RETAIN_MAX_COMPLETION_TOKENS=32000
|
||||
|
||||
# For models that support 16k output tokens
|
||||
export HINDSIGHT_API_RETAIN_MAX_COMPLETION_TOKENS=16000
|
||||
```
|
||||
|
||||
**Important:** `HINDSIGHT_API_RETAIN_MAX_COMPLETION_TOKENS` must be greater than `HINDSIGHT_API_RETAIN_CHUNK_SIZE` (default: 3000). The system will validate this on startup and provide an error message if the configuration is invalid.
|
||||
:::
|
||||
|
||||
### Configuration
|
||||
|
||||
```bash
|
||||
@@ -175,20 +189,40 @@ You can use any model supported by OpenAI Codex CLI
|
||||
- Usage is billed to your ChatGPT subscription (not separate API costs)
|
||||
- For personal development use only (see ChatGPT Terms of Service)
|
||||
|
||||
**Troubleshooting:**
|
||||
|
||||
If authentication fails:
|
||||
```bash
|
||||
# Re-login to refresh tokens
|
||||
codex auth login
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Claude Code Setup (Claude Pro/Max)
|
||||
|
||||
Use your Claude Pro or Max subscription for Hindsight without separate Anthropic API costs.
|
||||
|
||||
|
||||
:::warning Terms of Service Notice
|
||||
|
||||
This integration uses the Claude Agent SDK with your personal Claude Pro/Max subscription
|
||||
credentials. You must be logged into Claude Code on your own machine before using this provider.
|
||||
|
||||
**Please be aware:**
|
||||
|
||||
- Anthropic's [Agent SDK documentation](https://docs.claude.com/en/api/agent-sdk/overview)
|
||||
states that third-party developers should not offer claude.ai login or rate limits for
|
||||
their products. Hindsight does **not** perform any login on your behalf — it uses
|
||||
credentials you've already authenticated via `claude auth login`.
|
||||
- In January 2026, Anthropic [enforced restrictions](https://paddo.dev/blog/anthropic-walled-garden-crackdown/)
|
||||
against third-party tools using Claude subscription OAuth tokens. Those restrictions
|
||||
targeted tools that **spoofed the Claude Code client identity** — Hindsight uses the
|
||||
official Claude Agent SDK instead.
|
||||
- This provider is intended for **local, personal development use only**. Do not use it
|
||||
in production deployments or shared environments.
|
||||
- Anthropic's terms may change. If you want guaranteed compliance, use the `anthropic`
|
||||
provider with an API key instead.
|
||||
- Usage counts against your Claude Pro/Max subscription limits.
|
||||
|
||||
For production or team use, we recommend using `HINDSIGHT_API_LLM_PROVIDER=anthropic` with
|
||||
an API key from the [Anthropic Console](https://console.anthropic.com/).
|
||||
|
||||
:::
|
||||
|
||||
|
||||
**Prerequisites:**
|
||||
- Active Claude Pro or Max subscription
|
||||
- Claude Code CLI installed
|
||||
@@ -233,6 +267,7 @@ You can use any model supported by Claude Code CLI.
|
||||
- Usage billed to your Claude subscription (not separate API costs)
|
||||
- For personal development use only (see Claude Terms of Service)
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Embedding Model
|
||||
|
||||
@@ -4678,7 +4678,8 @@
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"title": "Content"
|
||||
"title": "Content",
|
||||
"description": "The mental model content as well-formatted markdown (auto-generated from reflect endpoint)"
|
||||
},
|
||||
"tags": {
|
||||
"items": {
|
||||
@@ -5397,7 +5398,8 @@
|
||||
},
|
||||
"text": {
|
||||
"type": "string",
|
||||
"title": "Text"
|
||||
"title": "Text",
|
||||
"description": "Fact text. When type='observation', this contains markdown-formatted consolidated knowledge"
|
||||
},
|
||||
"type": {
|
||||
"anyOf": [
|
||||
@@ -5657,7 +5659,8 @@
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "string",
|
||||
"title": "Text"
|
||||
"title": "Text",
|
||||
"description": "The reflect response as well-formatted markdown (headers, lists, bold/italic, code blocks, etc.)"
|
||||
},
|
||||
"based_on": {
|
||||
"anyOf": [
|
||||
@@ -5734,7 +5737,7 @@
|
||||
],
|
||||
"summary": "AI is transformative"
|
||||
},
|
||||
"text": "Based on my understanding, AI is a transformative technology...",
|
||||
"text": "## AI Overview\n\nBased on my understanding, AI is a **transformative technology**:\n\n- Used extensively in healthcare\n- Discussed in recent conversations\n- Continues to evolve rapidly",
|
||||
"trace": {
|
||||
"llm_calls": [
|
||||
{
|
||||
|
||||
@@ -50,6 +50,81 @@ docker run --rm -it --pull always -p 8888:8888 -p 9999:9999 \
|
||||
- **API Server**: http://localhost:8888
|
||||
- **Control Plane** (Web UI): http://localhost:9999
|
||||
|
||||
### Docker Image Variants
|
||||
|
||||
Hindsight provides two image variants with different size/capability tradeoffs:
|
||||
|
||||
| Variant | Size (AMD64) | Size (ARM64) | Use Case |
|
||||
|---------|--------------|--------------|----------|
|
||||
| **Full** (`latest`) | ~9 GB | ~3.7 GB | Includes local ML models (embeddings, reranking) |
|
||||
| **Slim** (`slim`) | ~500 MB | ~500 MB | Requires external embedding/reranking providers |
|
||||
|
||||
**Full image** (default):
|
||||
```bash
|
||||
docker run --rm -it -p 8888:8888 \
|
||||
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
|
||||
ghcr.io/vectorize-io/hindsight:latest
|
||||
```
|
||||
- ✅ Works out of the box with local ML models
|
||||
- ✅ No additional services needed
|
||||
- ❌ Larger image size (AMD64 includes CUDA libraries for GPU support)
|
||||
|
||||
**Slim image**:
|
||||
```bash
|
||||
docker run --rm -it -p 8888:8888 \
|
||||
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
|
||||
-e HINDSIGHT_API_EMBEDDINGS_PROVIDER=openai \
|
||||
-e HINDSIGHT_API_RERANKER_PROVIDER=cohere \
|
||||
-e HINDSIGHT_API_COHERE_API_KEY=$COHERE_API_KEY \
|
||||
ghcr.io/vectorize-io/hindsight:slim
|
||||
```
|
||||
- ✅ Dramatically smaller image (~95% reduction on AMD64)
|
||||
- ✅ Faster pull/deploy times
|
||||
- ✅ Lower memory footprint
|
||||
- ❌ Requires external embedding/reranking services (OpenAI, Cohere, TEI)
|
||||
|
||||
**When to use slim:**
|
||||
- Cloud deployments where image size matters
|
||||
- Using managed embedding services (OpenAI, Cohere)
|
||||
- Running on Text Embeddings Inference (TEI) infrastructure
|
||||
- Kubernetes environments with fast pull requirements
|
||||
|
||||
:::warning Slim Image Requires External Providers
|
||||
If you run the slim image **without** setting external embedding providers, you'll see this error:
|
||||
|
||||
```
|
||||
ImportError: sentence-transformers is required for LocalSTEmbeddings.
|
||||
Install it with: pip install sentence-transformers
|
||||
```
|
||||
|
||||
**Fix:** Always set embedding and reranking providers when using slim images:
|
||||
```bash
|
||||
-e HINDSIGHT_API_EMBEDDINGS_PROVIDER=openai
|
||||
-e HINDSIGHT_API_EMBEDDINGS_OPENAI_API_KEY=sk-xxx
|
||||
-e HINDSIGHT_API_RERANKER_PROVIDER=cohere
|
||||
-e HINDSIGHT_API_COHERE_API_KEY=xxx
|
||||
```
|
||||
:::
|
||||
|
||||
See [Configuration](./configuration#embeddings-and-reranking) for all embedding provider options.
|
||||
|
||||
### Available Tags
|
||||
|
||||
```bash
|
||||
# Standalone (API + Control Plane)
|
||||
ghcr.io/vectorize-io/hindsight:latest # Full, latest release
|
||||
ghcr.io/vectorize-io/hindsight:slim # Slim, latest release
|
||||
ghcr.io/vectorize-io/hindsight:0.4.9 # Full, specific version
|
||||
ghcr.io/vectorize-io/hindsight:0.4.9-slim # Slim, specific version
|
||||
|
||||
# API only
|
||||
ghcr.io/vectorize-io/hindsight-api:latest
|
||||
ghcr.io/vectorize-io/hindsight-api:slim
|
||||
|
||||
# Control Plane only
|
||||
ghcr.io/vectorize-io/hindsight-control-plane:latest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Helm / Kubernetes
|
||||
|
||||
@@ -91,6 +91,20 @@ export HINDSIGHT_API_RETAIN_LLM_PROVIDER=anthropic
|
||||
|
||||
Other LLM models not listed above may work with Hindsight, but they must support **at least 65,000 output tokens** to ensure reliable fact extraction. If you need support for a specific model that doesn't meet this requirement, please [open an issue](https://github.com/hindsight-ai/hindsight/issues) to request an exception.
|
||||
|
||||
:::tip Models with Limited Output Tokens
|
||||
If your model only supports 32k or fewer output tokens (e.g., some older models), you can reduce the retain completion token limit:
|
||||
|
||||
```bash
|
||||
# For models that support 32k output tokens
|
||||
export HINDSIGHT_API_RETAIN_MAX_COMPLETION_TOKENS=32000
|
||||
|
||||
# For models that support 16k output tokens
|
||||
export HINDSIGHT_API_RETAIN_MAX_COMPLETION_TOKENS=16000
|
||||
```
|
||||
|
||||
**Important:** `HINDSIGHT_API_RETAIN_MAX_COMPLETION_TOKENS` must be greater than `HINDSIGHT_API_RETAIN_CHUNK_SIZE` (default: 3000). The system will validate this on startup and provide an error message if the configuration is invalid.
|
||||
:::
|
||||
|
||||
### Configuration
|
||||
|
||||
```bash
|
||||
@@ -175,20 +189,40 @@ You can use any model supported by OpenAI Codex CLI
|
||||
- Usage is billed to your ChatGPT subscription (not separate API costs)
|
||||
- For personal development use only (see ChatGPT Terms of Service)
|
||||
|
||||
**Troubleshooting:**
|
||||
|
||||
If authentication fails:
|
||||
```bash
|
||||
# Re-login to refresh tokens
|
||||
codex auth login
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Claude Code Setup (Claude Pro/Max)
|
||||
|
||||
Use your Claude Pro or Max subscription for Hindsight without separate Anthropic API costs.
|
||||
|
||||
|
||||
:::warning Terms of Service Notice
|
||||
|
||||
This integration uses the Claude Agent SDK with your personal Claude Pro/Max subscription
|
||||
credentials. You must be logged into Claude Code on your own machine before using this provider.
|
||||
|
||||
**Please be aware:**
|
||||
|
||||
- Anthropic's [Agent SDK documentation](https://docs.claude.com/en/api/agent-sdk/overview)
|
||||
states that third-party developers should not offer claude.ai login or rate limits for
|
||||
their products. Hindsight does **not** perform any login on your behalf — it uses
|
||||
credentials you've already authenticated via `claude auth login`.
|
||||
- In January 2026, Anthropic [enforced restrictions](https://paddo.dev/blog/anthropic-walled-garden-crackdown/)
|
||||
against third-party tools using Claude subscription OAuth tokens. Those restrictions
|
||||
targeted tools that **spoofed the Claude Code client identity** — Hindsight uses the
|
||||
official Claude Agent SDK instead.
|
||||
- This provider is intended for **local, personal development use only**. Do not use it
|
||||
in production deployments or shared environments.
|
||||
- Anthropic's terms may change. If you want guaranteed compliance, use the `anthropic`
|
||||
provider with an API key instead.
|
||||
- Usage counts against your Claude Pro/Max subscription limits.
|
||||
|
||||
For production or team use, we recommend using `HINDSIGHT_API_LLM_PROVIDER=anthropic` with
|
||||
an API key from the [Anthropic Console](https://console.anthropic.com/).
|
||||
|
||||
:::
|
||||
|
||||
|
||||
**Prerequisites:**
|
||||
- Active Claude Pro or Max subscription
|
||||
- Claude Code CLI installed
|
||||
@@ -233,6 +267,7 @@ You can use any model supported by Claude Code CLI.
|
||||
- Usage billed to your Claude subscription (not separate API costs)
|
||||
- For personal development use only (see Claude Terms of Service)
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Embedding Model
|
||||
|
||||
@@ -49,8 +49,7 @@
|
||||
},
|
||||
"hindsightApiUrl": {
|
||||
"type": "string",
|
||||
"description": "External Hindsight API URL (e.g. 'https://mcp.hindsight.devcraft.team'). When set, skips local daemon and connects directly to this API.",
|
||||
"format": "uri"
|
||||
"description": "External Hindsight API URL (e.g. 'https://mcp.hindsight.devcraft.team'). When set, skips local daemon and connects directly to this API."
|
||||
},
|
||||
"hindsightApiToken": {
|
||||
"type": "string",
|
||||
|
||||
Generated
+3
-2
@@ -13,7 +13,7 @@
|
||||
},
|
||||
"hindsight-clients/typescript": {
|
||||
"name": "@vectorize-io/hindsight-client",
|
||||
"version": "0.4.4",
|
||||
"version": "0.4.9",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@hey-api/openapi-ts": "0.88.0",
|
||||
@@ -131,7 +131,7 @@
|
||||
},
|
||||
"hindsight-control-plane": {
|
||||
"name": "@vectorize-io/hindsight-control-plane",
|
||||
"version": "0.4.4",
|
||||
"version": "0.4.9",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
@@ -170,6 +170,7 @@
|
||||
"react-markdown": "^10.1.0",
|
||||
"react18-json-view": "^0.2.9",
|
||||
"recharts": "^3.5.1",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
|
||||
Reference in New Issue
Block a user