Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10bff9a1da | ||
|
|
3fa39f3385 | ||
|
|
7d95a002c7 | ||
|
|
83ca669011 | ||
|
|
e798979733 | ||
|
|
43f9a8bec2 | ||
|
|
f641b30d83 | ||
|
|
90be7c6829 | ||
|
|
6eec83b20d | ||
|
|
dd1e0986a1 | ||
|
|
69dec8ec34 | ||
|
|
888b50de12 | ||
|
|
fb7be3eced | ||
|
|
4499254f6d |
+3
-1
@@ -53,4 +53,6 @@ hindsight-clients/rust/target
|
||||
whats-next.md
|
||||
TASK.md
|
||||
# Changelog is now tracked in hindsight-docs/src/pages/changelog.md
|
||||
# CHANGELOG.md
|
||||
# CHANGELOG.md
|
||||
|
||||
blog-post*
|
||||
@@ -42,6 +42,16 @@ If you need more control over how and when your agent stores and recalls memorie
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
> 🤖 **Using a coding agent?** Install the Hindsight documentation skill for instant access to docs while you code:
|
||||
> ```bash
|
||||
> npx skills add https://github.com/vectorize-io/hindsight --skill hindsight-docs
|
||||
> ```
|
||||
> Works with Claude Code, Cursor, and other AI coding assistants.
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Quick Start
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
# Set to false when using external providers (TEI, OpenAI, Cohere)
|
||||
# PRELOAD_ML_MODELS=true/false - Pre-download ML models during build (default: true)
|
||||
# Only effective when INCLUDE_LOCAL_MODELS=true
|
||||
# NOTE: tiktoken encodings are ALWAYS preloaded (required for air-gapped deployments)
|
||||
#
|
||||
# Examples:
|
||||
# docker build -t hindsight . # Both (standalone)
|
||||
@@ -167,6 +168,28 @@ USER hindsight
|
||||
|
||||
ENV PATH="/app/api/.venv/bin:${PATH}"
|
||||
|
||||
# Pre-download tiktoken encoding (ALWAYS - required for token counting even in air-gapped envs)
|
||||
# Tiktoken is a core runtime dependency, not an optional ML model
|
||||
RUN MAX_RETRIES=3; \
|
||||
RETRY_DELAY=5; \
|
||||
for i in $(seq 1 $MAX_RETRIES); do \
|
||||
echo "Attempt $i/$MAX_RETRIES: Downloading tiktoken encoding..."; \
|
||||
/app/api/.venv/bin/python -c "\
|
||||
import tiktoken; \
|
||||
print('Downloading cl100k_base encoding...'); \
|
||||
tiktoken.get_encoding('cl100k_base'); \
|
||||
print('Tiktoken encoding cached successfully')" && break; \
|
||||
if [ $i -lt $MAX_RETRIES ]; then \
|
||||
echo "Attempt $i failed, retrying in ${RETRY_DELAY}s..."; \
|
||||
sleep $RETRY_DELAY; \
|
||||
RETRY_DELAY=$((RETRY_DELAY * 2)); \
|
||||
fi; \
|
||||
done; \
|
||||
if [ $i -eq $MAX_RETRIES ]; then \
|
||||
echo "ERROR: Failed to download tiktoken encoding after $MAX_RETRIES attempts"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
# Pre-download ML models to avoid runtime download (conditional)
|
||||
# Only runs if both PRELOAD_ML_MODELS=true AND INCLUDE_LOCAL_MODELS=true
|
||||
# Includes retry logic with exponential backoff for transient network failures
|
||||
@@ -185,7 +208,6 @@ print('Downloading embedding model...'); \
|
||||
SentenceTransformer('BAAI/bge-small-en-v1.5'); \
|
||||
print('Downloading cross-encoder model...'); \
|
||||
CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2'); \
|
||||
print('Downloading tiktoken encoding...'); import tiktoken; tiktoken.get_encoding('cl100k_base'); \
|
||||
print('Models cached successfully')" && break; \
|
||||
if [ $i -lt $MAX_RETRIES ]; then \
|
||||
echo "Attempt $i failed, retrying in ${RETRY_DELAY}s..."; \
|
||||
@@ -297,6 +319,28 @@ USER hindsight
|
||||
|
||||
ENV PATH="/app/api/.venv/bin:${PATH}"
|
||||
|
||||
# Pre-download tiktoken encoding (ALWAYS - required for token counting even in air-gapped envs)
|
||||
# Tiktoken is a core runtime dependency, not an optional ML model
|
||||
RUN MAX_RETRIES=3; \
|
||||
RETRY_DELAY=5; \
|
||||
for i in $(seq 1 $MAX_RETRIES); do \
|
||||
echo "Attempt $i/$MAX_RETRIES: Downloading tiktoken encoding..."; \
|
||||
/app/api/.venv/bin/python -c "\
|
||||
import tiktoken; \
|
||||
print('Downloading cl100k_base encoding...'); \
|
||||
tiktoken.get_encoding('cl100k_base'); \
|
||||
print('Tiktoken encoding cached successfully')" && break; \
|
||||
if [ $i -lt $MAX_RETRIES ]; then \
|
||||
echo "Attempt $i failed, retrying in ${RETRY_DELAY}s..."; \
|
||||
sleep $RETRY_DELAY; \
|
||||
RETRY_DELAY=$((RETRY_DELAY * 2)); \
|
||||
fi; \
|
||||
done; \
|
||||
if [ $i -eq $MAX_RETRIES ]; then \
|
||||
echo "ERROR: Failed to download tiktoken encoding after $MAX_RETRIES attempts"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
# Pre-download ML models to avoid runtime download (conditional)
|
||||
# Only runs if both PRELOAD_ML_MODELS=true AND INCLUDE_LOCAL_MODELS=true
|
||||
# Includes retry logic with exponential backoff for transient network failures
|
||||
@@ -315,7 +359,6 @@ print('Downloading embedding model...'); \
|
||||
SentenceTransformer('BAAI/bge-small-en-v1.5'); \
|
||||
print('Downloading cross-encoder model...'); \
|
||||
CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2'); \
|
||||
print('Downloading tiktoken encoding...'); import tiktoken; tiktoken.get_encoding('cl100k_base'); \
|
||||
print('Models cached successfully')" && break; \
|
||||
if [ $i -lt $MAX_RETRIES ]; then \
|
||||
echo "Attempt $i failed, retrying in ${RETRY_DELAY}s..."; \
|
||||
|
||||
@@ -127,6 +127,38 @@ API URL for control plane
|
||||
{{- printf "http://%s-api:%d" (include "hindsight.fullname" .) (.Values.api.service.port | int) }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
TEI reranker labels
|
||||
*/}}
|
||||
{{- define "hindsight.tei.reranker.labels" -}}
|
||||
{{ include "hindsight.labels" . }}
|
||||
app.kubernetes.io/component: tei-reranker
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
TEI reranker selector labels
|
||||
*/}}
|
||||
{{- define "hindsight.tei.reranker.selectorLabels" -}}
|
||||
{{ include "hindsight.selectorLabels" . }}
|
||||
app.kubernetes.io/component: tei-reranker
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
TEI embedding labels
|
||||
*/}}
|
||||
{{- define "hindsight.tei.embedding.labels" -}}
|
||||
{{ include "hindsight.labels" . }}
|
||||
app.kubernetes.io/component: tei-embedding
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
TEI embedding selector labels
|
||||
*/}}
|
||||
{{- define "hindsight.tei.embedding.selectorLabels" -}}
|
||||
{{ include "hindsight.selectorLabels" . }}
|
||||
app.kubernetes.io/component: tei-embedding
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Get the name of the secret to use
|
||||
*/}}
|
||||
|
||||
@@ -67,6 +67,18 @@ spec:
|
||||
- name: {{ $key }}
|
||||
value: {{ $value | quote }}
|
||||
{{- end }}
|
||||
{{- if .Values.tei.reranker.enabled }}
|
||||
- name: HINDSIGHT_API_RERANKER_PROVIDER
|
||||
value: "tei"
|
||||
- name: HINDSIGHT_API_RERANKER_TEI_URL
|
||||
value: "http://{{ include "hindsight.fullname" . }}-tei-reranker:{{ .Values.tei.reranker.port }}"
|
||||
{{- end }}
|
||||
{{- if .Values.tei.embedding.enabled }}
|
||||
- name: HINDSIGHT_API_EMBEDDINGS_PROVIDER
|
||||
value: "tei"
|
||||
- name: HINDSIGHT_API_EMBEDDINGS_TEI_URL
|
||||
value: "http://{{ include "hindsight.fullname" . }}-tei-embedding:{{ .Values.tei.embedding.port }}"
|
||||
{{- end }}
|
||||
{{- /* Only use api.secrets when not using existingSecret (for chart-managed secrets) */}}
|
||||
{{- if not .Values.existingSecret }}
|
||||
{{- range $key, $value := .Values.api.secrets }}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
{{- if .Values.tei.embedding.enabled }}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "hindsight.fullname" . }}-tei-embedding
|
||||
labels:
|
||||
{{- include "hindsight.tei.embedding.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.tei.embedding.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "hindsight.tei.embedding.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
{{- with .Values.podAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "hindsight.tei.embedding.selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
serviceAccountName: {{ include "hindsight.serviceAccountName" . }}
|
||||
{{- end }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: tei-embedding
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 10 }}
|
||||
image: "{{ .Values.tei.embedding.image.repository }}:{{ .Values.tei.embedding.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.tei.embedding.image.pullPolicy }}
|
||||
args:
|
||||
- "--model-id"
|
||||
- {{ .Values.tei.embedding.model | quote }}
|
||||
- "--hostname"
|
||||
- "0.0.0.0"
|
||||
{{- range .Values.tei.embedding.args }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.tei.embedding.port }}
|
||||
protocol: TCP
|
||||
env:
|
||||
- name: PORT
|
||||
value: {{ .Values.tei.embedding.port | quote }}
|
||||
{{- range $key, $value := .Values.tei.embedding.env }}
|
||||
- name: {{ $key }}
|
||||
value: {{ $value | quote }}
|
||||
{{- end }}
|
||||
livenessProbe:
|
||||
{{- toYaml .Values.tei.embedding.livenessProbe | nindent 10 }}
|
||||
readinessProbe:
|
||||
{{- toYaml .Values.tei.embedding.readinessProbe | nindent 10 }}
|
||||
resources:
|
||||
{{- toYaml .Values.tei.embedding.resources | nindent 10 }}
|
||||
volumeMounts:
|
||||
- name: model-cache
|
||||
mountPath: /data
|
||||
volumes:
|
||||
- name: model-cache
|
||||
emptyDir: {}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,17 @@
|
||||
{{- if .Values.tei.embedding.enabled }}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "hindsight.fullname" . }}-tei-embedding
|
||||
labels:
|
||||
{{- include "hindsight.tei.embedding.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- port: {{ .Values.tei.embedding.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
{{- include "hindsight.tei.embedding.selectorLabels" . | nindent 4 }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,76 @@
|
||||
{{- if .Values.tei.reranker.enabled }}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "hindsight.fullname" . }}-tei-reranker
|
||||
labels:
|
||||
{{- include "hindsight.tei.reranker.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.tei.reranker.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "hindsight.tei.reranker.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
{{- with .Values.podAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "hindsight.tei.reranker.selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
serviceAccountName: {{ include "hindsight.serviceAccountName" . }}
|
||||
{{- end }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: tei-reranker
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 10 }}
|
||||
image: "{{ .Values.tei.reranker.image.repository }}:{{ .Values.tei.reranker.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.tei.reranker.image.pullPolicy }}
|
||||
args:
|
||||
- "--model-id"
|
||||
- {{ .Values.tei.reranker.model | quote }}
|
||||
- "--hostname"
|
||||
- "0.0.0.0"
|
||||
{{- range .Values.tei.reranker.args }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.tei.reranker.port }}
|
||||
protocol: TCP
|
||||
env:
|
||||
- name: PORT
|
||||
value: {{ .Values.tei.reranker.port | quote }}
|
||||
{{- range $key, $value := .Values.tei.reranker.env }}
|
||||
- name: {{ $key }}
|
||||
value: {{ $value | quote }}
|
||||
{{- end }}
|
||||
livenessProbe:
|
||||
{{- toYaml .Values.tei.reranker.livenessProbe | nindent 10 }}
|
||||
readinessProbe:
|
||||
{{- toYaml .Values.tei.reranker.readinessProbe | nindent 10 }}
|
||||
resources:
|
||||
{{- toYaml .Values.tei.reranker.resources | nindent 10 }}
|
||||
volumeMounts:
|
||||
- name: model-cache
|
||||
mountPath: /data
|
||||
volumes:
|
||||
- name: model-cache
|
||||
emptyDir: {}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,17 @@
|
||||
{{- if .Values.tei.reranker.enabled }}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "hindsight.fullname" . }}-tei-reranker
|
||||
labels:
|
||||
{{- include "hindsight.tei.reranker.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- port: {{ .Values.tei.reranker.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
{{- include "hindsight.tei.reranker.selectorLabels" . | nindent 4 }}
|
||||
{{- end }}
|
||||
@@ -293,6 +293,84 @@ tolerations: []
|
||||
# Affinity (applied to all components unless overridden per-component)
|
||||
affinity: {}
|
||||
|
||||
# TEI (Text Embeddings Inference) - optional standalone deployments
|
||||
# for reranking and/or embedding models
|
||||
tei:
|
||||
reranker:
|
||||
enabled: false
|
||||
replicaCount: 1
|
||||
image:
|
||||
repository: ghcr.io/huggingface/text-embeddings-inference
|
||||
tag: cpu-1.8.3
|
||||
pullPolicy: IfNotPresent
|
||||
model: "cross-encoder/ms-marco-MiniLM-L-6-v2"
|
||||
port: 8090
|
||||
args:
|
||||
- "--auto-truncate"
|
||||
env:
|
||||
PAYLOAD_LIMIT: "10000000"
|
||||
MAX_CLIENT_BATCH_SIZE: "256"
|
||||
resources:
|
||||
limits:
|
||||
cpu: 2000m
|
||||
memory: 2Gi
|
||||
requests:
|
||||
cpu: 500m
|
||||
memory: 1Gi
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8090
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 6
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8090
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
|
||||
embedding:
|
||||
enabled: false
|
||||
replicaCount: 1
|
||||
image:
|
||||
repository: ghcr.io/huggingface/text-embeddings-inference
|
||||
tag: cpu-1.8.3
|
||||
pullPolicy: IfNotPresent
|
||||
model: "sentence-transformers/all-MiniLM-L6-v2"
|
||||
port: 8091
|
||||
args: []
|
||||
env:
|
||||
PAYLOAD_LIMIT: "10000000"
|
||||
MAX_CLIENT_BATCH_SIZE: "256"
|
||||
resources:
|
||||
limits:
|
||||
cpu: 2000m
|
||||
memory: 2Gi
|
||||
requests:
|
||||
cpu: 500m
|
||||
memory: 1Gi
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8091
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 6
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8091
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
|
||||
# Autoscaling
|
||||
autoscaling:
|
||||
enabled: false
|
||||
|
||||
@@ -6,7 +6,6 @@ Provides both HTTP REST API and MCP (Model Context Protocol) server.
|
||||
|
||||
import logging
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import FastAPI
|
||||
|
||||
@@ -46,14 +45,14 @@ def create_app(
|
||||
# Both HTTP and MCP
|
||||
app = create_app(memory, mcp_api_enabled=True)
|
||||
"""
|
||||
mcp_app = None
|
||||
mcp_servers = None
|
||||
|
||||
# Create MCP app first if enabled (we need its lifespan for chaining)
|
||||
# Create MCP servers first if enabled (we need their lifespans for chaining)
|
||||
if mcp_api_enabled:
|
||||
try:
|
||||
from .mcp import create_mcp_app
|
||||
from .mcp import MCPMiddleware, create_mcp_servers
|
||||
|
||||
mcp_app = create_mcp_app(memory=memory)
|
||||
mcp_servers = create_mcp_servers(memory=memory)
|
||||
except ImportError as e:
|
||||
logger.error(f"MCP server requested but dependencies not available: {e}")
|
||||
logger.error("Install with: pip install hindsight-api[mcp]")
|
||||
@@ -70,11 +69,9 @@ def create_app(
|
||||
app = FastAPI(title="Hindsight API", version="0.0.7")
|
||||
logger.info("HTTP REST API disabled")
|
||||
|
||||
# Mount MCP server and chain its lifespan if enabled
|
||||
if mcp_app is not None:
|
||||
# Get both MCP apps' underlying Starlette apps for lifespan access
|
||||
multi_bank_starlette_app = mcp_app.multi_bank_app
|
||||
single_bank_starlette_app = mcp_app.single_bank_app
|
||||
# Add MCP middleware and chain its lifespan if enabled
|
||||
if mcp_servers is not None:
|
||||
multi_bank_server, single_bank_server, multi_bank_starlette_app, single_bank_starlette_app = mcp_servers
|
||||
|
||||
# Store the original lifespan
|
||||
original_lifespan = app.router.lifespan_context
|
||||
@@ -94,8 +91,19 @@ def create_app(
|
||||
# Replace the app's lifespan with the chained version
|
||||
app.router.lifespan_context = chained_lifespan
|
||||
|
||||
# Mount the MCP middleware
|
||||
app.mount(mcp_mount_path, mcp_app)
|
||||
# Add MCP as a wrapping middleware — intercepts /mcp* requests directly,
|
||||
# passes everything else through to the FastAPI app. No Starlette Mount
|
||||
# means no 307 redirect for /mcp (no trailing slash).
|
||||
app.add_middleware(
|
||||
MCPMiddleware,
|
||||
memory=memory,
|
||||
prefix=mcp_mount_path,
|
||||
multi_bank_app=multi_bank_starlette_app,
|
||||
single_bank_app=single_bank_starlette_app,
|
||||
multi_bank_server=multi_bank_server,
|
||||
single_bank_server=single_bank_server,
|
||||
)
|
||||
|
||||
logger.info(f"MCP server enabled at {mcp_mount_path}/")
|
||||
|
||||
return app
|
||||
|
||||
@@ -2354,23 +2354,6 @@ def _register_routes(app: FastAPI):
|
||||
):
|
||||
"""Get a mental model by ID."""
|
||||
try:
|
||||
# Pre-operation validation hook
|
||||
validator = app.state.memory._operation_validator
|
||||
if validator:
|
||||
from hindsight_api.extensions.operation_validator import MentalModelGetContext
|
||||
|
||||
ctx = MentalModelGetContext(
|
||||
bank_id=bank_id,
|
||||
mental_model_id=mental_model_id,
|
||||
request_context=request_context,
|
||||
)
|
||||
validation = await validator.validate_mental_model_get(ctx)
|
||||
if not validation.allowed:
|
||||
raise OperationValidationError(
|
||||
validation.reason or "Operation not allowed",
|
||||
status_code=validation.status_code,
|
||||
)
|
||||
|
||||
mental_model = await app.state.memory.get_mental_model(
|
||||
bank_id=bank_id,
|
||||
mental_model_id=mental_model_id,
|
||||
@@ -2379,25 +2362,6 @@ def _register_routes(app: FastAPI):
|
||||
if mental_model is None:
|
||||
raise HTTPException(status_code=404, detail=f"Mental model '{mental_model_id}' not found")
|
||||
|
||||
# Post-operation hook
|
||||
if validator:
|
||||
from hindsight_api.extensions.operation_validator import MentalModelGetResult
|
||||
|
||||
content = mental_model.get("content", "")
|
||||
output_tokens = len(content) // 4 if content else 0
|
||||
|
||||
result_ctx = MentalModelGetResult(
|
||||
bank_id=bank_id,
|
||||
mental_model_id=mental_model_id,
|
||||
request_context=request_context,
|
||||
output_tokens=output_tokens,
|
||||
success=True,
|
||||
)
|
||||
try:
|
||||
await validator.on_mental_model_get_complete(result_ctx)
|
||||
except Exception as hook_err:
|
||||
logger.warning(f"Post-mental-model-get hook error (non-fatal): {hook_err}")
|
||||
|
||||
return MentalModelResponse(**mental_model)
|
||||
except (AuthenticationError, HTTPException):
|
||||
raise
|
||||
@@ -2427,23 +2391,6 @@ def _register_routes(app: FastAPI):
|
||||
):
|
||||
"""Create a mental model (async - returns operation_id)."""
|
||||
try:
|
||||
# Pre-operation validation hook
|
||||
validator = app.state.memory._operation_validator
|
||||
if validator:
|
||||
from hindsight_api.extensions.operation_validator import MentalModelRefreshContext
|
||||
|
||||
ctx = MentalModelRefreshContext(
|
||||
bank_id=bank_id,
|
||||
mental_model_id=None, # Not yet created
|
||||
request_context=request_context,
|
||||
)
|
||||
validation = await validator.validate_mental_model_refresh(ctx)
|
||||
if not validation.allowed:
|
||||
raise OperationValidationError(
|
||||
validation.reason or "Operation not allowed",
|
||||
status_code=validation.status_code,
|
||||
)
|
||||
|
||||
# 1. Create the mental model with placeholder content
|
||||
mental_model = await app.state.memory.create_mental_model(
|
||||
bank_id=bank_id,
|
||||
@@ -2491,23 +2438,6 @@ def _register_routes(app: FastAPI):
|
||||
):
|
||||
"""Refresh a mental model by re-running its source query (async)."""
|
||||
try:
|
||||
# Pre-operation validation hook
|
||||
validator = app.state.memory._operation_validator
|
||||
if validator:
|
||||
from hindsight_api.extensions.operation_validator import MentalModelRefreshContext
|
||||
|
||||
ctx = MentalModelRefreshContext(
|
||||
bank_id=bank_id,
|
||||
mental_model_id=mental_model_id,
|
||||
request_context=request_context,
|
||||
)
|
||||
validation = await validator.validate_mental_model_refresh(ctx)
|
||||
if not validation.allowed:
|
||||
raise OperationValidationError(
|
||||
validation.reason or "Operation not allowed",
|
||||
status_code=validation.status_code,
|
||||
)
|
||||
|
||||
result = await app.state.memory.submit_async_refresh_mental_model(
|
||||
bank_id=bank_id,
|
||||
mental_model_id=mental_model_id,
|
||||
|
||||
@@ -43,6 +43,10 @@ _current_bank_id: ContextVar[str | None] = ContextVar("current_bank_id", default
|
||||
# Context variable to hold the current API key (for tenant auth propagation)
|
||||
_current_api_key: ContextVar[str | None] = ContextVar("current_api_key", default=None)
|
||||
|
||||
# Context variables for tenant_id and api_key_id (set by authenticate, used by usage metering)
|
||||
_current_tenant_id: ContextVar[str | None] = ContextVar("current_tenant_id", default=None)
|
||||
_current_api_key_id: ContextVar[str | None] = ContextVar("current_api_key_id", default=None)
|
||||
|
||||
|
||||
def get_current_bank_id() -> str | None:
|
||||
"""Get the current bank_id from context."""
|
||||
@@ -54,6 +58,16 @@ def get_current_api_key() -> str | None:
|
||||
return _current_api_key.get()
|
||||
|
||||
|
||||
def get_current_tenant_id() -> str | None:
|
||||
"""Get the current tenant_id from context."""
|
||||
return _current_tenant_id.get()
|
||||
|
||||
|
||||
def get_current_api_key_id() -> str | None:
|
||||
"""Get the current api_key_id from context."""
|
||||
return _current_api_key_id.get()
|
||||
|
||||
|
||||
def create_mcp_server(memory: MemoryEngine, multi_bank: bool = True) -> FastMCP:
|
||||
"""
|
||||
Create and configure the Hindsight MCP server.
|
||||
@@ -73,8 +87,22 @@ def create_mcp_server(memory: MemoryEngine, multi_bank: bool = True) -> FastMCP:
|
||||
config = MCPToolsConfig(
|
||||
bank_id_resolver=get_current_bank_id,
|
||||
api_key_resolver=get_current_api_key, # Propagate API key for tenant auth
|
||||
tenant_id_resolver=get_current_tenant_id, # Propagate tenant_id for usage metering
|
||||
api_key_id_resolver=get_current_api_key_id, # Propagate api_key_id for usage metering
|
||||
include_bank_id_param=multi_bank,
|
||||
tools=None if multi_bank else {"retain", "recall", "reflect"}, # Scoped tools for single-bank mode
|
||||
tools=None
|
||||
if multi_bank
|
||||
else {
|
||||
"retain",
|
||||
"recall",
|
||||
"reflect",
|
||||
"list_mental_models",
|
||||
"get_mental_model",
|
||||
"create_mental_model",
|
||||
"update_mental_model",
|
||||
"delete_mental_model",
|
||||
"refresh_mental_model",
|
||||
}, # Scoped tools for single-bank mode (excludes bank management: list_banks, create_bank)
|
||||
retain_fire_and_forget=False, # HTTP MCP supports sync/async modes
|
||||
)
|
||||
|
||||
@@ -90,7 +118,10 @@ def create_mcp_server(memory: MemoryEngine, multi_bank: bool = True) -> FastMCP:
|
||||
|
||||
|
||||
class MCPMiddleware:
|
||||
"""ASGI middleware that handles authentication and routes to appropriate MCP server.
|
||||
"""ASGI middleware that intercepts MCP requests and routes to appropriate MCP server.
|
||||
|
||||
This middleware wraps the main FastAPI app and intercepts requests matching the
|
||||
configured prefix (default: /mcp). Non-MCP requests pass through to the inner app.
|
||||
|
||||
Authentication:
|
||||
1. If HINDSIGHT_API_MCP_AUTH_TOKEN is set (legacy), validates against that token
|
||||
@@ -121,27 +152,33 @@ class MCPMiddleware:
|
||||
--header "X-Bank-Id: my-bank" --header "Authorization: Bearer <token>"
|
||||
"""
|
||||
|
||||
def __init__(self, app, memory: MemoryEngine):
|
||||
def __init__(
|
||||
self,
|
||||
app,
|
||||
memory: MemoryEngine,
|
||||
prefix: str = "/mcp",
|
||||
multi_bank_app=None,
|
||||
single_bank_app=None,
|
||||
multi_bank_server=None,
|
||||
single_bank_server=None,
|
||||
):
|
||||
self.app = app
|
||||
self.prefix = prefix
|
||||
self.memory = memory
|
||||
self.tenant_extension = memory._tenant_extension
|
||||
|
||||
# Create two server instances:
|
||||
# 1. Multi-bank server (for /mcp/ root endpoint)
|
||||
self.multi_bank_server = create_mcp_server(memory, multi_bank=True)
|
||||
self.multi_bank_app = self.multi_bank_server.http_app(path="/")
|
||||
|
||||
# 2. Single-bank server (for /mcp/{bank_id}/ endpoints)
|
||||
self.single_bank_server = create_mcp_server(memory, multi_bank=False)
|
||||
self.single_bank_app = self.single_bank_server.http_app(path="/")
|
||||
|
||||
# Backward compatibility: expose multi_bank_app as mcp_app
|
||||
self.mcp_app = self.multi_bank_app
|
||||
|
||||
# Expose the lifespan for the parent app to chain (use multi-bank as default)
|
||||
self.lifespan = (
|
||||
self.multi_bank_app.lifespan_handler if hasattr(self.multi_bank_app, "lifespan_handler") else None
|
||||
)
|
||||
if multi_bank_app and single_bank_app:
|
||||
# Pre-created servers (used when called via add_middleware from create_app)
|
||||
self.multi_bank_app = multi_bank_app
|
||||
self.single_bank_app = single_bank_app
|
||||
self.multi_bank_server = multi_bank_server
|
||||
self.single_bank_server = single_bank_server
|
||||
else:
|
||||
# Create servers internally (for direct construction / tests)
|
||||
self.multi_bank_server = create_mcp_server(memory, multi_bank=True)
|
||||
self.multi_bank_app = self.multi_bank_server.http_app(path="/")
|
||||
self.single_bank_server = create_mcp_server(memory, multi_bank=False)
|
||||
self.single_bank_app = self.single_bank_server.http_app(path="/")
|
||||
|
||||
def _get_header(self, scope: dict, name: str) -> str | None:
|
||||
"""Extract a header value from ASGI scope."""
|
||||
@@ -153,9 +190,20 @@ class MCPMiddleware:
|
||||
|
||||
async def __call__(self, scope, receive, send):
|
||||
if scope["type"] != "http":
|
||||
await self.multi_bank_app(scope, receive, send)
|
||||
await self.app(scope, receive, send)
|
||||
return
|
||||
|
||||
path = scope.get("path", "")
|
||||
|
||||
# Check if this is an MCP request (matches prefix)
|
||||
if not (path == self.prefix or path.startswith(self.prefix + "/")):
|
||||
# Not an MCP request — pass through to the inner app
|
||||
await self.app(scope, receive, send)
|
||||
return
|
||||
|
||||
# Strip prefix from path
|
||||
path = path[len(self.prefix) :] or "/"
|
||||
|
||||
# Extract auth token from header (for tenant auth propagation)
|
||||
auth_header = self._get_header(scope, "Authorization")
|
||||
auth_token: str | None = None
|
||||
@@ -165,6 +213,8 @@ class MCPMiddleware:
|
||||
|
||||
# Authenticate: check legacy MCP_AUTH_TOKEN first, then TenantExtension
|
||||
tenant_context = None
|
||||
auth_tenant_id: str | None = None
|
||||
auth_api_key_id: str | None = None
|
||||
if MCP_AUTH_TOKEN:
|
||||
# Legacy authentication mode - validate against static token
|
||||
if not auth_token:
|
||||
@@ -178,7 +228,11 @@ class MCPMiddleware:
|
||||
else:
|
||||
# Use TenantExtension.authenticate_mcp() for auth
|
||||
try:
|
||||
tenant_context = await self.tenant_extension.authenticate_mcp(RequestContext(api_key=auth_token))
|
||||
auth_context = RequestContext(api_key=auth_token)
|
||||
tenant_context = await self.tenant_extension.authenticate_mcp(auth_context)
|
||||
# Capture tenant_id and api_key_id set by authenticate() for usage metering
|
||||
auth_tenant_id = auth_context.tenant_id
|
||||
auth_api_key_id = auth_context.api_key_id
|
||||
except AuthenticationError as e:
|
||||
await self._send_error(send, 401, str(e))
|
||||
return
|
||||
@@ -188,36 +242,15 @@ class MCPMiddleware:
|
||||
_current_schema.set(tenant_context.schema_name) if tenant_context and tenant_context.schema_name else None
|
||||
)
|
||||
|
||||
path = scope.get("path", "")
|
||||
|
||||
# Strip any mount prefix (e.g., /mcp) that FastAPI might not have stripped
|
||||
root_path = scope.get("root_path", "")
|
||||
if root_path and path.startswith(root_path):
|
||||
path = path[len(root_path) :] or "/"
|
||||
|
||||
# Also handle case where mount path wasn't stripped (e.g., /mcp/...)
|
||||
if path.startswith("/mcp/"):
|
||||
path = path[4:] # Remove /mcp prefix
|
||||
elif path == "/mcp":
|
||||
path = "/"
|
||||
|
||||
# Ensure path has leading slash (needed after stripping mount path)
|
||||
if path and not path.startswith("/"):
|
||||
path = "/" + path
|
||||
|
||||
# Try to get bank_id from header first (for Claude Code compatibility)
|
||||
bank_id = self._get_header(scope, "X-Bank-Id")
|
||||
bank_id_from_path = False
|
||||
|
||||
# MCP endpoint paths that should not be treated as bank_ids
|
||||
MCP_ENDPOINTS = {"sse", "messages"}
|
||||
|
||||
# If no header, try to extract from path: /{bank_id}/...
|
||||
new_path = path
|
||||
if not bank_id and path.startswith("/") and len(path) > 1:
|
||||
parts = path[1:].split("/", 1)
|
||||
# Don't treat MCP endpoints as bank_ids
|
||||
if parts[0] and parts[0] not in MCP_ENDPOINTS:
|
||||
if parts[0]:
|
||||
# First segment looks like a bank_id
|
||||
bank_id = parts[0]
|
||||
bank_id_from_path = True
|
||||
@@ -233,19 +266,32 @@ class MCPMiddleware:
|
||||
# - Header/env bank_id → multi-bank app (bank_id param, all tools)
|
||||
target_app = self.single_bank_app if bank_id_from_path else self.multi_bank_app
|
||||
|
||||
# Set bank_id and api_key context
|
||||
# Set bank_id, api_key, tenant_id, and api_key_id context
|
||||
bank_id_token = _current_bank_id.set(bank_id)
|
||||
# Store the auth token for tenant extension to validate
|
||||
api_key_token = _current_api_key.set(auth_token) if auth_token else None
|
||||
# Store tenant_id and api_key_id from authentication for usage metering
|
||||
tenant_id_token = _current_tenant_id.set(auth_tenant_id) if auth_tenant_id else None
|
||||
api_key_id_token = _current_api_key_id.set(auth_api_key_id) if auth_api_key_id else None
|
||||
try:
|
||||
new_scope = scope.copy()
|
||||
new_scope["path"] = new_path
|
||||
# Clear root_path since we're passing directly to the app
|
||||
new_scope["root_path"] = ""
|
||||
|
||||
# Wrap send to rewrite the SSE endpoint URL to include bank_id if using path-based routing
|
||||
# Wrap send to rewrite the SSE endpoint URL to include bank_id if using path-based routing.
|
||||
# Only rewrite SSE (text/event-stream) responses to avoid corrupting tool results
|
||||
# that might contain the literal string "data: /messages".
|
||||
is_sse_response = False
|
||||
|
||||
async def send_wrapper(message):
|
||||
if message["type"] == "http.response.body" and bank_id_from_path:
|
||||
nonlocal is_sse_response
|
||||
if message["type"] == "http.response.start":
|
||||
for header_name, header_value in message.get("headers", []):
|
||||
if header_name == b"content-type" and b"text/event-stream" in header_value:
|
||||
is_sse_response = True
|
||||
break
|
||||
if message["type"] == "http.response.body" and bank_id_from_path and is_sse_response:
|
||||
body = message.get("body", b"")
|
||||
if body and b"/messages" in body:
|
||||
# Rewrite /messages to /{bank_id}/messages in SSE endpoint event
|
||||
@@ -258,6 +304,10 @@ class MCPMiddleware:
|
||||
_current_bank_id.reset(bank_id_token)
|
||||
if api_key_token is not None:
|
||||
_current_api_key.reset(api_key_token)
|
||||
if tenant_id_token is not None:
|
||||
_current_tenant_id.reset(tenant_id_token)
|
||||
if api_key_id_token is not None:
|
||||
_current_api_key_id.reset(api_key_id_token)
|
||||
if schema_token is not None:
|
||||
_current_schema.reset(schema_token)
|
||||
|
||||
@@ -279,30 +329,19 @@ class MCPMiddleware:
|
||||
)
|
||||
|
||||
|
||||
def create_mcp_app(memory: MemoryEngine):
|
||||
"""
|
||||
Create an ASGI app that handles MCP requests with dynamic tool exposure.
|
||||
def create_mcp_servers(memory: MemoryEngine):
|
||||
"""Create multi-bank and single-bank MCP servers and their Starlette apps.
|
||||
|
||||
Authentication:
|
||||
Uses the TenantExtension from the MemoryEngine (same auth as REST API).
|
||||
|
||||
Two modes based on URL structure:
|
||||
|
||||
1. Single-bank mode (recommended for agent isolation):
|
||||
- URL: /mcp/{bank_id}/
|
||||
- Tools: retain, recall, reflect (no bank_id parameter)
|
||||
- Example: claude mcp add --transport http my-agent http://localhost:8888/mcp/my-agent-bank/
|
||||
|
||||
2. Multi-bank mode (for cross-bank operations):
|
||||
- URL: /mcp/
|
||||
- Tools: retain, recall, reflect, list_banks, create_bank (all with bank_id parameter)
|
||||
- Bank ID from: X-Bank-Id header or HINDSIGHT_MCP_BANK_ID env var (default: "default")
|
||||
- Example: claude mcp add --transport http hindsight http://localhost:8888/mcp --header "X-Bank-Id: my-bank"
|
||||
|
||||
Args:
|
||||
memory: MemoryEngine instance
|
||||
Returns the servers and apps separately so lifespans can be chained before
|
||||
the middleware wraps the main app.
|
||||
|
||||
Returns:
|
||||
ASGI application
|
||||
Tuple of (multi_bank_server, single_bank_server, multi_bank_app, single_bank_app)
|
||||
"""
|
||||
return MCPMiddleware(None, memory)
|
||||
multi_bank_server = create_mcp_server(memory, multi_bank=True)
|
||||
multi_bank_app = multi_bank_server.http_app(path="/")
|
||||
|
||||
single_bank_server = create_mcp_server(memory, multi_bank=False)
|
||||
single_bank_app = single_bank_server.http_app(path="/")
|
||||
|
||||
return multi_bank_server, single_bank_server, multi_bank_app, single_bank_app
|
||||
|
||||
@@ -66,27 +66,40 @@ ENV_CONSOLIDATION_LLM_TIMEOUT = "HINDSIGHT_API_CONSOLIDATION_LLM_TIMEOUT"
|
||||
ENV_EMBEDDINGS_PROVIDER = "HINDSIGHT_API_EMBEDDINGS_PROVIDER"
|
||||
ENV_EMBEDDINGS_LOCAL_MODEL = "HINDSIGHT_API_EMBEDDINGS_LOCAL_MODEL"
|
||||
ENV_EMBEDDINGS_LOCAL_FORCE_CPU = "HINDSIGHT_API_EMBEDDINGS_LOCAL_FORCE_CPU"
|
||||
ENV_EMBEDDINGS_LOCAL_TRUST_REMOTE_CODE = "HINDSIGHT_API_EMBEDDINGS_LOCAL_TRUST_REMOTE_CODE"
|
||||
ENV_EMBEDDINGS_TEI_URL = "HINDSIGHT_API_EMBEDDINGS_TEI_URL"
|
||||
ENV_EMBEDDINGS_OPENAI_API_KEY = "HINDSIGHT_API_EMBEDDINGS_OPENAI_API_KEY"
|
||||
ENV_EMBEDDINGS_OPENAI_MODEL = "HINDSIGHT_API_EMBEDDINGS_OPENAI_MODEL"
|
||||
ENV_EMBEDDINGS_OPENAI_BASE_URL = "HINDSIGHT_API_EMBEDDINGS_OPENAI_BASE_URL"
|
||||
|
||||
ENV_COHERE_API_KEY = "HINDSIGHT_API_COHERE_API_KEY"
|
||||
# Cohere configuration (separate for embeddings and reranker)
|
||||
ENV_EMBEDDINGS_COHERE_API_KEY = "HINDSIGHT_API_EMBEDDINGS_COHERE_API_KEY"
|
||||
ENV_EMBEDDINGS_COHERE_MODEL = "HINDSIGHT_API_EMBEDDINGS_COHERE_MODEL"
|
||||
ENV_EMBEDDINGS_COHERE_BASE_URL = "HINDSIGHT_API_EMBEDDINGS_COHERE_BASE_URL"
|
||||
ENV_RERANKER_COHERE_API_KEY = "HINDSIGHT_API_RERANKER_COHERE_API_KEY"
|
||||
ENV_RERANKER_COHERE_MODEL = "HINDSIGHT_API_RERANKER_COHERE_MODEL"
|
||||
ENV_RERANKER_COHERE_BASE_URL = "HINDSIGHT_API_RERANKER_COHERE_BASE_URL"
|
||||
|
||||
# LiteLLM gateway configuration (for embeddings and reranker via LiteLLM proxy)
|
||||
# Deprecated: Legacy shared Cohere API key (for backward compatibility)
|
||||
ENV_COHERE_API_KEY = "HINDSIGHT_API_COHERE_API_KEY"
|
||||
|
||||
# LiteLLM configuration (separate for embeddings and reranker)
|
||||
ENV_EMBEDDINGS_LITELLM_API_BASE = "HINDSIGHT_API_EMBEDDINGS_LITELLM_API_BASE"
|
||||
ENV_EMBEDDINGS_LITELLM_API_KEY = "HINDSIGHT_API_EMBEDDINGS_LITELLM_API_KEY"
|
||||
ENV_EMBEDDINGS_LITELLM_MODEL = "HINDSIGHT_API_EMBEDDINGS_LITELLM_MODEL"
|
||||
ENV_RERANKER_LITELLM_API_BASE = "HINDSIGHT_API_RERANKER_LITELLM_API_BASE"
|
||||
ENV_RERANKER_LITELLM_API_KEY = "HINDSIGHT_API_RERANKER_LITELLM_API_KEY"
|
||||
ENV_RERANKER_LITELLM_MODEL = "HINDSIGHT_API_RERANKER_LITELLM_MODEL"
|
||||
|
||||
# Deprecated: Legacy shared LiteLLM config (for backward compatibility)
|
||||
ENV_LITELLM_API_BASE = "HINDSIGHT_API_LITELLM_API_BASE"
|
||||
ENV_LITELLM_API_KEY = "HINDSIGHT_API_LITELLM_API_KEY"
|
||||
ENV_EMBEDDINGS_LITELLM_MODEL = "HINDSIGHT_API_EMBEDDINGS_LITELLM_MODEL"
|
||||
ENV_RERANKER_LITELLM_MODEL = "HINDSIGHT_API_RERANKER_LITELLM_MODEL"
|
||||
|
||||
ENV_RERANKER_PROVIDER = "HINDSIGHT_API_RERANKER_PROVIDER"
|
||||
ENV_RERANKER_LOCAL_MODEL = "HINDSIGHT_API_RERANKER_LOCAL_MODEL"
|
||||
ENV_RERANKER_LOCAL_FORCE_CPU = "HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU"
|
||||
ENV_RERANKER_LOCAL_MAX_CONCURRENT = "HINDSIGHT_API_RERANKER_LOCAL_MAX_CONCURRENT"
|
||||
ENV_RERANKER_LOCAL_TRUST_REMOTE_CODE = "HINDSIGHT_API_RERANKER_LOCAL_TRUST_REMOTE_CODE"
|
||||
ENV_RERANKER_TEI_URL = "HINDSIGHT_API_RERANKER_TEI_URL"
|
||||
ENV_RERANKER_TEI_BATCH_SIZE = "HINDSIGHT_API_RERANKER_TEI_BATCH_SIZE"
|
||||
ENV_RERANKER_TEI_MAX_CONCURRENT = "HINDSIGHT_API_RERANKER_TEI_MAX_CONCURRENT"
|
||||
@@ -190,6 +203,7 @@ DEFAULT_LLM_VERTEXAI_SERVICE_ACCOUNT_KEY = None # Optional, uses ADC if not set
|
||||
DEFAULT_EMBEDDINGS_PROVIDER = "local"
|
||||
DEFAULT_EMBEDDINGS_LOCAL_MODEL = "BAAI/bge-small-en-v1.5"
|
||||
DEFAULT_EMBEDDINGS_LOCAL_FORCE_CPU = False # Force CPU mode for local embeddings (avoids MPS/XPC issues on macOS)
|
||||
DEFAULT_EMBEDDINGS_LOCAL_TRUST_REMOTE_CODE = False # Security: disabled by default, required for some models
|
||||
DEFAULT_EMBEDDINGS_OPENAI_MODEL = "text-embedding-3-small"
|
||||
DEFAULT_EMBEDDING_DIMENSION = 384
|
||||
|
||||
@@ -197,6 +211,9 @@ DEFAULT_RERANKER_PROVIDER = "local"
|
||||
DEFAULT_RERANKER_LOCAL_MODEL = "cross-encoder/ms-marco-MiniLM-L-6-v2"
|
||||
DEFAULT_RERANKER_LOCAL_FORCE_CPU = False # Force CPU mode for local reranker (avoids MPS/XPC issues on macOS)
|
||||
DEFAULT_RERANKER_LOCAL_MAX_CONCURRENT = 4 # Limit concurrent CPU-bound reranking to prevent thrashing
|
||||
DEFAULT_RERANKER_LOCAL_TRUST_REMOTE_CODE = (
|
||||
False # Security: disabled by default, required for some models like jina-reranker-v2
|
||||
)
|
||||
DEFAULT_RERANKER_TEI_BATCH_SIZE = 128
|
||||
DEFAULT_RERANKER_TEI_MAX_CONCURRENT = 8
|
||||
DEFAULT_RERANKER_MAX_CANDIDATES = 300
|
||||
@@ -393,20 +410,32 @@ class HindsightConfig:
|
||||
embeddings_provider: str
|
||||
embeddings_local_model: str
|
||||
embeddings_local_force_cpu: bool
|
||||
embeddings_local_trust_remote_code: bool
|
||||
embeddings_tei_url: str | None
|
||||
embeddings_openai_base_url: str | None
|
||||
embeddings_cohere_api_key: str | None
|
||||
embeddings_cohere_model: str
|
||||
embeddings_cohere_base_url: str | None
|
||||
embeddings_litellm_api_base: str
|
||||
embeddings_litellm_api_key: str | None
|
||||
embeddings_litellm_model: str
|
||||
|
||||
# Reranker
|
||||
reranker_provider: str
|
||||
reranker_local_model: str
|
||||
reranker_local_force_cpu: bool
|
||||
reranker_local_max_concurrent: int
|
||||
reranker_local_trust_remote_code: bool
|
||||
reranker_tei_url: str | None
|
||||
reranker_tei_batch_size: int
|
||||
reranker_tei_max_concurrent: int
|
||||
reranker_max_candidates: int
|
||||
reranker_cohere_api_key: str | None
|
||||
reranker_cohere_model: str
|
||||
reranker_cohere_base_url: str | None
|
||||
reranker_litellm_api_base: str
|
||||
reranker_litellm_api_key: str | None
|
||||
reranker_litellm_model: str
|
||||
|
||||
# Server
|
||||
host: str
|
||||
@@ -586,9 +615,21 @@ class HindsightConfig:
|
||||
ENV_EMBEDDINGS_LOCAL_FORCE_CPU, str(DEFAULT_EMBEDDINGS_LOCAL_FORCE_CPU)
|
||||
).lower()
|
||||
in ("true", "1"),
|
||||
embeddings_local_trust_remote_code=os.getenv(
|
||||
ENV_EMBEDDINGS_LOCAL_TRUST_REMOTE_CODE, str(DEFAULT_EMBEDDINGS_LOCAL_TRUST_REMOTE_CODE)
|
||||
).lower()
|
||||
in ("true", "1"),
|
||||
embeddings_tei_url=os.getenv(ENV_EMBEDDINGS_TEI_URL),
|
||||
embeddings_openai_base_url=os.getenv(ENV_EMBEDDINGS_OPENAI_BASE_URL) or None,
|
||||
# Cohere embeddings (with backward-compatible fallback to shared API key)
|
||||
embeddings_cohere_api_key=os.getenv(ENV_EMBEDDINGS_COHERE_API_KEY) or os.getenv(ENV_COHERE_API_KEY),
|
||||
embeddings_cohere_model=os.getenv(ENV_EMBEDDINGS_COHERE_MODEL, DEFAULT_EMBEDDINGS_COHERE_MODEL),
|
||||
embeddings_cohere_base_url=os.getenv(ENV_EMBEDDINGS_COHERE_BASE_URL) or None,
|
||||
# LiteLLM embeddings (with backward-compatible fallback to shared config)
|
||||
embeddings_litellm_api_base=os.getenv(ENV_EMBEDDINGS_LITELLM_API_BASE)
|
||||
or os.getenv(ENV_LITELLM_API_BASE, DEFAULT_LITELLM_API_BASE),
|
||||
embeddings_litellm_api_key=os.getenv(ENV_EMBEDDINGS_LITELLM_API_KEY) or os.getenv(ENV_LITELLM_API_KEY),
|
||||
embeddings_litellm_model=os.getenv(ENV_EMBEDDINGS_LITELLM_MODEL, DEFAULT_EMBEDDINGS_LITELLM_MODEL),
|
||||
# Reranker
|
||||
reranker_provider=os.getenv(ENV_RERANKER_PROVIDER, DEFAULT_RERANKER_PROVIDER),
|
||||
reranker_local_model=os.getenv(ENV_RERANKER_LOCAL_MODEL, DEFAULT_RERANKER_LOCAL_MODEL),
|
||||
@@ -599,13 +640,25 @@ class HindsightConfig:
|
||||
reranker_local_max_concurrent=int(
|
||||
os.getenv(ENV_RERANKER_LOCAL_MAX_CONCURRENT, str(DEFAULT_RERANKER_LOCAL_MAX_CONCURRENT))
|
||||
),
|
||||
reranker_local_trust_remote_code=os.getenv(
|
||||
ENV_RERANKER_LOCAL_TRUST_REMOTE_CODE, str(DEFAULT_RERANKER_LOCAL_TRUST_REMOTE_CODE)
|
||||
).lower()
|
||||
in ("true", "1"),
|
||||
reranker_tei_url=os.getenv(ENV_RERANKER_TEI_URL),
|
||||
reranker_tei_batch_size=int(os.getenv(ENV_RERANKER_TEI_BATCH_SIZE, str(DEFAULT_RERANKER_TEI_BATCH_SIZE))),
|
||||
reranker_tei_max_concurrent=int(
|
||||
os.getenv(ENV_RERANKER_TEI_MAX_CONCURRENT, str(DEFAULT_RERANKER_TEI_MAX_CONCURRENT))
|
||||
),
|
||||
reranker_max_candidates=int(os.getenv(ENV_RERANKER_MAX_CANDIDATES, str(DEFAULT_RERANKER_MAX_CANDIDATES))),
|
||||
# Cohere reranker (with backward-compatible fallback to shared API key)
|
||||
reranker_cohere_api_key=os.getenv(ENV_RERANKER_COHERE_API_KEY) or os.getenv(ENV_COHERE_API_KEY),
|
||||
reranker_cohere_model=os.getenv(ENV_RERANKER_COHERE_MODEL, DEFAULT_RERANKER_COHERE_MODEL),
|
||||
reranker_cohere_base_url=os.getenv(ENV_RERANKER_COHERE_BASE_URL) or None,
|
||||
# LiteLLM reranker (with backward-compatible fallback to shared config)
|
||||
reranker_litellm_api_base=os.getenv(ENV_RERANKER_LITELLM_API_BASE)
|
||||
or os.getenv(ENV_LITELLM_API_BASE, DEFAULT_LITELLM_API_BASE),
|
||||
reranker_litellm_api_key=os.getenv(ENV_RERANKER_LITELLM_API_KEY) or os.getenv(ENV_LITELLM_API_KEY),
|
||||
reranker_litellm_model=os.getenv(ENV_RERANKER_LITELLM_MODEL, DEFAULT_RERANKER_LITELLM_MODEL),
|
||||
# Server
|
||||
host=os.getenv(ENV_HOST, DEFAULT_HOST),
|
||||
port=int(os.getenv(ENV_PORT, DEFAULT_PORT)),
|
||||
|
||||
@@ -24,20 +24,18 @@ from ..config import (
|
||||
DEFAULT_RERANKER_LOCAL_FORCE_CPU,
|
||||
DEFAULT_RERANKER_LOCAL_MAX_CONCURRENT,
|
||||
DEFAULT_RERANKER_LOCAL_MODEL,
|
||||
DEFAULT_RERANKER_LOCAL_TRUST_REMOTE_CODE,
|
||||
DEFAULT_RERANKER_PROVIDER,
|
||||
DEFAULT_RERANKER_TEI_BATCH_SIZE,
|
||||
DEFAULT_RERANKER_TEI_MAX_CONCURRENT,
|
||||
ENV_COHERE_API_KEY,
|
||||
ENV_LITELLM_API_BASE,
|
||||
ENV_LITELLM_API_KEY,
|
||||
ENV_RERANKER_COHERE_BASE_URL,
|
||||
ENV_RERANKER_COHERE_API_KEY,
|
||||
ENV_RERANKER_COHERE_MODEL,
|
||||
ENV_RERANKER_FLASHRANK_CACHE_DIR,
|
||||
ENV_RERANKER_FLASHRANK_MODEL,
|
||||
ENV_RERANKER_LITELLM_MODEL,
|
||||
ENV_RERANKER_LOCAL_FORCE_CPU,
|
||||
ENV_RERANKER_LOCAL_MAX_CONCURRENT,
|
||||
ENV_RERANKER_LOCAL_MODEL,
|
||||
ENV_RERANKER_LOCAL_TRUST_REMOTE_CODE,
|
||||
ENV_RERANKER_PROVIDER,
|
||||
ENV_RERANKER_TEI_BATCH_SIZE,
|
||||
ENV_RERANKER_TEI_MAX_CONCURRENT,
|
||||
@@ -102,7 +100,13 @@ class LocalSTCrossEncoder(CrossEncoderModel):
|
||||
_executor: ThreadPoolExecutor | None = None
|
||||
_max_concurrent: int = 4 # Limit concurrent CPU-bound reranking calls
|
||||
|
||||
def __init__(self, model_name: str | None = None, max_concurrent: int = 4, force_cpu: bool = False):
|
||||
def __init__(
|
||||
self,
|
||||
model_name: str | None = None,
|
||||
max_concurrent: int = 4,
|
||||
force_cpu: bool = False,
|
||||
trust_remote_code: bool = False,
|
||||
):
|
||||
"""
|
||||
Initialize local SentenceTransformers cross-encoder.
|
||||
|
||||
@@ -113,9 +117,13 @@ class LocalSTCrossEncoder(CrossEncoderModel):
|
||||
Higher values may cause CPU thrashing under load.
|
||||
force_cpu: Force CPU mode (avoids MPS/XPC issues on macOS in daemon mode).
|
||||
Default: False
|
||||
trust_remote_code: Allow loading models with custom code (security risk).
|
||||
Required for some models like jina-reranker-v2-base-multilingual.
|
||||
Default: False (disabled for security)
|
||||
"""
|
||||
self.model_name = model_name or DEFAULT_RERANKER_LOCAL_MODEL
|
||||
self.force_cpu = force_cpu
|
||||
self.trust_remote_code = trust_remote_code
|
||||
self._model = None
|
||||
LocalSTCrossEncoder._max_concurrent = max_concurrent
|
||||
|
||||
@@ -181,6 +189,7 @@ class LocalSTCrossEncoder(CrossEncoderModel):
|
||||
self.model_name,
|
||||
device=device,
|
||||
model_kwargs={"low_cpu_mem_usage": False},
|
||||
trust_remote_code=self.trust_remote_code,
|
||||
)
|
||||
finally:
|
||||
# Restore original logging level
|
||||
@@ -847,23 +856,27 @@ def create_cross_encoder_from_env() -> CrossEncoderModel:
|
||||
model_name=config.reranker_local_model,
|
||||
max_concurrent=config.reranker_local_max_concurrent,
|
||||
force_cpu=config.reranker_local_force_cpu,
|
||||
trust_remote_code=config.reranker_local_trust_remote_code,
|
||||
)
|
||||
elif provider == "cohere":
|
||||
api_key = os.environ.get(ENV_COHERE_API_KEY)
|
||||
api_key = config.reranker_cohere_api_key
|
||||
if not api_key:
|
||||
raise ValueError(f"{ENV_COHERE_API_KEY} is required when {ENV_RERANKER_PROVIDER} is 'cohere'")
|
||||
model = os.environ.get(ENV_RERANKER_COHERE_MODEL, DEFAULT_RERANKER_COHERE_MODEL)
|
||||
base_url = os.environ.get(ENV_RERANKER_COHERE_BASE_URL) or None
|
||||
return CohereCrossEncoder(api_key=api_key, model=model, base_url=base_url)
|
||||
raise ValueError(f"{ENV_RERANKER_COHERE_API_KEY} is required when {ENV_RERANKER_PROVIDER} is 'cohere'")
|
||||
return CohereCrossEncoder(
|
||||
api_key=api_key,
|
||||
model=config.reranker_cohere_model,
|
||||
base_url=config.reranker_cohere_base_url,
|
||||
)
|
||||
elif provider == "flashrank":
|
||||
model = os.environ.get(ENV_RERANKER_FLASHRANK_MODEL, DEFAULT_RERANKER_FLASHRANK_MODEL)
|
||||
cache_dir = os.environ.get(ENV_RERANKER_FLASHRANK_CACHE_DIR, DEFAULT_RERANKER_FLASHRANK_CACHE_DIR)
|
||||
return FlashRankCrossEncoder(model_name=model, cache_dir=cache_dir)
|
||||
elif provider == "litellm":
|
||||
api_base = os.environ.get(ENV_LITELLM_API_BASE, DEFAULT_LITELLM_API_BASE)
|
||||
api_key = os.environ.get(ENV_LITELLM_API_KEY)
|
||||
model = os.environ.get(ENV_RERANKER_LITELLM_MODEL, DEFAULT_RERANKER_LITELLM_MODEL)
|
||||
return LiteLLMCrossEncoder(api_base=api_base, api_key=api_key, model=model)
|
||||
return LiteLLMCrossEncoder(
|
||||
api_base=config.reranker_litellm_api_base,
|
||||
api_key=config.reranker_litellm_api_key,
|
||||
model=config.reranker_litellm_model,
|
||||
)
|
||||
elif provider == "rrf":
|
||||
return RRFPassthroughCrossEncoder()
|
||||
else:
|
||||
|
||||
@@ -21,22 +21,19 @@ from ..config import (
|
||||
DEFAULT_EMBEDDINGS_LITELLM_MODEL,
|
||||
DEFAULT_EMBEDDINGS_LOCAL_FORCE_CPU,
|
||||
DEFAULT_EMBEDDINGS_LOCAL_MODEL,
|
||||
DEFAULT_EMBEDDINGS_LOCAL_TRUST_REMOTE_CODE,
|
||||
DEFAULT_EMBEDDINGS_OPENAI_MODEL,
|
||||
DEFAULT_EMBEDDINGS_PROVIDER,
|
||||
DEFAULT_LITELLM_API_BASE,
|
||||
ENV_COHERE_API_KEY,
|
||||
ENV_EMBEDDINGS_COHERE_BASE_URL,
|
||||
ENV_EMBEDDINGS_COHERE_MODEL,
|
||||
ENV_EMBEDDINGS_LITELLM_MODEL,
|
||||
ENV_EMBEDDINGS_COHERE_API_KEY,
|
||||
ENV_EMBEDDINGS_LOCAL_FORCE_CPU,
|
||||
ENV_EMBEDDINGS_LOCAL_MODEL,
|
||||
ENV_EMBEDDINGS_LOCAL_TRUST_REMOTE_CODE,
|
||||
ENV_EMBEDDINGS_OPENAI_API_KEY,
|
||||
ENV_EMBEDDINGS_OPENAI_BASE_URL,
|
||||
ENV_EMBEDDINGS_OPENAI_MODEL,
|
||||
ENV_EMBEDDINGS_PROVIDER,
|
||||
ENV_EMBEDDINGS_TEI_URL,
|
||||
ENV_LITELLM_API_BASE,
|
||||
ENV_LITELLM_API_KEY,
|
||||
ENV_LLM_API_KEY,
|
||||
)
|
||||
|
||||
@@ -95,7 +92,7 @@ class LocalSTEmbeddings(Embeddings):
|
||||
The embedding dimension is auto-detected from the model.
|
||||
"""
|
||||
|
||||
def __init__(self, model_name: str | None = None, force_cpu: bool = False):
|
||||
def __init__(self, model_name: str | None = None, force_cpu: bool = False, trust_remote_code: bool = False):
|
||||
"""
|
||||
Initialize local SentenceTransformers embeddings.
|
||||
|
||||
@@ -104,9 +101,13 @@ class LocalSTEmbeddings(Embeddings):
|
||||
Default: BAAI/bge-small-en-v1.5
|
||||
force_cpu: Force CPU mode (avoids MPS/XPC issues on macOS in daemon mode).
|
||||
Default: False
|
||||
trust_remote_code: Allow loading models with custom code (security risk).
|
||||
Required for some models with custom architectures.
|
||||
Default: False (disabled for security)
|
||||
"""
|
||||
self.model_name = model_name or DEFAULT_EMBEDDINGS_LOCAL_MODEL
|
||||
self.force_cpu = force_cpu
|
||||
self.trust_remote_code = trust_remote_code
|
||||
self._model = None
|
||||
self._dimension: int | None = None
|
||||
|
||||
@@ -176,6 +177,7 @@ class LocalSTEmbeddings(Embeddings):
|
||||
self.model_name,
|
||||
device=device,
|
||||
model_kwargs={"low_cpu_mem_usage": False},
|
||||
trust_remote_code=self.trust_remote_code,
|
||||
)
|
||||
finally:
|
||||
# Restore original logging level
|
||||
@@ -741,6 +743,7 @@ def create_embeddings_from_env() -> Embeddings:
|
||||
return LocalSTEmbeddings(
|
||||
model_name=config.embeddings_local_model,
|
||||
force_cpu=config.embeddings_local_force_cpu,
|
||||
trust_remote_code=config.embeddings_local_trust_remote_code,
|
||||
)
|
||||
elif provider == "openai":
|
||||
# Use dedicated embeddings API key, or fall back to LLM API key
|
||||
@@ -754,17 +757,20 @@ def create_embeddings_from_env() -> Embeddings:
|
||||
base_url = os.environ.get(ENV_EMBEDDINGS_OPENAI_BASE_URL) or None
|
||||
return OpenAIEmbeddings(api_key=api_key, model=model, base_url=base_url)
|
||||
elif provider == "cohere":
|
||||
api_key = os.environ.get(ENV_COHERE_API_KEY)
|
||||
api_key = config.embeddings_cohere_api_key
|
||||
if not api_key:
|
||||
raise ValueError(f"{ENV_COHERE_API_KEY} is required when {ENV_EMBEDDINGS_PROVIDER} is 'cohere'")
|
||||
model = os.environ.get(ENV_EMBEDDINGS_COHERE_MODEL, DEFAULT_EMBEDDINGS_COHERE_MODEL)
|
||||
base_url = os.environ.get(ENV_EMBEDDINGS_COHERE_BASE_URL) or None
|
||||
return CohereEmbeddings(api_key=api_key, model=model, base_url=base_url)
|
||||
raise ValueError(f"{ENV_EMBEDDINGS_COHERE_API_KEY} is required when {ENV_EMBEDDINGS_PROVIDER} is 'cohere'")
|
||||
return CohereEmbeddings(
|
||||
api_key=api_key,
|
||||
model=config.embeddings_cohere_model,
|
||||
base_url=config.embeddings_cohere_base_url,
|
||||
)
|
||||
elif provider == "litellm":
|
||||
api_base = os.environ.get(ENV_LITELLM_API_BASE, DEFAULT_LITELLM_API_BASE)
|
||||
api_key = os.environ.get(ENV_LITELLM_API_KEY)
|
||||
model = os.environ.get(ENV_EMBEDDINGS_LITELLM_MODEL, DEFAULT_EMBEDDINGS_LITELLM_MODEL)
|
||||
return LiteLLMEmbeddings(api_base=api_base, api_key=api_key, model=model)
|
||||
return LiteLLMEmbeddings(
|
||||
api_base=config.embeddings_litellm_api_base,
|
||||
api_key=config.embeddings_litellm_api_key,
|
||||
model=config.embeddings_litellm_model,
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unknown embeddings provider: {provider}. Supported: 'local', 'tei', 'openai', 'cohere', 'litellm'"
|
||||
|
||||
@@ -545,16 +545,19 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
f"[BATCH_RETAIN_TASK] Starting background batch retain for bank_id={bank_id}, {len(contents)} items"
|
||||
)
|
||||
|
||||
# Restore tenant_id/api_key_id from task payload so downstream operations
|
||||
# (e.g., consolidation and mental model refreshes) can attribute usage.
|
||||
# Restore tenant_id/api_key_id from task payload so extensions
|
||||
# (e.g., operation validators) can attribute the operation correctly.
|
||||
# internal=True to skip extension auth (worker has no API key),
|
||||
# user_initiated=True so extensions know this originated from a user request.
|
||||
from hindsight_api.models import RequestContext
|
||||
|
||||
internal_context = RequestContext(
|
||||
context = RequestContext(
|
||||
internal=True,
|
||||
user_initiated=True,
|
||||
tenant_id=task_dict.get("_tenant_id"),
|
||||
api_key_id=task_dict.get("_api_key_id"),
|
||||
)
|
||||
await self.retain_batch_async(bank_id=bank_id, contents=contents, request_context=internal_context)
|
||||
await self.retain_batch_async(bank_id=bank_id, contents=contents, request_context=context)
|
||||
|
||||
logger.info(f"[BATCH_RETAIN_TASK] Completed background batch retain for bank_id={bank_id}")
|
||||
|
||||
@@ -1484,6 +1487,9 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
unit_ids=result,
|
||||
success=True,
|
||||
error=None,
|
||||
llm_input_tokens=total_usage.input_tokens,
|
||||
llm_output_tokens=total_usage.output_tokens,
|
||||
llm_total_tokens=total_usage.total_tokens,
|
||||
)
|
||||
try:
|
||||
await self._operation_validator.on_retain_complete(result_ctx)
|
||||
@@ -4690,6 +4696,18 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
Pinned mental model dict or None if not found
|
||||
"""
|
||||
await self._authenticate_tenant(request_context)
|
||||
|
||||
# Pre-operation validation (credit check / usage metering)
|
||||
if self._operation_validator:
|
||||
from hindsight_api.extensions.operation_validator import MentalModelGetContext
|
||||
|
||||
ctx = MentalModelGetContext(
|
||||
bank_id=bank_id,
|
||||
mental_model_id=mental_model_id,
|
||||
request_context=request_context,
|
||||
)
|
||||
await self._validate_operation(self._operation_validator.validate_mental_model_get(ctx))
|
||||
|
||||
pool = await self._get_pool()
|
||||
|
||||
async with acquire_with_retry(pool) as conn:
|
||||
@@ -4705,7 +4723,28 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
mental_model_id,
|
||||
)
|
||||
|
||||
return self._row_to_mental_model(row) if row else None
|
||||
result = self._row_to_mental_model(row) if row else None
|
||||
|
||||
# Post-operation hook (usage recording)
|
||||
if result and self._operation_validator:
|
||||
from hindsight_api.extensions.operation_validator import MentalModelGetResult
|
||||
|
||||
content = result.get("content", "")
|
||||
output_tokens = len(content) // 4 if content else 0
|
||||
|
||||
result_ctx = MentalModelGetResult(
|
||||
bank_id=bank_id,
|
||||
mental_model_id=mental_model_id,
|
||||
request_context=request_context,
|
||||
output_tokens=output_tokens,
|
||||
success=True,
|
||||
)
|
||||
try:
|
||||
await self._operation_validator.on_mental_model_get_complete(result_ctx)
|
||||
except Exception as hook_err:
|
||||
logger.warning(f"Post-mental-model-get hook error (non-fatal): {hook_err}")
|
||||
|
||||
return result
|
||||
|
||||
async def create_mental_model(
|
||||
self,
|
||||
@@ -5696,6 +5735,17 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
"""
|
||||
await self._authenticate_tenant(request_context)
|
||||
|
||||
# Pre-operation validation (credit check)
|
||||
if self._operation_validator:
|
||||
from hindsight_api.extensions.operation_validator import MentalModelRefreshContext
|
||||
|
||||
ctx = MentalModelRefreshContext(
|
||||
bank_id=bank_id,
|
||||
mental_model_id=mental_model_id,
|
||||
request_context=request_context,
|
||||
)
|
||||
await self._validate_operation(self._operation_validator.validate_mental_model_refresh(ctx))
|
||||
|
||||
# Verify mental model exists
|
||||
mental_model = await self.get_mental_model(bank_id, mental_model_id, request_context=request_context)
|
||||
if not mental_model:
|
||||
|
||||
@@ -132,6 +132,10 @@ class RetainResult:
|
||||
unit_ids: list[list[str]] # List of unit IDs per content item
|
||||
success: bool = True
|
||||
error: str | None = None
|
||||
# Actual LLM token usage (populated by engine when available)
|
||||
llm_input_tokens: int | None = None
|
||||
llm_output_tokens: int | None = None
|
||||
llm_total_tokens: int | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -197,18 +197,30 @@ def main():
|
||||
embeddings_provider=config.embeddings_provider,
|
||||
embeddings_local_model=config.embeddings_local_model,
|
||||
embeddings_local_force_cpu=config.embeddings_local_force_cpu,
|
||||
embeddings_local_trust_remote_code=config.embeddings_local_trust_remote_code,
|
||||
embeddings_tei_url=config.embeddings_tei_url,
|
||||
embeddings_openai_base_url=config.embeddings_openai_base_url,
|
||||
embeddings_cohere_api_key=config.embeddings_cohere_api_key,
|
||||
embeddings_cohere_model=config.embeddings_cohere_model,
|
||||
embeddings_cohere_base_url=config.embeddings_cohere_base_url,
|
||||
embeddings_litellm_api_base=config.embeddings_litellm_api_base,
|
||||
embeddings_litellm_api_key=config.embeddings_litellm_api_key,
|
||||
embeddings_litellm_model=config.embeddings_litellm_model,
|
||||
reranker_provider=config.reranker_provider,
|
||||
reranker_local_model=config.reranker_local_model,
|
||||
reranker_local_force_cpu=config.reranker_local_force_cpu,
|
||||
reranker_local_max_concurrent=config.reranker_local_max_concurrent,
|
||||
reranker_local_trust_remote_code=config.reranker_local_trust_remote_code,
|
||||
reranker_tei_url=config.reranker_tei_url,
|
||||
reranker_tei_batch_size=config.reranker_tei_batch_size,
|
||||
reranker_tei_max_concurrent=config.reranker_tei_max_concurrent,
|
||||
reranker_max_candidates=config.reranker_max_candidates,
|
||||
reranker_cohere_api_key=config.reranker_cohere_api_key,
|
||||
reranker_cohere_model=config.reranker_cohere_model,
|
||||
reranker_cohere_base_url=config.reranker_cohere_base_url,
|
||||
reranker_litellm_api_base=config.reranker_litellm_api_base,
|
||||
reranker_litellm_api_key=config.reranker_litellm_api_key,
|
||||
reranker_litellm_model=config.reranker_litellm_model,
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
log_level=args.log_level,
|
||||
|
||||
@@ -35,6 +35,12 @@ class MCPToolsConfig:
|
||||
# How to resolve API key for tenant auth (optional)
|
||||
api_key_resolver: Callable[[], str | None] | None = None
|
||||
|
||||
# How to resolve tenant_id for usage metering (set by MCP middleware after auth)
|
||||
tenant_id_resolver: Callable[[], str | None] | None = None
|
||||
|
||||
# How to resolve api_key_id for usage metering (set by MCP middleware after auth)
|
||||
api_key_id_resolver: Callable[[], str | None] | None = None
|
||||
|
||||
# Whether to include bank_id as a parameter on tools (for multi-bank support)
|
||||
include_bank_id_param: bool = False
|
||||
|
||||
@@ -50,13 +56,15 @@ class MCPToolsConfig:
|
||||
|
||||
|
||||
def _get_request_context(config: MCPToolsConfig) -> RequestContext:
|
||||
"""Create RequestContext with API key from resolver if available.
|
||||
"""Create RequestContext with auth details from resolvers.
|
||||
|
||||
This enables tenant auth to work with MCP tools by propagating
|
||||
the Bearer token from the MCP middleware to the memory engine.
|
||||
This enables tenant auth and usage metering to work with MCP tools by propagating
|
||||
the authentication results from the MCP middleware to the memory engine.
|
||||
"""
|
||||
api_key = config.api_key_resolver() if config.api_key_resolver else None
|
||||
return RequestContext(api_key=api_key)
|
||||
tenant_id = config.tenant_id_resolver() if config.tenant_id_resolver else None
|
||||
api_key_id = config.api_key_id_resolver() if config.api_key_id_resolver else None
|
||||
return RequestContext(api_key=api_key, tenant_id=tenant_id, api_key_id=api_key_id)
|
||||
|
||||
|
||||
def parse_timestamp(timestamp: str) -> datetime | None:
|
||||
@@ -119,7 +127,19 @@ def register_mcp_tools(
|
||||
memory: MemoryEngine instance
|
||||
config: Tool configuration
|
||||
"""
|
||||
tools_to_register = config.tools or {"retain", "recall", "reflect", "list_banks", "create_bank"}
|
||||
tools_to_register = config.tools or {
|
||||
"retain",
|
||||
"recall",
|
||||
"reflect",
|
||||
"list_banks",
|
||||
"create_bank",
|
||||
"list_mental_models",
|
||||
"get_mental_model",
|
||||
"create_mental_model",
|
||||
"update_mental_model",
|
||||
"delete_mental_model",
|
||||
"refresh_mental_model",
|
||||
}
|
||||
|
||||
if "retain" in tools_to_register:
|
||||
_register_retain(mcp, memory, config)
|
||||
@@ -136,6 +156,25 @@ def register_mcp_tools(
|
||||
if "create_bank" in tools_to_register:
|
||||
_register_create_bank(mcp, memory, config)
|
||||
|
||||
# Mental model tools
|
||||
if "list_mental_models" in tools_to_register:
|
||||
_register_list_mental_models(mcp, memory, config)
|
||||
|
||||
if "get_mental_model" in tools_to_register:
|
||||
_register_get_mental_model(mcp, memory, config)
|
||||
|
||||
if "create_mental_model" in tools_to_register:
|
||||
_register_create_mental_model(mcp, memory, config)
|
||||
|
||||
if "update_mental_model" in tools_to_register:
|
||||
_register_update_mental_model(mcp, memory, config)
|
||||
|
||||
if "delete_mental_model" in tools_to_register:
|
||||
_register_delete_mental_model(mcp, memory, config)
|
||||
|
||||
if "refresh_mental_model" in tools_to_register:
|
||||
_register_refresh_mental_model(mcp, memory, config)
|
||||
|
||||
|
||||
def _register_retain(mcp: FastMCP, memory: MemoryEngine, config: MCPToolsConfig) -> None:
|
||||
"""Register the retain tool."""
|
||||
@@ -511,3 +550,567 @@ def _register_create_bank(mcp: FastMCP, memory: MemoryEngine, config: MCPToolsCo
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating bank: {e}", exc_info=True)
|
||||
return f'{{"error": "{e}"}}'
|
||||
|
||||
|
||||
def _validate_mental_model_inputs(
|
||||
name: str | None = None, source_query: str | None = None, max_tokens: int | None = None
|
||||
) -> str | None:
|
||||
"""Validate mental model inputs, returning an error message or None if valid."""
|
||||
if name is not None and not name.strip():
|
||||
return "name cannot be empty"
|
||||
if source_query is not None and not source_query.strip():
|
||||
return "source_query cannot be empty"
|
||||
if max_tokens is not None and (max_tokens < 256 or max_tokens > 8192):
|
||||
return f"max_tokens must be between 256 and 8192, got {max_tokens}"
|
||||
return None
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# MENTAL MODEL TOOLS
|
||||
# =========================================================================
|
||||
|
||||
|
||||
def _register_list_mental_models(mcp: FastMCP, memory: MemoryEngine, config: MCPToolsConfig) -> None:
|
||||
"""Register the list_mental_models tool."""
|
||||
|
||||
if config.include_bank_id_param:
|
||||
|
||||
@mcp.tool()
|
||||
async def list_mental_models(
|
||||
tags: list[str] | None = None,
|
||||
bank_id: str | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
List mental models (pinned reflections) for a memory bank.
|
||||
|
||||
Mental models are living documents that stay current by periodically re-running
|
||||
a source query through reflect. Use them to maintain up-to-date summaries,
|
||||
preferences, or synthesized knowledge.
|
||||
|
||||
Args:
|
||||
tags: Optional tags to filter by (returns models matching any tag)
|
||||
bank_id: Optional bank to list from (defaults to session bank). Use for cross-bank operations.
|
||||
"""
|
||||
try:
|
||||
target_bank = bank_id or config.bank_id_resolver()
|
||||
if target_bank is None:
|
||||
return '{"error": "No bank_id configured", "items": []}'
|
||||
|
||||
models = await memory.list_mental_models(
|
||||
bank_id=target_bank,
|
||||
tags=tags,
|
||||
request_context=_get_request_context(config),
|
||||
)
|
||||
return json.dumps({"items": models}, indent=2, default=str)
|
||||
except Exception as e:
|
||||
logger.error(f"Error listing mental models: {e}", exc_info=True)
|
||||
return f'{{"error": "{e}", "items": []}}'
|
||||
|
||||
else:
|
||||
|
||||
@mcp.tool()
|
||||
async def list_mental_models(
|
||||
tags: list[str] | None = None,
|
||||
) -> dict:
|
||||
"""
|
||||
List mental models (pinned reflections) for this memory bank.
|
||||
|
||||
Mental models are living documents that stay current by periodically re-running
|
||||
a source query through reflect. Use them to maintain up-to-date summaries,
|
||||
preferences, or synthesized knowledge.
|
||||
|
||||
Args:
|
||||
tags: Optional tags to filter by (returns models matching any tag)
|
||||
"""
|
||||
try:
|
||||
target_bank = config.bank_id_resolver()
|
||||
if target_bank is None:
|
||||
return {"error": "No bank_id configured", "items": []}
|
||||
|
||||
models = await memory.list_mental_models(
|
||||
bank_id=target_bank,
|
||||
tags=tags,
|
||||
request_context=_get_request_context(config),
|
||||
)
|
||||
return {"items": models}
|
||||
except Exception as e:
|
||||
logger.error(f"Error listing mental models: {e}", exc_info=True)
|
||||
return {"error": str(e), "items": []}
|
||||
|
||||
|
||||
def _register_get_mental_model(mcp: FastMCP, memory: MemoryEngine, config: MCPToolsConfig) -> None:
|
||||
"""Register the get_mental_model tool."""
|
||||
|
||||
if config.include_bank_id_param:
|
||||
|
||||
@mcp.tool()
|
||||
async def get_mental_model(
|
||||
mental_model_id: str,
|
||||
bank_id: str | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
Get a specific mental model by ID.
|
||||
|
||||
Returns the full mental model including its generated content, source query,
|
||||
and metadata. Use list_mental_models first to discover available model IDs.
|
||||
|
||||
Args:
|
||||
mental_model_id: The ID of the mental model to retrieve
|
||||
bank_id: Optional bank (defaults to session bank). Use for cross-bank operations.
|
||||
"""
|
||||
try:
|
||||
target_bank = bank_id or config.bank_id_resolver()
|
||||
if target_bank is None:
|
||||
return '{"error": "No bank_id configured"}'
|
||||
|
||||
model = await memory.get_mental_model(
|
||||
bank_id=target_bank,
|
||||
mental_model_id=mental_model_id,
|
||||
request_context=_get_request_context(config),
|
||||
)
|
||||
if model is None:
|
||||
return json.dumps({"error": f"Mental model '{mental_model_id}' not found in bank '{target_bank}'"})
|
||||
return json.dumps(model, indent=2, default=str)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting mental model: {e}", exc_info=True)
|
||||
return f'{{"error": "{e}"}}'
|
||||
|
||||
else:
|
||||
|
||||
@mcp.tool()
|
||||
async def get_mental_model(
|
||||
mental_model_id: str,
|
||||
) -> dict:
|
||||
"""
|
||||
Get a specific mental model by ID.
|
||||
|
||||
Returns the full mental model including its generated content, source query,
|
||||
and metadata. Use list_mental_models first to discover available model IDs.
|
||||
|
||||
Args:
|
||||
mental_model_id: The ID of the mental model to retrieve
|
||||
"""
|
||||
try:
|
||||
target_bank = config.bank_id_resolver()
|
||||
if target_bank is None:
|
||||
return {"error": "No bank_id configured"}
|
||||
|
||||
model = await memory.get_mental_model(
|
||||
bank_id=target_bank,
|
||||
mental_model_id=mental_model_id,
|
||||
request_context=_get_request_context(config),
|
||||
)
|
||||
if model is None:
|
||||
return {"error": f"Mental model '{mental_model_id}' not found in bank '{target_bank}'"}
|
||||
return model
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting mental model: {e}", exc_info=True)
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
def _register_create_mental_model(mcp: FastMCP, memory: MemoryEngine, config: MCPToolsConfig) -> None:
|
||||
"""Register the create_mental_model tool."""
|
||||
|
||||
if config.include_bank_id_param:
|
||||
|
||||
@mcp.tool()
|
||||
async def create_mental_model(
|
||||
name: str,
|
||||
source_query: str,
|
||||
mental_model_id: str | None = None,
|
||||
tags: list[str] | None = None,
|
||||
max_tokens: int = 2048,
|
||||
bank_id: str | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
Create a new mental model (pinned reflection).
|
||||
|
||||
A mental model is a living document generated by running the source_query through
|
||||
reflect. The content is auto-generated asynchronously - use the returned operation_id
|
||||
to track progress.
|
||||
|
||||
EXAMPLES:
|
||||
- name="Coding Preferences", source_query="What coding patterns and tools does the user prefer?"
|
||||
- name="Project Goals", source_query="What are the user's current project goals and priorities?"
|
||||
- name="Communication Style", source_query="How does the user prefer to communicate?"
|
||||
|
||||
Args:
|
||||
name: Human-readable name for the mental model
|
||||
source_query: The query to run through reflect to generate content
|
||||
mental_model_id: Optional custom ID (alphanumeric lowercase with hyphens). Auto-generated if not provided.
|
||||
tags: Optional tags for scoped visibility filtering
|
||||
max_tokens: Maximum tokens for generated content (256-8192, default: 2048)
|
||||
bank_id: Optional bank (defaults to session bank). Use for cross-bank operations.
|
||||
"""
|
||||
try:
|
||||
target_bank = bank_id or config.bank_id_resolver()
|
||||
if target_bank is None:
|
||||
return '{"error": "No bank_id configured"}'
|
||||
|
||||
validation_error = _validate_mental_model_inputs(
|
||||
name=name, source_query=source_query, max_tokens=max_tokens
|
||||
)
|
||||
if validation_error:
|
||||
return json.dumps({"error": validation_error})
|
||||
|
||||
request_context = _get_request_context(config)
|
||||
|
||||
# Create with placeholder content
|
||||
model = await memory.create_mental_model(
|
||||
bank_id=target_bank,
|
||||
name=name,
|
||||
source_query=source_query,
|
||||
content="Generating content...",
|
||||
mental_model_id=mental_model_id,
|
||||
tags=tags,
|
||||
max_tokens=max_tokens,
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
# Schedule async refresh to generate actual content
|
||||
result = await memory.submit_async_refresh_mental_model(
|
||||
bank_id=target_bank,
|
||||
mental_model_id=model["id"],
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"mental_model_id": model["id"],
|
||||
"operation_id": result["operation_id"],
|
||||
"status": "created",
|
||||
"message": f"Mental model '{name}' created. Content is being generated asynchronously.",
|
||||
}
|
||||
)
|
||||
except ValueError as e:
|
||||
return json.dumps({"error": str(e)})
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating mental model: {e}", exc_info=True)
|
||||
return f'{{"error": "{e}"}}'
|
||||
|
||||
else:
|
||||
|
||||
@mcp.tool()
|
||||
async def create_mental_model(
|
||||
name: str,
|
||||
source_query: str,
|
||||
mental_model_id: str | None = None,
|
||||
tags: list[str] | None = None,
|
||||
max_tokens: int = 2048,
|
||||
) -> dict:
|
||||
"""
|
||||
Create a new mental model (pinned reflection).
|
||||
|
||||
A mental model is a living document generated by running the source_query through
|
||||
reflect. The content is auto-generated asynchronously - use the returned operation_id
|
||||
to track progress.
|
||||
|
||||
EXAMPLES:
|
||||
- name="Coding Preferences", source_query="What coding patterns and tools does the user prefer?"
|
||||
- name="Project Goals", source_query="What are the user's current project goals and priorities?"
|
||||
- name="Communication Style", source_query="How does the user prefer to communicate?"
|
||||
|
||||
Args:
|
||||
name: Human-readable name for the mental model
|
||||
source_query: The query to run through reflect to generate content
|
||||
mental_model_id: Optional custom ID (alphanumeric lowercase with hyphens). Auto-generated if not provided.
|
||||
tags: Optional tags for scoped visibility filtering
|
||||
max_tokens: Maximum tokens for generated content (256-8192, default: 2048)
|
||||
"""
|
||||
try:
|
||||
target_bank = config.bank_id_resolver()
|
||||
if target_bank is None:
|
||||
return {"error": "No bank_id configured"}
|
||||
|
||||
validation_error = _validate_mental_model_inputs(
|
||||
name=name, source_query=source_query, max_tokens=max_tokens
|
||||
)
|
||||
if validation_error:
|
||||
return {"error": validation_error}
|
||||
|
||||
request_context = _get_request_context(config)
|
||||
|
||||
model = await memory.create_mental_model(
|
||||
bank_id=target_bank,
|
||||
name=name,
|
||||
source_query=source_query,
|
||||
content="Generating content...",
|
||||
mental_model_id=mental_model_id,
|
||||
tags=tags,
|
||||
max_tokens=max_tokens,
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
result = await memory.submit_async_refresh_mental_model(
|
||||
bank_id=target_bank,
|
||||
mental_model_id=model["id"],
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
return {
|
||||
"mental_model_id": model["id"],
|
||||
"operation_id": result["operation_id"],
|
||||
"status": "created",
|
||||
"message": f"Mental model '{name}' created. Content is being generated asynchronously.",
|
||||
}
|
||||
except ValueError as e:
|
||||
return {"error": str(e)}
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating mental model: {e}", exc_info=True)
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
def _register_update_mental_model(mcp: FastMCP, memory: MemoryEngine, config: MCPToolsConfig) -> None:
|
||||
"""Register the update_mental_model tool."""
|
||||
|
||||
if config.include_bank_id_param:
|
||||
|
||||
@mcp.tool()
|
||||
async def update_mental_model(
|
||||
mental_model_id: str,
|
||||
name: str | None = None,
|
||||
source_query: str | None = None,
|
||||
max_tokens: int | None = None,
|
||||
tags: list[str] | None = None,
|
||||
bank_id: str | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
Update a mental model's metadata.
|
||||
|
||||
Changes the name, source query, or tags of an existing mental model.
|
||||
To regenerate the content, use refresh_mental_model after updating the source query.
|
||||
|
||||
Args:
|
||||
mental_model_id: The ID of the mental model to update
|
||||
name: New name (leave None to keep current)
|
||||
source_query: New source query (leave None to keep current)
|
||||
max_tokens: New max tokens for content generation (256-8192, leave None to keep current)
|
||||
tags: New tags (leave None to keep current)
|
||||
bank_id: Optional bank (defaults to session bank). Use for cross-bank operations.
|
||||
"""
|
||||
try:
|
||||
target_bank = bank_id or config.bank_id_resolver()
|
||||
if target_bank is None:
|
||||
return '{"error": "No bank_id configured"}'
|
||||
|
||||
validation_error = _validate_mental_model_inputs(
|
||||
name=name, source_query=source_query, max_tokens=max_tokens
|
||||
)
|
||||
if validation_error:
|
||||
return json.dumps({"error": validation_error})
|
||||
|
||||
model = await memory.update_mental_model(
|
||||
bank_id=target_bank,
|
||||
mental_model_id=mental_model_id,
|
||||
name=name,
|
||||
source_query=source_query,
|
||||
max_tokens=max_tokens,
|
||||
tags=tags,
|
||||
request_context=_get_request_context(config),
|
||||
)
|
||||
if model is None:
|
||||
return json.dumps({"error": f"Mental model '{mental_model_id}' not found in bank '{target_bank}'"})
|
||||
return json.dumps(model, indent=2, default=str)
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating mental model: {e}", exc_info=True)
|
||||
return f'{{"error": "{e}"}}'
|
||||
|
||||
else:
|
||||
|
||||
@mcp.tool()
|
||||
async def update_mental_model(
|
||||
mental_model_id: str,
|
||||
name: str | None = None,
|
||||
source_query: str | None = None,
|
||||
max_tokens: int | None = None,
|
||||
tags: list[str] | None = None,
|
||||
) -> dict:
|
||||
"""
|
||||
Update a mental model's metadata.
|
||||
|
||||
Changes the name, source query, or tags of an existing mental model.
|
||||
To regenerate the content, use refresh_mental_model after updating the source query.
|
||||
|
||||
Args:
|
||||
mental_model_id: The ID of the mental model to update
|
||||
name: New name (leave None to keep current)
|
||||
source_query: New source query (leave None to keep current)
|
||||
max_tokens: New max tokens for content generation (256-8192, leave None to keep current)
|
||||
tags: New tags (leave None to keep current)
|
||||
"""
|
||||
try:
|
||||
target_bank = config.bank_id_resolver()
|
||||
if target_bank is None:
|
||||
return {"error": "No bank_id configured"}
|
||||
|
||||
validation_error = _validate_mental_model_inputs(
|
||||
name=name, source_query=source_query, max_tokens=max_tokens
|
||||
)
|
||||
if validation_error:
|
||||
return {"error": validation_error}
|
||||
|
||||
model = await memory.update_mental_model(
|
||||
bank_id=target_bank,
|
||||
mental_model_id=mental_model_id,
|
||||
name=name,
|
||||
source_query=source_query,
|
||||
max_tokens=max_tokens,
|
||||
tags=tags,
|
||||
request_context=_get_request_context(config),
|
||||
)
|
||||
if model is None:
|
||||
return {"error": f"Mental model '{mental_model_id}' not found in bank '{target_bank}'"}
|
||||
return model
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating mental model: {e}", exc_info=True)
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
def _register_delete_mental_model(mcp: FastMCP, memory: MemoryEngine, config: MCPToolsConfig) -> None:
|
||||
"""Register the delete_mental_model tool."""
|
||||
|
||||
if config.include_bank_id_param:
|
||||
|
||||
@mcp.tool()
|
||||
async def delete_mental_model(
|
||||
mental_model_id: str,
|
||||
bank_id: str | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
Delete a mental model.
|
||||
|
||||
Permanently removes a mental model and its generated content.
|
||||
|
||||
Args:
|
||||
mental_model_id: The ID of the mental model to delete
|
||||
bank_id: Optional bank (defaults to session bank). Use for cross-bank operations.
|
||||
"""
|
||||
try:
|
||||
target_bank = bank_id or config.bank_id_resolver()
|
||||
if target_bank is None:
|
||||
return '{"error": "No bank_id configured"}'
|
||||
|
||||
deleted = await memory.delete_mental_model(
|
||||
bank_id=target_bank,
|
||||
mental_model_id=mental_model_id,
|
||||
request_context=_get_request_context(config),
|
||||
)
|
||||
if not deleted:
|
||||
return json.dumps({"error": f"Mental model '{mental_model_id}' not found in bank '{target_bank}'"})
|
||||
return json.dumps({"status": "deleted", "mental_model_id": mental_model_id})
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting mental model: {e}", exc_info=True)
|
||||
return f'{{"error": "{e}"}}'
|
||||
|
||||
else:
|
||||
|
||||
@mcp.tool()
|
||||
async def delete_mental_model(
|
||||
mental_model_id: str,
|
||||
) -> dict:
|
||||
"""
|
||||
Delete a mental model.
|
||||
|
||||
Permanently removes a mental model and its generated content.
|
||||
|
||||
Args:
|
||||
mental_model_id: The ID of the mental model to delete
|
||||
"""
|
||||
try:
|
||||
target_bank = config.bank_id_resolver()
|
||||
if target_bank is None:
|
||||
return {"error": "No bank_id configured"}
|
||||
|
||||
deleted = await memory.delete_mental_model(
|
||||
bank_id=target_bank,
|
||||
mental_model_id=mental_model_id,
|
||||
request_context=_get_request_context(config),
|
||||
)
|
||||
if not deleted:
|
||||
return {"error": f"Mental model '{mental_model_id}' not found in bank '{target_bank}'"}
|
||||
return {"status": "deleted", "mental_model_id": mental_model_id}
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting mental model: {e}", exc_info=True)
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
def _register_refresh_mental_model(mcp: FastMCP, memory: MemoryEngine, config: MCPToolsConfig) -> None:
|
||||
"""Register the refresh_mental_model tool."""
|
||||
|
||||
if config.include_bank_id_param:
|
||||
|
||||
@mcp.tool()
|
||||
async def refresh_mental_model(
|
||||
mental_model_id: str,
|
||||
bank_id: str | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
Refresh a mental model by re-running its source query.
|
||||
|
||||
Schedules an async task to re-run the source query through reflect and update the
|
||||
mental model's content with fresh results. Use this after adding new memories or
|
||||
when the mental model's content may be stale.
|
||||
|
||||
Args:
|
||||
mental_model_id: The ID of the mental model to refresh
|
||||
bank_id: Optional bank (defaults to session bank). Use for cross-bank operations.
|
||||
"""
|
||||
try:
|
||||
target_bank = bank_id or config.bank_id_resolver()
|
||||
if target_bank is None:
|
||||
return '{"error": "No bank_id configured"}'
|
||||
|
||||
result = await memory.submit_async_refresh_mental_model(
|
||||
bank_id=target_bank,
|
||||
mental_model_id=mental_model_id,
|
||||
request_context=_get_request_context(config),
|
||||
)
|
||||
return json.dumps(
|
||||
{
|
||||
"operation_id": result["operation_id"],
|
||||
"status": "queued",
|
||||
"message": f"Refresh queued for mental model '{mental_model_id}'.",
|
||||
}
|
||||
)
|
||||
except ValueError as e:
|
||||
return json.dumps({"error": str(e)})
|
||||
except Exception as e:
|
||||
logger.error(f"Error refreshing mental model: {e}", exc_info=True)
|
||||
return f'{{"error": "{e}"}}'
|
||||
|
||||
else:
|
||||
|
||||
@mcp.tool()
|
||||
async def refresh_mental_model(
|
||||
mental_model_id: str,
|
||||
) -> dict:
|
||||
"""
|
||||
Refresh a mental model by re-running its source query.
|
||||
|
||||
Schedules an async task to re-run the source query through reflect and update the
|
||||
mental model's content with fresh results. Use this after adding new memories or
|
||||
when the mental model's content may be stale.
|
||||
|
||||
Args:
|
||||
mental_model_id: The ID of the mental model to refresh
|
||||
"""
|
||||
try:
|
||||
target_bank = config.bank_id_resolver()
|
||||
if target_bank is None:
|
||||
return {"error": "No bank_id configured"}
|
||||
|
||||
result = await memory.submit_async_refresh_mental_model(
|
||||
bank_id=target_bank,
|
||||
mental_model_id=mental_model_id,
|
||||
request_context=_get_request_context(config),
|
||||
)
|
||||
return {
|
||||
"operation_id": result["operation_id"],
|
||||
"status": "queued",
|
||||
"message": f"Refresh queued for mental model '{mental_model_id}'.",
|
||||
}
|
||||
except ValueError as e:
|
||||
return {"error": str(e)}
|
||||
except Exception as e:
|
||||
logger.error(f"Error refreshing mental model: {e}", exc_info=True)
|
||||
return {"error": str(e)}
|
||||
|
||||
@@ -20,7 +20,8 @@ class RequestContext:
|
||||
api_key: str | None = None
|
||||
api_key_id: str | None = None # UUID of the API key used for authentication
|
||||
tenant_id: str | None = None # Tenant identifier (set by extension after auth)
|
||||
internal: bool = False # True for background/internal operations (not user-visible)
|
||||
internal: bool = False # True for background/internal operations (skips extension auth)
|
||||
user_initiated: bool = False # True for async operations that originated from a user request
|
||||
|
||||
|
||||
from pgvector.sqlalchemy import Vector
|
||||
|
||||
@@ -353,6 +353,14 @@ class TestOperationHooksParameters:
|
||||
assert post_result.error is None
|
||||
assert post_result.unit_ids == result # Should match the return value
|
||||
|
||||
# Verify actual LLM token usage is populated
|
||||
assert post_result.llm_input_tokens is not None
|
||||
assert post_result.llm_input_tokens > 0
|
||||
assert post_result.llm_output_tokens is not None
|
||||
assert post_result.llm_output_tokens > 0
|
||||
assert post_result.llm_total_tokens is not None
|
||||
assert post_result.llm_total_tokens == post_result.llm_input_tokens + post_result.llm_output_tokens
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_recall_pre_hook_receives_all_parameters(self, memory_with_tracking_validator):
|
||||
"""Pre-recall hook receives all user-provided parameters."""
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Integration test for MCP endpoint routing.
|
||||
|
||||
This test verifies that /mcp/ and /mcp/{bank_id}/ expose different tool sets.
|
||||
This test verifies that /mcp/ and /mcp/{bank_id}/ expose different tool sets,
|
||||
and that URLs with or without trailing slashes both work (no 307 redirect).
|
||||
"""
|
||||
|
||||
import httpx
|
||||
@@ -39,12 +40,18 @@ async def test_mcp_endpoint_routing_integration(memory):
|
||||
|
||||
multi_tools = {t.name for t in multi_result.tools}
|
||||
|
||||
# Multi-bank should have all tools including bank management
|
||||
# Multi-bank should have all tools including bank management and mental models
|
||||
assert "retain" in multi_tools
|
||||
assert "recall" in multi_tools
|
||||
assert "reflect" in multi_tools
|
||||
assert "list_banks" in multi_tools, "Multi-bank should expose list_banks"
|
||||
assert "create_bank" in multi_tools, "Multi-bank should expose create_bank"
|
||||
assert "list_mental_models" in multi_tools, "Multi-bank should expose list_mental_models"
|
||||
assert "create_mental_model" in multi_tools, "Multi-bank should expose create_mental_model"
|
||||
assert "get_mental_model" in multi_tools, "Multi-bank should expose get_mental_model"
|
||||
assert "update_mental_model" in multi_tools, "Multi-bank should expose update_mental_model"
|
||||
assert "delete_mental_model" in multi_tools, "Multi-bank should expose delete_mental_model"
|
||||
assert "refresh_mental_model" in multi_tools, "Multi-bank should expose refresh_mental_model"
|
||||
|
||||
# Multi-bank retain should have bank_id parameter
|
||||
retain_tool = next((t for t in multi_result.tools if t.name == "retain"), None)
|
||||
@@ -64,10 +71,12 @@ async def test_mcp_endpoint_routing_integration(memory):
|
||||
|
||||
single_tools = {t.name for t in single_result.tools}
|
||||
|
||||
# Single-bank should only have scoped tools (no bank management)
|
||||
# Single-bank should have scoped tools including mental models (no bank management)
|
||||
assert "retain" in single_tools
|
||||
assert "recall" in single_tools
|
||||
assert "reflect" in single_tools
|
||||
assert "list_mental_models" in single_tools, "Single-bank should expose list_mental_models"
|
||||
assert "create_mental_model" in single_tools, "Single-bank should expose create_mental_model"
|
||||
assert "list_banks" not in single_tools, "Single-bank should NOT expose list_banks"
|
||||
assert "create_bank" not in single_tools, "Single-bank should NOT expose create_bank"
|
||||
|
||||
@@ -76,3 +85,196 @@ async def test_mcp_endpoint_routing_integration(memory):
|
||||
assert retain_tool is not None
|
||||
single_params = set(retain_tool.inputSchema.get("properties", {}).keys())
|
||||
assert "bank_id" not in single_params, "Single-bank retain should NOT have bank_id parameter"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mcp_no_trailing_slash_works(memory):
|
||||
"""Test that /mcp (no trailing slash) discovers tools without 307 redirect.
|
||||
|
||||
Starlette's Mount class redirects /mcp to /mcp/ with a 307 Temporary Redirect.
|
||||
Many MCP clients don't follow POST redirects, causing 0 tools to be discovered.
|
||||
MCPMiddleware wraps the app directly (no Mount), so the redirect never happens.
|
||||
"""
|
||||
from hindsight_api.api import create_app
|
||||
|
||||
app = create_app(memory, mcp_api_enabled=True, initialize_memory=False)
|
||||
|
||||
async with app.router.lifespan_context(app):
|
||||
from httpx import ASGITransport
|
||||
|
||||
async with httpx.AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as http_client:
|
||||
# /mcp (no slash) should work the same as /mcp/
|
||||
async with streamable_http_client("http://test/mcp", http_client=http_client) as (
|
||||
read_stream,
|
||||
write_stream,
|
||||
_,
|
||||
):
|
||||
async with ClientSession(read_stream, write_stream) as session:
|
||||
await session.initialize()
|
||||
result = await session.list_tools()
|
||||
|
||||
tools = {t.name for t in result.tools}
|
||||
assert len(tools) >= 11, f"Expected at least 11 tools from /mcp, got {len(tools)}: {tools}"
|
||||
assert "retain" in tools
|
||||
assert "recall" in tools
|
||||
assert "list_banks" in tools
|
||||
|
||||
# /mcp/my-bank (single-bank, no slash) should also work
|
||||
async with streamable_http_client("http://test/mcp/my-bank", http_client=http_client) as (
|
||||
read_stream,
|
||||
write_stream,
|
||||
_,
|
||||
):
|
||||
async with ClientSession(read_stream, write_stream) as session:
|
||||
await session.initialize()
|
||||
result = await session.list_tools()
|
||||
|
||||
tools = {t.name for t in result.tools}
|
||||
assert "retain" in tools
|
||||
assert "list_banks" not in tools, "Single-bank /mcp/my-bank should NOT expose list_banks"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mcp_tool_execution_through_client(memory):
|
||||
"""Test that tools can be called (not just discovered) through the MCP client.
|
||||
|
||||
This verifies the full pipeline: HTTP → middleware → FastMCP → tool → engine → response.
|
||||
Previous tests only checked tool discovery (list_tools), not actual execution.
|
||||
"""
|
||||
from httpx import ASGITransport
|
||||
|
||||
from hindsight_api.api import create_app
|
||||
|
||||
app = create_app(memory, mcp_api_enabled=True, initialize_memory=False)
|
||||
|
||||
async with app.router.lifespan_context(app):
|
||||
async with httpx.AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as http_client:
|
||||
async with streamable_http_client("http://test/mcp/", http_client=http_client) as (
|
||||
read_stream,
|
||||
write_stream,
|
||||
_,
|
||||
):
|
||||
async with ClientSession(read_stream, write_stream) as session:
|
||||
await session.initialize()
|
||||
|
||||
# Execute list_banks tool
|
||||
result = await session.call_tool("list_banks", arguments={})
|
||||
assert result is not None
|
||||
assert len(result.content) > 0
|
||||
# The result text should be valid JSON with a "banks" key
|
||||
import json
|
||||
|
||||
response_text = result.content[0].text
|
||||
parsed = json.loads(response_text)
|
||||
assert "banks" in parsed
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mcp_mental_model_validation_through_client(memory):
|
||||
"""Test that input validation works through the real MCP transport.
|
||||
|
||||
Verifies that invalid inputs return error messages without crashing,
|
||||
and that the engine is never called with invalid data.
|
||||
"""
|
||||
from httpx import ASGITransport
|
||||
|
||||
from hindsight_api.api import create_app
|
||||
|
||||
app = create_app(memory, mcp_api_enabled=True, initialize_memory=False)
|
||||
|
||||
async with app.router.lifespan_context(app):
|
||||
async with httpx.AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as http_client:
|
||||
async with streamable_http_client("http://test/mcp/", http_client=http_client) as (
|
||||
read_stream,
|
||||
write_stream,
|
||||
_,
|
||||
):
|
||||
async with ClientSession(read_stream, write_stream) as session:
|
||||
await session.initialize()
|
||||
|
||||
# Test: empty name should return validation error
|
||||
import json
|
||||
|
||||
result = await session.call_tool(
|
||||
"create_mental_model",
|
||||
arguments={"name": "", "source_query": "test query"},
|
||||
)
|
||||
assert result is not None
|
||||
parsed = json.loads(result.content[0].text)
|
||||
assert "error" in parsed
|
||||
assert "name cannot be empty" in parsed["error"]
|
||||
|
||||
# Test: max_tokens out of range should return validation error
|
||||
result = await session.call_tool(
|
||||
"create_mental_model",
|
||||
arguments={"name": "Test", "source_query": "test query", "max_tokens": 0},
|
||||
)
|
||||
parsed = json.loads(result.content[0].text)
|
||||
assert "error" in parsed
|
||||
assert "max_tokens must be between 256 and 8192" in parsed["error"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mcp_bank_named_sse_routes_to_single_bank(memory):
|
||||
"""Test that a bank named 'sse' routes to single-bank mode.
|
||||
|
||||
Regression test: the old MCP_ENDPOINTS blocklist prevented banks named 'sse'
|
||||
or 'messages' from being accessed via path routing. They fell through to
|
||||
multi-bank mode instead.
|
||||
"""
|
||||
from httpx import ASGITransport
|
||||
|
||||
from hindsight_api.api import create_app
|
||||
|
||||
app = create_app(memory, mcp_api_enabled=True, initialize_memory=False)
|
||||
|
||||
async with app.router.lifespan_context(app):
|
||||
async with httpx.AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as http_client:
|
||||
async with streamable_http_client("http://test/mcp/sse/", http_client=http_client) as (
|
||||
read_stream,
|
||||
write_stream,
|
||||
_,
|
||||
):
|
||||
async with ClientSession(read_stream, write_stream) as session:
|
||||
await session.initialize()
|
||||
result = await session.list_tools()
|
||||
tools = {t.name for t in result.tools}
|
||||
|
||||
# Should be single-bank mode (no bank management tools)
|
||||
assert "retain" in tools
|
||||
assert "recall" in tools
|
||||
assert "list_banks" not in tools, "Bank 'sse' should route to single-bank mode"
|
||||
assert "create_bank" not in tools
|
||||
|
||||
# retain should NOT have bank_id parameter (single-bank mode)
|
||||
retain_tool = next(t for t in result.tools if t.name == "retain")
|
||||
params = set(retain_tool.inputSchema.get("properties", {}).keys())
|
||||
assert "bank_id" not in params
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mcp_bank_named_messages_routes_to_single_bank(memory):
|
||||
"""Test that a bank named 'messages' routes to single-bank mode.
|
||||
|
||||
Same regression test as test_mcp_bank_named_sse_routes_to_single_bank but for 'messages'.
|
||||
"""
|
||||
from httpx import ASGITransport
|
||||
|
||||
from hindsight_api.api import create_app
|
||||
|
||||
app = create_app(memory, mcp_api_enabled=True, initialize_memory=False)
|
||||
|
||||
async with app.router.lifespan_context(app):
|
||||
async with httpx.AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as http_client:
|
||||
async with streamable_http_client("http://test/mcp/messages/", http_client=http_client) as (
|
||||
read_stream,
|
||||
write_stream,
|
||||
_,
|
||||
):
|
||||
async with ClientSession(read_stream, write_stream) as session:
|
||||
await session.initialize()
|
||||
result = await session.list_tools()
|
||||
tools = {t.name for t in result.tools}
|
||||
|
||||
assert "retain" in tools
|
||||
assert "list_banks" not in tools, "Bank 'messages' should route to single-bank mode"
|
||||
|
||||
@@ -165,5 +165,5 @@ class TestMCPExtensionIntegration:
|
||||
assert "create_bank" in tools
|
||||
# Extension tool also present
|
||||
assert "test_extension_tool" in tools
|
||||
# Total: 5 core + 1 extension = 6 tools
|
||||
assert len(tools) == 6
|
||||
# At least 11 core + 1 extension = 12 tools (may grow as new tools are added)
|
||||
assert len(tools) >= 12
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"""Test MCP server routing with dynamic bank_id."""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_memory():
|
||||
@@ -17,7 +18,7 @@ def mock_memory():
|
||||
@pytest.mark.asyncio
|
||||
async def test_mcp_context_variable():
|
||||
"""Test that context variable works correctly."""
|
||||
from hindsight_api.api.mcp import get_current_bank_id, _current_bank_id
|
||||
from hindsight_api.api.mcp import _current_bank_id, get_current_bank_id
|
||||
|
||||
# Initially None
|
||||
assert get_current_bank_id() is None
|
||||
@@ -36,7 +37,7 @@ async def test_mcp_context_variable():
|
||||
@pytest.mark.asyncio
|
||||
async def test_mcp_tools_use_context_bank_id(mock_memory):
|
||||
"""Test that MCP tools use bank_id from context."""
|
||||
from hindsight_api.api.mcp import create_mcp_server, _current_bank_id
|
||||
from hindsight_api.api.mcp import _current_bank_id, create_mcp_server
|
||||
|
||||
mcp_server = create_mcp_server(mock_memory)
|
||||
|
||||
@@ -62,6 +63,7 @@ async def test_mcp_tools_use_context_bank_id(mock_memory):
|
||||
|
||||
def test_path_parsing_logic():
|
||||
"""Test the path parsing logic for bank_id extraction."""
|
||||
|
||||
def parse_path(path):
|
||||
"""Simulate the path parsing logic from MCPMiddleware."""
|
||||
if not path.startswith("/") or len(path) <= 1:
|
||||
@@ -102,7 +104,7 @@ def test_path_parsing_logic():
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_key_context_variable():
|
||||
"""Test that API key context variable works correctly."""
|
||||
from hindsight_api.api.mcp import get_current_api_key, _current_api_key
|
||||
from hindsight_api.api.mcp import _current_api_key, get_current_api_key
|
||||
|
||||
# Initially None
|
||||
assert get_current_api_key() is None
|
||||
@@ -121,7 +123,7 @@ async def test_api_key_context_variable():
|
||||
@pytest.mark.asyncio
|
||||
async def test_mcp_tools_propagate_api_key(mock_memory):
|
||||
"""Test that MCP tools propagate API key to RequestContext."""
|
||||
from hindsight_api.api.mcp import create_mcp_server, _current_bank_id, _current_api_key
|
||||
from hindsight_api.api.mcp import _current_api_key, _current_bank_id, create_mcp_server
|
||||
|
||||
mcp_server = create_mcp_server(mock_memory)
|
||||
tools = mcp_server._tool_manager._tools
|
||||
@@ -143,21 +145,99 @@ async def test_mcp_tools_propagate_api_key(mock_memory):
|
||||
_current_api_key.reset(api_key_token)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tenant_id_context_variable():
|
||||
"""Test that tenant_id and api_key_id context variables work correctly."""
|
||||
from hindsight_api.api.mcp import (
|
||||
_current_api_key_id,
|
||||
_current_tenant_id,
|
||||
get_current_api_key_id,
|
||||
get_current_tenant_id,
|
||||
)
|
||||
|
||||
# Initially None
|
||||
assert get_current_tenant_id() is None
|
||||
assert get_current_api_key_id() is None
|
||||
|
||||
# Set and verify
|
||||
tenant_token = _current_tenant_id.set("org-123")
|
||||
key_id_token = _current_api_key_id.set("key-456")
|
||||
try:
|
||||
assert get_current_tenant_id() == "org-123"
|
||||
assert get_current_api_key_id() == "key-456"
|
||||
finally:
|
||||
_current_tenant_id.reset(tenant_token)
|
||||
_current_api_key_id.reset(key_id_token)
|
||||
|
||||
# Back to None after reset
|
||||
assert get_current_tenant_id() is None
|
||||
assert get_current_api_key_id() is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mcp_tools_propagate_tenant_id_and_api_key_id(mock_memory):
|
||||
"""Test that MCP tools propagate tenant_id and api_key_id to RequestContext.
|
||||
|
||||
This is the critical test for usage metering: the UsageMeteringValidator reads
|
||||
request_context.tenant_id to identify the org for billing. Without this,
|
||||
MCP operations get tenant_id="unknown" and billing is skipped entirely.
|
||||
"""
|
||||
from hindsight_api.api.mcp import (
|
||||
_current_api_key,
|
||||
_current_api_key_id,
|
||||
_current_bank_id,
|
||||
_current_tenant_id,
|
||||
create_mcp_server,
|
||||
)
|
||||
|
||||
mcp_server = create_mcp_server(mock_memory)
|
||||
tools = mcp_server._tool_manager._tools
|
||||
|
||||
# Set all context vars (simulating what MCPMiddleware does after authenticate_mcp)
|
||||
bank_token = _current_bank_id.set("test-bank")
|
||||
api_key_token = _current_api_key.set("hsk_test_key")
|
||||
tenant_token = _current_tenant_id.set("org-billing-123")
|
||||
key_id_token = _current_api_key_id.set("key-uuid-456")
|
||||
try:
|
||||
retain_tool = tools["retain"]
|
||||
await retain_tool.fn(content="test content", context="test_context", async_processing=False)
|
||||
|
||||
# Verify the RequestContext passed to memory engine has all auth fields
|
||||
mock_memory.retain_batch_async.assert_called_once()
|
||||
request_context = mock_memory.retain_batch_async.call_args.kwargs["request_context"]
|
||||
assert request_context.api_key == "hsk_test_key"
|
||||
assert request_context.tenant_id == "org-billing-123"
|
||||
assert request_context.api_key_id == "key-uuid-456"
|
||||
finally:
|
||||
_current_bank_id.reset(bank_token)
|
||||
_current_api_key.reset(api_key_token)
|
||||
_current_tenant_id.reset(tenant_token)
|
||||
_current_api_key_id.reset(key_id_token)
|
||||
|
||||
|
||||
def test_multi_bank_mode_exposes_all_tools(mock_memory):
|
||||
"""Test that multi-bank mode exposes all tools including bank management."""
|
||||
"""Test that multi-bank mode exposes all tools including bank management and mental models."""
|
||||
from hindsight_api.api.mcp import create_mcp_server
|
||||
|
||||
# Create server in multi-bank mode (default)
|
||||
mcp_server = create_mcp_server(mock_memory, multi_bank=True)
|
||||
tools = mcp_server._tool_manager._tools
|
||||
|
||||
# Should have all tools
|
||||
# Core tools
|
||||
assert "retain" in tools
|
||||
assert "recall" in tools
|
||||
assert "reflect" in tools
|
||||
assert "list_banks" in tools
|
||||
assert "create_bank" in tools
|
||||
|
||||
# Mental model tools
|
||||
assert "list_mental_models" in tools
|
||||
assert "get_mental_model" in tools
|
||||
assert "create_mental_model" in tools
|
||||
assert "update_mental_model" in tools
|
||||
assert "delete_mental_model" in tools
|
||||
assert "refresh_mental_model" in tools
|
||||
|
||||
|
||||
def test_single_bank_mode_excludes_bank_management_tools(mock_memory):
|
||||
"""Test that single-bank mode only exposes bank-scoped tools."""
|
||||
@@ -167,11 +247,19 @@ def test_single_bank_mode_excludes_bank_management_tools(mock_memory):
|
||||
mcp_server = create_mcp_server(mock_memory, multi_bank=False)
|
||||
tools = mcp_server._tool_manager._tools
|
||||
|
||||
# Should only have bank-scoped tools
|
||||
# Should have bank-scoped tools
|
||||
assert "retain" in tools
|
||||
assert "recall" in tools
|
||||
assert "reflect" in tools
|
||||
|
||||
# Mental model tools should also be present (they're bank-scoped)
|
||||
assert "list_mental_models" in tools
|
||||
assert "get_mental_model" in tools
|
||||
assert "create_mental_model" in tools
|
||||
assert "update_mental_model" in tools
|
||||
assert "delete_mental_model" in tools
|
||||
assert "refresh_mental_model" in tools
|
||||
|
||||
# Should NOT have bank management tools
|
||||
assert "list_banks" not in tools
|
||||
assert "create_bank" not in tools
|
||||
@@ -179,46 +267,56 @@ def test_single_bank_mode_excludes_bank_management_tools(mock_memory):
|
||||
|
||||
def test_multi_bank_mode_tools_have_bank_id_param(mock_memory):
|
||||
"""Test that multi-bank mode tools include bank_id parameter."""
|
||||
from hindsight_api.api.mcp import create_mcp_server
|
||||
import inspect
|
||||
|
||||
from hindsight_api.api.mcp import create_mcp_server
|
||||
|
||||
mcp_server = create_mcp_server(mock_memory, multi_bank=True)
|
||||
tools = mcp_server._tool_manager._tools
|
||||
|
||||
# Check that tools have bank_id parameter
|
||||
retain_tool = tools["retain"]
|
||||
retain_sig = inspect.signature(retain_tool.fn)
|
||||
assert "bank_id" in retain_sig.parameters
|
||||
|
||||
recall_tool = tools["recall"]
|
||||
recall_sig = inspect.signature(recall_tool.fn)
|
||||
assert "bank_id" in recall_sig.parameters
|
||||
|
||||
reflect_tool = tools["reflect"]
|
||||
reflect_sig = inspect.signature(reflect_tool.fn)
|
||||
assert "bank_id" in reflect_sig.parameters
|
||||
# All bank-scoped tools should have bank_id parameter in multi-bank mode
|
||||
bank_scoped_tools = [
|
||||
"retain",
|
||||
"recall",
|
||||
"reflect",
|
||||
"list_mental_models",
|
||||
"get_mental_model",
|
||||
"create_mental_model",
|
||||
"update_mental_model",
|
||||
"delete_mental_model",
|
||||
"refresh_mental_model",
|
||||
]
|
||||
for tool_name in bank_scoped_tools:
|
||||
tool = tools[tool_name]
|
||||
sig = inspect.signature(tool.fn)
|
||||
assert "bank_id" in sig.parameters, f"{tool_name} should have bank_id param in multi-bank mode"
|
||||
|
||||
|
||||
def test_single_bank_mode_tools_no_bank_id_param(mock_memory):
|
||||
"""Test that single-bank mode tools do NOT include bank_id parameter."""
|
||||
from hindsight_api.api.mcp import create_mcp_server
|
||||
import inspect
|
||||
|
||||
from hindsight_api.api.mcp import create_mcp_server
|
||||
|
||||
mcp_server = create_mcp_server(mock_memory, multi_bank=False)
|
||||
tools = mcp_server._tool_manager._tools
|
||||
|
||||
# Check that tools do NOT have bank_id parameter
|
||||
retain_tool = tools["retain"]
|
||||
retain_sig = inspect.signature(retain_tool.fn)
|
||||
assert "bank_id" not in retain_sig.parameters
|
||||
|
||||
recall_tool = tools["recall"]
|
||||
recall_sig = inspect.signature(recall_tool.fn)
|
||||
assert "bank_id" not in recall_sig.parameters
|
||||
|
||||
reflect_tool = tools["reflect"]
|
||||
reflect_sig = inspect.signature(reflect_tool.fn)
|
||||
assert "bank_id" not in reflect_sig.parameters
|
||||
# No bank-scoped tool should have bank_id parameter in single-bank mode
|
||||
bank_scoped_tools = [
|
||||
"retain",
|
||||
"recall",
|
||||
"reflect",
|
||||
"list_mental_models",
|
||||
"get_mental_model",
|
||||
"create_mental_model",
|
||||
"update_mental_model",
|
||||
"delete_mental_model",
|
||||
"refresh_mental_model",
|
||||
]
|
||||
for tool_name in bank_scoped_tools:
|
||||
tool = tools[tool_name]
|
||||
sig = inspect.signature(tool.fn)
|
||||
assert "bank_id" not in sig.parameters, f"{tool_name} should NOT have bank_id param in single-bank mode"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -242,19 +340,26 @@ async def test_middleware_handles_both_endpoints(mock_memory):
|
||||
assert "recall" in multi_bank_tools
|
||||
assert "list_banks" in multi_bank_tools
|
||||
assert "create_bank" in multi_bank_tools
|
||||
assert "list_mental_models" in multi_bank_tools
|
||||
assert "create_mental_model" in multi_bank_tools
|
||||
|
||||
# Single-bank should only have scoped tools
|
||||
assert "retain" in single_bank_tools
|
||||
assert "recall" in single_bank_tools
|
||||
assert "list_mental_models" in single_bank_tools
|
||||
assert "create_mental_model" in single_bank_tools
|
||||
assert "list_banks" not in single_bank_tools
|
||||
assert "create_bank" not in single_bank_tools
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_routing_logic_from_url_path():
|
||||
"""Test that routing correctly selects server based on URL structure."""
|
||||
"""Test that routing correctly selects server based on URL structure.
|
||||
|
||||
Simulates the path parsing logic from MCPMiddleware.__call__ after the
|
||||
prefix has been stripped. Any first path segment is treated as a bank_id.
|
||||
"""
|
||||
from hindsight_api.api.mcp import MCPMiddleware
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
# Mock memory
|
||||
mock_memory = MagicMock()
|
||||
@@ -263,28 +368,23 @@ async def test_routing_logic_from_url_path():
|
||||
middleware = MCPMiddleware(None, mock_memory)
|
||||
|
||||
# Simulate different URL patterns and verify routing
|
||||
# Path is what remains after stripping the /mcp prefix
|
||||
test_cases = [
|
||||
# (path_after_stripping_mcp, expected_bank_id_from_path, expected_bank_id, description)
|
||||
# (path_after_prefix_strip, expected_bank_id_from_path, expected_bank_id, description)
|
||||
("/alice/messages", True, "alice", "Bank ID in path with endpoint"),
|
||||
("/my-agent-123/", True, "my-agent-123", "Bank ID in path with trailing slash"),
|
||||
("ciccio/messages", True, "ciccio", "Bank ID without leading slash (after mount strip)"),
|
||||
("bob", True, "bob", "Bank ID only, no leading slash"),
|
||||
("/messages", False, None, "MCP endpoint, no bank ID"),
|
||||
("/sse/", True, "sse", "Bank named 'sse' routes to single-bank"),
|
||||
("/messages/", True, "messages", "Bank named 'messages' routes to single-bank"),
|
||||
("/", False, None, "Root path, no bank ID"),
|
||||
]
|
||||
|
||||
for path, expected_bank_from_path, expected_bank_id, description in test_cases:
|
||||
# Simulate the path parsing logic with leading slash normalization
|
||||
if path and not path.startswith("/"):
|
||||
path = "/" + path
|
||||
|
||||
bank_id = None
|
||||
bank_id_from_path = False
|
||||
MCP_ENDPOINTS = {"sse", "messages"}
|
||||
|
||||
if path.startswith("/") and len(path) > 1:
|
||||
parts = path[1:].split("/", 1)
|
||||
if parts[0] and parts[0] not in MCP_ENDPOINTS:
|
||||
if parts[0]:
|
||||
bank_id = parts[0]
|
||||
bank_id_from_path = True
|
||||
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
"""Tests for the shared MCP tools module."""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from hindsight_api.mcp_tools import build_content_dict, parse_timestamp
|
||||
from hindsight_api.mcp_tools import (
|
||||
MCPToolsConfig,
|
||||
_validate_mental_model_inputs,
|
||||
build_content_dict,
|
||||
parse_timestamp,
|
||||
register_mcp_tools,
|
||||
)
|
||||
|
||||
|
||||
class TestParseTimestamp:
|
||||
@@ -61,3 +68,579 @@ class TestBuildContentDict:
|
||||
result, error = build_content_dict("test content", "test_context", None)
|
||||
assert error is None
|
||||
assert "event_date" not in result
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Mental Model MCP Tool Tests
|
||||
# =========================================================================
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_memory():
|
||||
"""Create a mock MemoryEngine with mental model methods."""
|
||||
memory = MagicMock()
|
||||
memory.list_mental_models = AsyncMock(
|
||||
return_value=[
|
||||
{"id": "mm-1", "name": "Coding Prefs", "source_query": "coding preferences?", "content": "Prefers Python"},
|
||||
{"id": "mm-2", "name": "Goals", "source_query": "current goals?", "content": "Ship v2"},
|
||||
]
|
||||
)
|
||||
memory.get_mental_model = AsyncMock(
|
||||
return_value={
|
||||
"id": "mm-1",
|
||||
"name": "Coding Prefs",
|
||||
"source_query": "coding preferences?",
|
||||
"content": "Prefers Python",
|
||||
}
|
||||
)
|
||||
memory.create_mental_model = AsyncMock(return_value={"id": "mm-new"})
|
||||
memory.submit_async_refresh_mental_model = AsyncMock(return_value={"operation_id": "op-123"})
|
||||
memory.update_mental_model = AsyncMock(
|
||||
return_value={
|
||||
"id": "mm-1",
|
||||
"name": "Updated Name",
|
||||
"source_query": "new query?",
|
||||
"content": "Updated",
|
||||
}
|
||||
)
|
||||
memory.delete_mental_model = AsyncMock(return_value=True)
|
||||
return memory
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mcp_server_with_mental_models(mock_memory):
|
||||
"""Create a FastMCP server with mental model tools registered (multi-bank mode)."""
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("test", stateless_http=True)
|
||||
config = MCPToolsConfig(
|
||||
bank_id_resolver=lambda: "test-bank",
|
||||
include_bank_id_param=True,
|
||||
tools={
|
||||
"list_mental_models",
|
||||
"get_mental_model",
|
||||
"create_mental_model",
|
||||
"update_mental_model",
|
||||
"delete_mental_model",
|
||||
"refresh_mental_model",
|
||||
},
|
||||
)
|
||||
register_mcp_tools(mcp, mock_memory, config)
|
||||
return mcp
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mcp_server_single_bank(mock_memory):
|
||||
"""Create a FastMCP server with mental model tools registered (single-bank mode)."""
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("test")
|
||||
config = MCPToolsConfig(
|
||||
bank_id_resolver=lambda: "fixed-bank",
|
||||
include_bank_id_param=False,
|
||||
tools={
|
||||
"list_mental_models",
|
||||
"get_mental_model",
|
||||
"create_mental_model",
|
||||
"update_mental_model",
|
||||
"delete_mental_model",
|
||||
"refresh_mental_model",
|
||||
},
|
||||
)
|
||||
register_mcp_tools(mcp, mock_memory, config)
|
||||
return mcp
|
||||
|
||||
|
||||
class TestMentalModelToolRegistration:
|
||||
"""Test that mental model tools are registered correctly."""
|
||||
|
||||
def test_tools_registered_multi_bank(self, mcp_server_with_mental_models):
|
||||
tools = mcp_server_with_mental_models._tool_manager._tools
|
||||
expected = {
|
||||
"list_mental_models",
|
||||
"get_mental_model",
|
||||
"create_mental_model",
|
||||
"update_mental_model",
|
||||
"delete_mental_model",
|
||||
"refresh_mental_model",
|
||||
}
|
||||
assert expected == set(tools.keys())
|
||||
|
||||
def test_tools_registered_single_bank(self, mcp_server_single_bank):
|
||||
tools = mcp_server_single_bank._tool_manager._tools
|
||||
expected = {
|
||||
"list_mental_models",
|
||||
"get_mental_model",
|
||||
"create_mental_model",
|
||||
"update_mental_model",
|
||||
"delete_mental_model",
|
||||
"refresh_mental_model",
|
||||
}
|
||||
assert expected == set(tools.keys())
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_mental_models_propagates_request_context(self, mock_memory):
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("test", stateless_http=True)
|
||||
config = MCPToolsConfig(
|
||||
bank_id_resolver=lambda: "test-bank",
|
||||
api_key_resolver=lambda: "test-api-key",
|
||||
include_bank_id_param=True,
|
||||
tools={"list_mental_models"},
|
||||
)
|
||||
register_mcp_tools(mcp, mock_memory, config)
|
||||
await _tools(mcp)["list_mental_models"].fn()
|
||||
request_context = mock_memory.list_mental_models.call_args.kwargs["request_context"]
|
||||
assert request_context.api_key == "test-api-key"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_mental_model_propagates_request_context(self, mock_memory):
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("test", stateless_http=True)
|
||||
config = MCPToolsConfig(
|
||||
bank_id_resolver=lambda: "test-bank",
|
||||
api_key_resolver=lambda: "test-api-key",
|
||||
include_bank_id_param=True,
|
||||
tools={"create_mental_model"},
|
||||
)
|
||||
register_mcp_tools(mcp, mock_memory, config)
|
||||
await _tools(mcp)["create_mental_model"].fn(name="Test", source_query="query")
|
||||
request_context = mock_memory.create_mental_model.call_args.kwargs["request_context"]
|
||||
assert request_context.api_key == "test-api-key"
|
||||
|
||||
def test_mental_model_tools_in_default_set(self):
|
||||
"""Mental model tools should be in the default tools set when config.tools is None."""
|
||||
from fastmcp import FastMCP
|
||||
|
||||
memory = MagicMock()
|
||||
# Mock all engine methods that tools reference
|
||||
memory.retain_batch_async = AsyncMock()
|
||||
memory.submit_async_retain = AsyncMock(return_value={"operation_id": "op"})
|
||||
memory.recall_async = AsyncMock(return_value=MagicMock(results=[]))
|
||||
memory.reflect_async = AsyncMock()
|
||||
memory.list_banks = AsyncMock(return_value=[])
|
||||
memory.get_bank_profile = AsyncMock(return_value={})
|
||||
memory.update_bank = AsyncMock()
|
||||
memory.list_mental_models = AsyncMock(return_value=[])
|
||||
memory.get_mental_model = AsyncMock()
|
||||
memory.create_mental_model = AsyncMock()
|
||||
memory.submit_async_refresh_mental_model = AsyncMock()
|
||||
memory.update_mental_model = AsyncMock()
|
||||
memory.delete_mental_model = AsyncMock()
|
||||
|
||||
mcp = FastMCP("test", stateless_http=True)
|
||||
config = MCPToolsConfig(
|
||||
bank_id_resolver=lambda: "bank",
|
||||
include_bank_id_param=True,
|
||||
tools=None, # Default - all tools
|
||||
)
|
||||
register_mcp_tools(mcp, memory, config)
|
||||
tools = mcp._tool_manager._tools
|
||||
assert "list_mental_models" in tools
|
||||
assert "create_mental_model" in tools
|
||||
assert "refresh_mental_model" in tools
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def no_bank_mcp_server(mock_memory):
|
||||
"""Create a multi-bank MCP server where bank_id_resolver returns None."""
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("test", stateless_http=True)
|
||||
config = MCPToolsConfig(
|
||||
bank_id_resolver=lambda: None,
|
||||
include_bank_id_param=True,
|
||||
tools={
|
||||
"list_mental_models",
|
||||
"get_mental_model",
|
||||
"create_mental_model",
|
||||
"update_mental_model",
|
||||
"delete_mental_model",
|
||||
"refresh_mental_model",
|
||||
},
|
||||
)
|
||||
register_mcp_tools(mcp, mock_memory, config)
|
||||
return mcp
|
||||
|
||||
|
||||
def _tools(mcp_server):
|
||||
"""Helper to get tools dict from MCP server."""
|
||||
return mcp_server._tool_manager._tools
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestListMentalModels:
|
||||
async def test_list_multi_bank(self, mcp_server_with_mental_models, mock_memory):
|
||||
result = await _tools(mcp_server_with_mental_models)["list_mental_models"].fn()
|
||||
assert '"mm-1"' in result
|
||||
assert '"mm-2"' in result
|
||||
mock_memory.list_mental_models.assert_called_once()
|
||||
assert mock_memory.list_mental_models.call_args.kwargs["bank_id"] == "test-bank"
|
||||
|
||||
async def test_list_with_bank_id_override(self, mcp_server_with_mental_models, mock_memory):
|
||||
"""Explicit bank_id should override the resolver."""
|
||||
await _tools(mcp_server_with_mental_models)["list_mental_models"].fn(bank_id="other-bank")
|
||||
assert mock_memory.list_mental_models.call_args.kwargs["bank_id"] == "other-bank"
|
||||
|
||||
async def test_list_with_tags(self, mcp_server_with_mental_models, mock_memory):
|
||||
await _tools(mcp_server_with_mental_models)["list_mental_models"].fn(tags=["work"])
|
||||
assert mock_memory.list_mental_models.call_args.kwargs["tags"] == ["work"]
|
||||
|
||||
async def test_list_single_bank(self, mcp_server_single_bank, mock_memory):
|
||||
result = await _tools(mcp_server_single_bank)["list_mental_models"].fn()
|
||||
assert isinstance(result, dict)
|
||||
assert len(result["items"]) == 2
|
||||
assert mock_memory.list_mental_models.call_args.kwargs["bank_id"] == "fixed-bank"
|
||||
|
||||
async def test_list_no_bank_returns_error(self, no_bank_mcp_server):
|
||||
result = await _tools(no_bank_mcp_server)["list_mental_models"].fn()
|
||||
assert "error" in result
|
||||
|
||||
async def test_list_engine_error_multi_bank(self, mcp_server_with_mental_models, mock_memory):
|
||||
mock_memory.list_mental_models.side_effect = RuntimeError("DB connection lost")
|
||||
result = await _tools(mcp_server_with_mental_models)["list_mental_models"].fn()
|
||||
assert "error" in result
|
||||
assert "DB connection lost" in result
|
||||
|
||||
async def test_list_engine_error_single_bank(self, mcp_server_single_bank, mock_memory):
|
||||
mock_memory.list_mental_models.side_effect = RuntimeError("DB connection lost")
|
||||
result = await _tools(mcp_server_single_bank)["list_mental_models"].fn()
|
||||
assert isinstance(result, dict)
|
||||
assert "error" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestGetMentalModel:
|
||||
async def test_get_multi_bank(self, mcp_server_with_mental_models, mock_memory):
|
||||
result = await _tools(mcp_server_with_mental_models)["get_mental_model"].fn(mental_model_id="mm-1")
|
||||
assert '"mm-1"' in result
|
||||
assert mock_memory.get_mental_model.call_args.kwargs["mental_model_id"] == "mm-1"
|
||||
|
||||
async def test_get_with_bank_id_override(self, mcp_server_with_mental_models, mock_memory):
|
||||
await _tools(mcp_server_with_mental_models)["get_mental_model"].fn(mental_model_id="mm-1", bank_id="other-bank")
|
||||
assert mock_memory.get_mental_model.call_args.kwargs["bank_id"] == "other-bank"
|
||||
|
||||
async def test_get_not_found_multi_bank(self, mcp_server_with_mental_models, mock_memory):
|
||||
mock_memory.get_mental_model.return_value = None
|
||||
result = await _tools(mcp_server_with_mental_models)["get_mental_model"].fn(mental_model_id="missing")
|
||||
assert "not found" in result
|
||||
|
||||
async def test_get_not_found_single_bank(self, mcp_server_single_bank, mock_memory):
|
||||
mock_memory.get_mental_model.return_value = None
|
||||
result = await _tools(mcp_server_single_bank)["get_mental_model"].fn(mental_model_id="missing")
|
||||
assert isinstance(result, dict)
|
||||
assert "not found" in result["error"]
|
||||
|
||||
async def test_get_single_bank(self, mcp_server_single_bank, mock_memory):
|
||||
result = await _tools(mcp_server_single_bank)["get_mental_model"].fn(mental_model_id="mm-1")
|
||||
assert isinstance(result, dict)
|
||||
assert result["id"] == "mm-1"
|
||||
|
||||
async def test_get_no_bank_returns_error(self, no_bank_mcp_server):
|
||||
result = await _tools(no_bank_mcp_server)["get_mental_model"].fn(mental_model_id="mm-1")
|
||||
assert "error" in result
|
||||
|
||||
async def test_get_engine_error(self, mcp_server_with_mental_models, mock_memory):
|
||||
mock_memory.get_mental_model.side_effect = RuntimeError("DB error")
|
||||
result = await _tools(mcp_server_with_mental_models)["get_mental_model"].fn(mental_model_id="mm-1")
|
||||
assert "error" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestCreateMentalModel:
|
||||
async def test_create_multi_bank(self, mcp_server_with_mental_models, mock_memory):
|
||||
result = await _tools(mcp_server_with_mental_models)["create_mental_model"].fn(
|
||||
name="Test Model",
|
||||
source_query="What are the user's preferences?",
|
||||
)
|
||||
assert '"mm-new"' in result
|
||||
assert '"op-123"' in result
|
||||
mock_memory.create_mental_model.assert_called_once()
|
||||
call_kwargs = mock_memory.create_mental_model.call_args.kwargs
|
||||
assert call_kwargs["name"] == "Test Model"
|
||||
assert call_kwargs["source_query"] == "What are the user's preferences?"
|
||||
assert call_kwargs["content"] == "Generating content..."
|
||||
# Verify async refresh was scheduled
|
||||
mock_memory.submit_async_refresh_mental_model.assert_called_once()
|
||||
assert mock_memory.submit_async_refresh_mental_model.call_args.kwargs["mental_model_id"] == "mm-new"
|
||||
|
||||
async def test_create_with_custom_id(self, mcp_server_with_mental_models, mock_memory):
|
||||
await _tools(mcp_server_with_mental_models)["create_mental_model"].fn(
|
||||
name="Test", source_query="query", mental_model_id="custom-id"
|
||||
)
|
||||
assert mock_memory.create_mental_model.call_args.kwargs["mental_model_id"] == "custom-id"
|
||||
|
||||
async def test_create_with_tags_and_max_tokens(self, mcp_server_with_mental_models, mock_memory):
|
||||
await _tools(mcp_server_with_mental_models)["create_mental_model"].fn(
|
||||
name="Test", source_query="query", tags=["work", "coding"], max_tokens=4096
|
||||
)
|
||||
call_kwargs = mock_memory.create_mental_model.call_args.kwargs
|
||||
assert call_kwargs["tags"] == ["work", "coding"]
|
||||
assert call_kwargs["max_tokens"] == 4096
|
||||
|
||||
async def test_create_with_bank_id_override(self, mcp_server_with_mental_models, mock_memory):
|
||||
await _tools(mcp_server_with_mental_models)["create_mental_model"].fn(
|
||||
name="Test", source_query="query", bank_id="other-bank"
|
||||
)
|
||||
assert mock_memory.create_mental_model.call_args.kwargs["bank_id"] == "other-bank"
|
||||
assert mock_memory.submit_async_refresh_mental_model.call_args.kwargs["bank_id"] == "other-bank"
|
||||
|
||||
async def test_create_single_bank(self, mcp_server_single_bank, mock_memory):
|
||||
result = await _tools(mcp_server_single_bank)["create_mental_model"].fn(name="Test", source_query="query")
|
||||
assert isinstance(result, dict)
|
||||
assert result["mental_model_id"] == "mm-new"
|
||||
assert result["operation_id"] == "op-123"
|
||||
|
||||
async def test_create_no_bank_returns_error(self, no_bank_mcp_server):
|
||||
result = await _tools(no_bank_mcp_server)["create_mental_model"].fn(name="Test", source_query="query")
|
||||
assert "error" in result
|
||||
|
||||
async def test_create_value_error_multi_bank(self, mcp_server_with_mental_models, mock_memory):
|
||||
"""ValueError from engine (e.g. invalid ID format) should return error, not crash."""
|
||||
mock_memory.create_mental_model.side_effect = ValueError("ID must be alphanumeric lowercase")
|
||||
result = await _tools(mcp_server_with_mental_models)["create_mental_model"].fn(
|
||||
name="Test", source_query="query", mental_model_id="INVALID!!"
|
||||
)
|
||||
assert "alphanumeric" in result
|
||||
|
||||
async def test_create_value_error_single_bank(self, mcp_server_single_bank, mock_memory):
|
||||
mock_memory.create_mental_model.side_effect = ValueError("ID must be alphanumeric lowercase")
|
||||
result = await _tools(mcp_server_single_bank)["create_mental_model"].fn(
|
||||
name="Test", source_query="query", mental_model_id="INVALID!!"
|
||||
)
|
||||
assert isinstance(result, dict)
|
||||
assert "alphanumeric" in result["error"]
|
||||
|
||||
async def test_create_engine_error(self, mcp_server_with_mental_models, mock_memory):
|
||||
mock_memory.create_mental_model.side_effect = RuntimeError("DB error")
|
||||
result = await _tools(mcp_server_with_mental_models)["create_mental_model"].fn(
|
||||
name="Test", source_query="query"
|
||||
)
|
||||
assert "error" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestUpdateMentalModel:
|
||||
async def test_update_multi_bank(self, mcp_server_with_mental_models, mock_memory):
|
||||
result = await _tools(mcp_server_with_mental_models)["update_mental_model"].fn(
|
||||
mental_model_id="mm-1", name="Updated Name"
|
||||
)
|
||||
assert '"Updated Name"' in result
|
||||
call_kwargs = mock_memory.update_mental_model.call_args.kwargs
|
||||
assert call_kwargs["name"] == "Updated Name"
|
||||
assert call_kwargs["source_query"] is None # Not updated
|
||||
|
||||
async def test_update_multiple_fields(self, mcp_server_with_mental_models, mock_memory):
|
||||
await _tools(mcp_server_with_mental_models)["update_mental_model"].fn(
|
||||
mental_model_id="mm-1", name="New Name", source_query="new query?", tags=["updated"], max_tokens=4096
|
||||
)
|
||||
call_kwargs = mock_memory.update_mental_model.call_args.kwargs
|
||||
assert call_kwargs["name"] == "New Name"
|
||||
assert call_kwargs["source_query"] == "new query?"
|
||||
assert call_kwargs["tags"] == ["updated"]
|
||||
assert call_kwargs["max_tokens"] == 4096
|
||||
|
||||
async def test_update_with_bank_id_override(self, mcp_server_with_mental_models, mock_memory):
|
||||
await _tools(mcp_server_with_mental_models)["update_mental_model"].fn(
|
||||
mental_model_id="mm-1", name="X", bank_id="other-bank"
|
||||
)
|
||||
assert mock_memory.update_mental_model.call_args.kwargs["bank_id"] == "other-bank"
|
||||
|
||||
async def test_update_not_found_multi_bank(self, mcp_server_with_mental_models, mock_memory):
|
||||
mock_memory.update_mental_model.return_value = None
|
||||
result = await _tools(mcp_server_with_mental_models)["update_mental_model"].fn(
|
||||
mental_model_id="missing", name="X"
|
||||
)
|
||||
assert "not found" in result
|
||||
|
||||
async def test_update_single_bank(self, mcp_server_single_bank, mock_memory):
|
||||
result = await _tools(mcp_server_single_bank)["update_mental_model"].fn(mental_model_id="mm-1", name="Updated")
|
||||
assert isinstance(result, dict)
|
||||
assert mock_memory.update_mental_model.call_args.kwargs["bank_id"] == "fixed-bank"
|
||||
|
||||
async def test_update_not_found_single_bank(self, mcp_server_single_bank, mock_memory):
|
||||
mock_memory.update_mental_model.return_value = None
|
||||
result = await _tools(mcp_server_single_bank)["update_mental_model"].fn(mental_model_id="missing", name="X")
|
||||
assert isinstance(result, dict)
|
||||
assert "not found" in result["error"]
|
||||
|
||||
async def test_update_no_bank_returns_error(self, no_bank_mcp_server):
|
||||
result = await _tools(no_bank_mcp_server)["update_mental_model"].fn(mental_model_id="mm-1", name="X")
|
||||
assert "error" in result
|
||||
|
||||
async def test_update_engine_error(self, mcp_server_with_mental_models, mock_memory):
|
||||
mock_memory.update_mental_model.side_effect = RuntimeError("DB error")
|
||||
result = await _tools(mcp_server_with_mental_models)["update_mental_model"].fn(mental_model_id="mm-1", name="X")
|
||||
assert "error" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestDeleteMentalModel:
|
||||
async def test_delete_multi_bank(self, mcp_server_with_mental_models, mock_memory):
|
||||
result = await _tools(mcp_server_with_mental_models)["delete_mental_model"].fn(mental_model_id="mm-1")
|
||||
assert '"deleted"' in result
|
||||
assert mock_memory.delete_mental_model.call_args.kwargs["mental_model_id"] == "mm-1"
|
||||
|
||||
async def test_delete_with_bank_id_override(self, mcp_server_with_mental_models, mock_memory):
|
||||
await _tools(mcp_server_with_mental_models)["delete_mental_model"].fn(
|
||||
mental_model_id="mm-1", bank_id="other-bank"
|
||||
)
|
||||
assert mock_memory.delete_mental_model.call_args.kwargs["bank_id"] == "other-bank"
|
||||
|
||||
async def test_delete_not_found_multi_bank(self, mcp_server_with_mental_models, mock_memory):
|
||||
mock_memory.delete_mental_model.return_value = False
|
||||
result = await _tools(mcp_server_with_mental_models)["delete_mental_model"].fn(mental_model_id="missing")
|
||||
assert "not found" in result
|
||||
|
||||
async def test_delete_not_found_single_bank(self, mcp_server_single_bank, mock_memory):
|
||||
mock_memory.delete_mental_model.return_value = False
|
||||
result = await _tools(mcp_server_single_bank)["delete_mental_model"].fn(mental_model_id="missing")
|
||||
assert isinstance(result, dict)
|
||||
assert "not found" in result["error"]
|
||||
|
||||
async def test_delete_single_bank(self, mcp_server_single_bank, mock_memory):
|
||||
result = await _tools(mcp_server_single_bank)["delete_mental_model"].fn(mental_model_id="mm-1")
|
||||
assert isinstance(result, dict)
|
||||
assert result["status"] == "deleted"
|
||||
|
||||
async def test_delete_no_bank_returns_error(self, no_bank_mcp_server):
|
||||
result = await _tools(no_bank_mcp_server)["delete_mental_model"].fn(mental_model_id="mm-1")
|
||||
assert "error" in result
|
||||
|
||||
async def test_delete_engine_error(self, mcp_server_with_mental_models, mock_memory):
|
||||
mock_memory.delete_mental_model.side_effect = RuntimeError("DB error")
|
||||
result = await _tools(mcp_server_with_mental_models)["delete_mental_model"].fn(mental_model_id="mm-1")
|
||||
assert "error" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestRefreshMentalModel:
|
||||
async def test_refresh_multi_bank(self, mcp_server_with_mental_models, mock_memory):
|
||||
result = await _tools(mcp_server_with_mental_models)["refresh_mental_model"].fn(mental_model_id="mm-1")
|
||||
assert '"op-123"' in result
|
||||
assert '"queued"' in result
|
||||
|
||||
async def test_refresh_with_bank_id_override(self, mcp_server_with_mental_models, mock_memory):
|
||||
await _tools(mcp_server_with_mental_models)["refresh_mental_model"].fn(
|
||||
mental_model_id="mm-1", bank_id="other-bank"
|
||||
)
|
||||
assert mock_memory.submit_async_refresh_mental_model.call_args.kwargs["bank_id"] == "other-bank"
|
||||
|
||||
async def test_refresh_not_found_multi_bank(self, mcp_server_with_mental_models, mock_memory):
|
||||
mock_memory.submit_async_refresh_mental_model.side_effect = ValueError("Mental model 'missing' not found")
|
||||
result = await _tools(mcp_server_with_mental_models)["refresh_mental_model"].fn(mental_model_id="missing")
|
||||
assert "not found" in result
|
||||
|
||||
async def test_refresh_not_found_single_bank(self, mcp_server_single_bank, mock_memory):
|
||||
mock_memory.submit_async_refresh_mental_model.side_effect = ValueError("not found")
|
||||
result = await _tools(mcp_server_single_bank)["refresh_mental_model"].fn(mental_model_id="missing")
|
||||
assert isinstance(result, dict)
|
||||
assert "not found" in result["error"]
|
||||
|
||||
async def test_refresh_single_bank(self, mcp_server_single_bank, mock_memory):
|
||||
result = await _tools(mcp_server_single_bank)["refresh_mental_model"].fn(mental_model_id="mm-1")
|
||||
assert isinstance(result, dict)
|
||||
assert result["operation_id"] == "op-123"
|
||||
|
||||
async def test_refresh_no_bank_returns_error(self, no_bank_mcp_server):
|
||||
result = await _tools(no_bank_mcp_server)["refresh_mental_model"].fn(mental_model_id="mm-1")
|
||||
assert "error" in result
|
||||
|
||||
async def test_refresh_engine_error(self, mcp_server_with_mental_models, mock_memory):
|
||||
mock_memory.submit_async_refresh_mental_model.side_effect = RuntimeError("DB error")
|
||||
result = await _tools(mcp_server_with_mental_models)["refresh_mental_model"].fn(mental_model_id="mm-1")
|
||||
assert "error" in result
|
||||
|
||||
|
||||
class TestValidateMentalModelInputs:
|
||||
"""Tests for the _validate_mental_model_inputs helper."""
|
||||
|
||||
def test_valid_inputs(self):
|
||||
assert _validate_mental_model_inputs(name="Test", source_query="query", max_tokens=2048) is None
|
||||
|
||||
def test_none_inputs(self):
|
||||
assert _validate_mental_model_inputs() is None
|
||||
|
||||
def test_empty_name(self):
|
||||
result = _validate_mental_model_inputs(name="")
|
||||
assert result == "name cannot be empty"
|
||||
|
||||
def test_whitespace_name(self):
|
||||
result = _validate_mental_model_inputs(name=" ")
|
||||
assert result == "name cannot be empty"
|
||||
|
||||
def test_empty_source_query(self):
|
||||
result = _validate_mental_model_inputs(source_query="")
|
||||
assert result == "source_query cannot be empty"
|
||||
|
||||
def test_whitespace_source_query(self):
|
||||
result = _validate_mental_model_inputs(source_query=" \t ")
|
||||
assert result == "source_query cannot be empty"
|
||||
|
||||
def test_max_tokens_too_low(self):
|
||||
result = _validate_mental_model_inputs(max_tokens=0)
|
||||
assert "max_tokens must be between 256 and 8192" in result
|
||||
|
||||
def test_max_tokens_too_high(self):
|
||||
result = _validate_mental_model_inputs(max_tokens=10000)
|
||||
assert "max_tokens must be between 256 and 8192" in result
|
||||
|
||||
def test_max_tokens_at_lower_bound(self):
|
||||
assert _validate_mental_model_inputs(max_tokens=256) is None
|
||||
|
||||
def test_max_tokens_at_upper_bound(self):
|
||||
assert _validate_mental_model_inputs(max_tokens=8192) is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestMentalModelInputValidation:
|
||||
"""Tests that validation is applied in create/update tools before engine calls."""
|
||||
|
||||
async def test_create_empty_name_returns_error_multi_bank(self, mcp_server_with_mental_models, mock_memory):
|
||||
result = await _tools(mcp_server_with_mental_models)["create_mental_model"].fn(name="", source_query="query")
|
||||
assert "name cannot be empty" in result
|
||||
mock_memory.create_mental_model.assert_not_called()
|
||||
|
||||
async def test_create_empty_source_query_returns_error_multi_bank(self, mcp_server_with_mental_models, mock_memory):
|
||||
result = await _tools(mcp_server_with_mental_models)["create_mental_model"].fn(name="Test", source_query="")
|
||||
assert "source_query cannot be empty" in result
|
||||
mock_memory.create_mental_model.assert_not_called()
|
||||
|
||||
async def test_create_max_tokens_too_low_multi_bank(self, mcp_server_with_mental_models, mock_memory):
|
||||
result = await _tools(mcp_server_with_mental_models)["create_mental_model"].fn(
|
||||
name="Test", source_query="query", max_tokens=0
|
||||
)
|
||||
assert "max_tokens must be between 256 and 8192" in result
|
||||
mock_memory.create_mental_model.assert_not_called()
|
||||
|
||||
async def test_create_max_tokens_too_high_single_bank(self, mcp_server_single_bank, mock_memory):
|
||||
result = await _tools(mcp_server_single_bank)["create_mental_model"].fn(
|
||||
name="Test", source_query="query", max_tokens=10000
|
||||
)
|
||||
assert isinstance(result, dict)
|
||||
assert "max_tokens must be between 256 and 8192" in result["error"]
|
||||
mock_memory.create_mental_model.assert_not_called()
|
||||
|
||||
async def test_update_empty_name_returns_error_multi_bank(self, mcp_server_with_mental_models, mock_memory):
|
||||
result = await _tools(mcp_server_with_mental_models)["update_mental_model"].fn(mental_model_id="mm-1", name="")
|
||||
assert "name cannot be empty" in result
|
||||
mock_memory.update_mental_model.assert_not_called()
|
||||
|
||||
async def test_update_empty_name_returns_error_single_bank(self, mcp_server_single_bank, mock_memory):
|
||||
result = await _tools(mcp_server_single_bank)["update_mental_model"].fn(mental_model_id="mm-1", name=" ")
|
||||
assert isinstance(result, dict)
|
||||
assert "name cannot be empty" in result["error"]
|
||||
mock_memory.update_mental_model.assert_not_called()
|
||||
|
||||
async def test_not_found_error_includes_bank_id_multi_bank(self, mcp_server_with_mental_models, mock_memory):
|
||||
mock_memory.get_mental_model.return_value = None
|
||||
result = await _tools(mcp_server_with_mental_models)["get_mental_model"].fn(mental_model_id="missing")
|
||||
assert "test-bank" in result
|
||||
|
||||
async def test_not_found_error_includes_bank_id_single_bank(self, mcp_server_single_bank, mock_memory):
|
||||
mock_memory.get_mental_model.return_value = None
|
||||
result = await _tools(mcp_server_single_bank)["get_mental_model"].fn(mental_model_id="missing")
|
||||
assert isinstance(result, dict)
|
||||
assert "fixed-bank" in result["error"]
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
# How We Solved Memory Conflicts in Hindsight
|
||||
One of the hardest problems we tackled in Hindsight was dealing with contradictions. When you're building a memory system for AI agents, reality isn't static. It evolves.
|
||||
|
||||
A CRM agent might learn that "Acme Corp is a key prospect" in January, then encounter "Acme Corp is now a paying customer" in March. Naive approaches either lose the history or drown in duplicate facts.
|
||||
|
||||
We needed a system that could handle this gracefully. Here's how we built it.
|
||||
|
||||
## **The Core Problem: Facts vs. Knowledge**
|
||||
|
||||
Early on, we made a distinction that shaped the way we store and receive memories: raw facts aren't the same as consolidated knowledge. Facts are immediate observations; what an agent learns in a single interaction. Knowledge is durable understanding extracted from those facts over time.
|
||||
|
||||
Our consolidation pipeline runs as a background job after new memories are retained, transforming ephemeral facts into lasting knowledge. The key insight is that we don't just store the latest information, we track how knowledge evolves.
|
||||
|
||||

|
||||
|
||||
## **Finding Related Information**
|
||||
|
||||
When a new fact comes in, we first need to find existing observations that might conflict with it. This happens in our `_find_related_observations` function, which uses the full recall system with:
|
||||
|
||||
* **Semantic similarity** via embeddings to surface conceptually related observations
|
||||
* **Token budget** that naturally limits comparison scope via `consolidation_max_tokens`
|
||||
* **Security-aware filtering** using strict tag matching to prevent cross-user information leakage
|
||||
|
||||
```python
|
||||
recall_result = await memory_engine.recall_async(
|
||||
bank_id=bank_id,
|
||||
query=query,
|
||||
max_tokens=config.consolidation_max_tokens,
|
||||
fact_type=["observation"],
|
||||
tags=tags,
|
||||
tags_match="all_strict",
|
||||
)
|
||||
```
|
||||
|
||||
## **LLM-Powered Conflict Analysis**
|
||||
|
||||
The real work happens in `_consolidate_with_llm`, where a single LLM call analyzes the new fact against existing observations. We provide rich context: the text of existing observations, their proof counts (how many supporting facts), and source memories. We organize those memories into a time series with dates and order them chronologically.
|
||||
|
||||
This lets the model make informed decisions about whether new information is redundant, contradictory, or represents a genuine state change.
|
||||
|
||||
## **Three Merge Strategies**
|
||||
|
||||
Our consolidation prompt defines three core merge rules:
|
||||
|
||||
**Redundant information:** When the same information is worded differently, we update the existing observation. "Acme Corp is a prospect" plus "Acme Corp is a potential customer" just becomes a single, cleaner observation.
|
||||
|
||||
**Direct contradictions:** When opposite information exists about the same topic, we preserve both states with temporal markers. The critical rule: updated text must capture both states. We don't overwrite old information. Instead, we try to create a temporal narrative that explains how the facts change over time. When no clear explanation exists, we consider the most recent data point to be up-to-date.
|
||||
|
||||
**State updates:** When new information replaces old state, we explicitly capture the transition with phrases like "used to," "now," or "changed from X to Y." We never just state the new fact, we capture the evolution.
|
||||
|
||||
## **Preserving Business Relationship History**
|
||||
|
||||
Consider how this handles an evolving business relationship. An agent learns these facts over six months:
|
||||
|
||||
1. January: "Acme Corp expressed interest in our enterprise tier"
|
||||
2. February: "Met with Acme Corp's CTO to discuss integration requirements"
|
||||
3. April: "Acme Corp signed a $50K annual contract"
|
||||
4. September: "Acme Corp upgraded to the $150K tier after expanding to 3 regions"
|
||||
|
||||

|
||||
|
||||
A naive system might just keep the latest fact: "Acme Corp is on a $150K contract." Useful, but you've lost the relationship arc.
|
||||
|
||||
Our system consolidates this into something like: "Acme Corp progressed from prospect (January) to $50K customer (April), then upgraded to $150K tier in September after regional expansion."
|
||||
|
||||
The full journey is preserved. An agent can answer "How did we land Acme Corp?" without losing the relationship history that makes that question meaningful.
|
||||
|
||||
## **Temporal Metadata**
|
||||
|
||||
We maintain temporal metadata for each observation:
|
||||
|
||||
```sql
|
||||
occurred_start = LEAST(occurred_start, COALESCE($7, occurred_start))
|
||||
occurred_end = GREATEST(occurred_end, COALESCE($8, occurred_end))
|
||||
mentioned_at = GREATEST(mentioned_at, COALESCE($9, mentioned_at))
|
||||
```
|
||||
|
||||
This ensures `occurred_start` keeps the earliest time something was true, `occurred_end` tracks the most recent observation, and `mentioned_at` records when it was last referenced.
|
||||
|
||||
## **History as an Audit Trail**
|
||||
|
||||
Every observation maintains a complete change history:
|
||||
|
||||
```python
|
||||
history.append({
|
||||
"previous_text": model["text"],
|
||||
"changed_at": datetime.now(timezone.utc).isoformat(),
|
||||
"reason": reason,
|
||||
"source_memory_id": str(memory_id),
|
||||
})
|
||||
```
|
||||
|
||||
This audit trail lets the system explain how knowledge evolved, trace back to source facts that caused updates, and provide reasoning for why information changed. When an agent says "Acme Corp is a $150K customer," it can also explain how it knows that and what changed along the way.
|
||||
|
||||

|
||||
|
||||
## **Security at the Boundary**
|
||||
|
||||
One design decision we're particularly happy with: tag-based security boundaries during consolidation.
|
||||
|
||||
New observations inherit their source fact's tags. When updating existing observations, tags merge (union) so all contributors can see the knowledge they helped create. Consolidation only happens within the same security scope. Strict matching prevents information leakage while still allowing collaborative knowledge building.
|
||||
|
||||
```python
|
||||
existing_tags = set(model.get("tags", []) or [])
|
||||
source_tags = set(source_fact_tags or [])
|
||||
merged_tags = list(existing_tags | source_tags)
|
||||
```
|
||||
|
||||
## **Durable Knowledge, Not Ephemeral State**
|
||||
|
||||
A principle that significantly reduced false conflicts: we distinguish between durable knowledge and ephemeral state.
|
||||
|
||||
Good consolidation extracts lasting facts. "User visited Acme Corp at Room 105" becomes "Acme Corp is located in Room 105." But "User is currently in Room 105" isn't tracked because ephemeral position data changes constantly.
|
||||
|
||||
This focus on durability means the system isn't constantly flagging temporary state changes as conflicts.
|
||||
|
||||
## **Why This Matters**
|
||||
|
||||
Handling contradictory information in agent memory doesn't require picking winners and losers. By tracking temporal evolution, preserving history, and consolidating intelligently, we built a system that maintains nuanced understanding of how knowledge changes.
|
||||
|
||||
For agents that need to track changing preferences, understand temporal relationships, maintain audit trails, and build trust through explainable updates.
|
||||
|
||||
This approach allows Hindsight to provide more than just memory storage. It can provide context that improves with every interaction.
|
||||
@@ -269,15 +269,16 @@ export HINDSIGHT_API_RETAIN_LLM_MAX_BACKOFF=120.0 # Cap at 2min instead of 1m
|
||||
|----------|-------------|---------|
|
||||
| `HINDSIGHT_API_EMBEDDINGS_PROVIDER` | Provider: `local`, `tei`, `openai`, `cohere`, or `litellm` | `local` |
|
||||
| `HINDSIGHT_API_EMBEDDINGS_LOCAL_MODEL` | Model for local provider | `BAAI/bge-small-en-v1.5` |
|
||||
| `HINDSIGHT_API_EMBEDDINGS_LOCAL_TRUST_REMOTE_CODE` | Allow loading models with custom code (security risk, disabled by default) | `false` |
|
||||
| `HINDSIGHT_API_EMBEDDINGS_TEI_URL` | TEI server URL | - |
|
||||
| `HINDSIGHT_API_EMBEDDINGS_OPENAI_API_KEY` | OpenAI API key (falls back to `HINDSIGHT_API_LLM_API_KEY`) | - |
|
||||
| `HINDSIGHT_API_EMBEDDINGS_OPENAI_MODEL` | OpenAI embedding model | `text-embedding-3-small` |
|
||||
| `HINDSIGHT_API_EMBEDDINGS_OPENAI_BASE_URL` | Custom base URL for OpenAI-compatible API (e.g., Azure OpenAI) | - |
|
||||
| `HINDSIGHT_API_COHERE_API_KEY` | Cohere API key (shared for embeddings and reranker) | - |
|
||||
| `HINDSIGHT_API_EMBEDDINGS_COHERE_API_KEY` | Cohere API key for embeddings | - |
|
||||
| `HINDSIGHT_API_EMBEDDINGS_COHERE_MODEL` | Cohere embedding model | `embed-english-v3.0` |
|
||||
| `HINDSIGHT_API_EMBEDDINGS_COHERE_BASE_URL` | Custom base URL for Cohere-compatible API (e.g., Azure-hosted) | - |
|
||||
| `HINDSIGHT_API_LITELLM_API_BASE` | LiteLLM proxy base URL (shared for embeddings and reranker) | `http://localhost:4000` |
|
||||
| `HINDSIGHT_API_LITELLM_API_KEY` | LiteLLM proxy API key (optional, depends on proxy config) | - |
|
||||
| `HINDSIGHT_API_EMBEDDINGS_LITELLM_API_BASE` | LiteLLM proxy base URL for embeddings | `http://localhost:4000` |
|
||||
| `HINDSIGHT_API_EMBEDDINGS_LITELLM_API_KEY` | LiteLLM proxy API key for embeddings (optional, depends on proxy config) | - |
|
||||
| `HINDSIGHT_API_EMBEDDINGS_LITELLM_MODEL` | LiteLLM embedding model (use provider prefix, e.g., `cohere/embed-english-v3.0`) | `text-embedding-3-small` |
|
||||
|
||||
```bash
|
||||
@@ -285,6 +286,11 @@ export HINDSIGHT_API_RETAIN_LLM_MAX_BACKOFF=120.0 # Cap at 2min instead of 1m
|
||||
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=local
|
||||
export HINDSIGHT_API_EMBEDDINGS_LOCAL_MODEL=BAAI/bge-small-en-v1.5
|
||||
|
||||
# Local with custom model requiring trust_remote_code
|
||||
# WARNING: Only enable trust_remote_code for models you trust (security risk)
|
||||
# export HINDSIGHT_API_EMBEDDINGS_LOCAL_MODEL=your-custom-model
|
||||
# export HINDSIGHT_API_EMBEDDINGS_LOCAL_TRUST_REMOTE_CODE=true
|
||||
|
||||
# OpenAI - cloud-based embeddings
|
||||
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=openai
|
||||
export HINDSIGHT_API_EMBEDDINGS_OPENAI_API_KEY=sk-xxxxxxxxxxxx # or reuses HINDSIGHT_API_LLM_API_KEY
|
||||
@@ -302,19 +308,19 @@ export HINDSIGHT_API_EMBEDDINGS_TEI_URL=http://localhost:8080
|
||||
|
||||
# Cohere - cloud-based embeddings
|
||||
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=cohere
|
||||
export HINDSIGHT_API_COHERE_API_KEY=your-api-key
|
||||
export HINDSIGHT_API_EMBEDDINGS_COHERE_API_KEY=your-api-key
|
||||
export HINDSIGHT_API_EMBEDDINGS_COHERE_MODEL=embed-english-v3.0 # 1024 dimensions
|
||||
|
||||
# Azure-hosted Cohere - embeddings via custom endpoint
|
||||
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=cohere
|
||||
export HINDSIGHT_API_COHERE_API_KEY=your-azure-api-key
|
||||
export HINDSIGHT_API_EMBEDDINGS_COHERE_API_KEY=your-azure-api-key
|
||||
export HINDSIGHT_API_EMBEDDINGS_COHERE_MODEL=embed-english-v3.0
|
||||
export HINDSIGHT_API_EMBEDDINGS_COHERE_BASE_URL=https://your-azure-cohere-endpoint.com
|
||||
|
||||
# LiteLLM proxy - unified gateway for multiple providers
|
||||
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=litellm
|
||||
export HINDSIGHT_API_LITELLM_API_BASE=http://localhost:4000
|
||||
export HINDSIGHT_API_LITELLM_API_KEY=your-litellm-key # optional
|
||||
export HINDSIGHT_API_EMBEDDINGS_LITELLM_API_BASE=http://localhost:4000
|
||||
export HINDSIGHT_API_EMBEDDINGS_LITELLM_API_KEY=your-litellm-key # optional
|
||||
export HINDSIGHT_API_EMBEDDINGS_LITELLM_MODEL=text-embedding-3-small # or cohere/embed-english-v3.0
|
||||
```
|
||||
|
||||
@@ -341,11 +347,15 @@ Supported OpenAI embedding dimensions:
|
||||
| `HINDSIGHT_API_RERANKER_PROVIDER` | Provider: `local`, `tei`, `cohere`, `flashrank`, `litellm`, or `rrf` | `local` |
|
||||
| `HINDSIGHT_API_RERANKER_LOCAL_MODEL` | Model for local provider | `cross-encoder/ms-marco-MiniLM-L-6-v2` |
|
||||
| `HINDSIGHT_API_RERANKER_LOCAL_MAX_CONCURRENT` | Max concurrent local reranking (prevents CPU thrashing under load) | `4` |
|
||||
| `HINDSIGHT_API_RERANKER_LOCAL_TRUST_REMOTE_CODE` | Allow loading models with custom code (security risk, disabled by default) | `false` |
|
||||
| `HINDSIGHT_API_RERANKER_TEI_URL` | TEI server URL | - |
|
||||
| `HINDSIGHT_API_RERANKER_TEI_BATCH_SIZE` | Batch size for TEI reranking | `128` |
|
||||
| `HINDSIGHT_API_RERANKER_TEI_MAX_CONCURRENT` | Max concurrent TEI reranking requests | `8` |
|
||||
| `HINDSIGHT_API_RERANKER_COHERE_API_KEY` | Cohere API key for reranking | - |
|
||||
| `HINDSIGHT_API_RERANKER_COHERE_MODEL` | Cohere rerank model | `rerank-english-v3.0` |
|
||||
| `HINDSIGHT_API_RERANKER_COHERE_BASE_URL` | Custom base URL for Cohere-compatible API (e.g., Azure-hosted) | - |
|
||||
| `HINDSIGHT_API_RERANKER_LITELLM_API_BASE` | LiteLLM proxy base URL for reranking | `http://localhost:4000` |
|
||||
| `HINDSIGHT_API_RERANKER_LITELLM_API_KEY` | LiteLLM proxy API key for reranking (optional, depends on proxy config) | - |
|
||||
| `HINDSIGHT_API_RERANKER_LITELLM_MODEL` | LiteLLM rerank model (use provider prefix, e.g., `cohere/rerank-english-v3.0`) | `cohere/rerank-english-v3.0` |
|
||||
| `HINDSIGHT_API_RERANKER_FLASHRANK_MODEL` | FlashRank model for fast CPU-based reranking | `ms-marco-MiniLM-L-12-v2` |
|
||||
| `HINDSIGHT_API_RERANKER_FLASHRANK_CACHE_DIR` | Cache directory for FlashRank models | System default |
|
||||
@@ -355,25 +365,31 @@ Supported OpenAI embedding dimensions:
|
||||
export HINDSIGHT_API_RERANKER_PROVIDER=local
|
||||
export HINDSIGHT_API_RERANKER_LOCAL_MODEL=cross-encoder/ms-marco-MiniLM-L-6-v2
|
||||
|
||||
# Local with custom model requiring trust_remote_code (e.g., jina-reranker-v2)
|
||||
# WARNING: Only enable trust_remote_code for models you trust (security risk)
|
||||
export HINDSIGHT_API_RERANKER_PROVIDER=local
|
||||
export HINDSIGHT_API_RERANKER_LOCAL_MODEL=jinaai/jina-reranker-v2-base-multilingual
|
||||
export HINDSIGHT_API_RERANKER_LOCAL_TRUST_REMOTE_CODE=true
|
||||
|
||||
# TEI - for high-performance inference
|
||||
export HINDSIGHT_API_RERANKER_PROVIDER=tei
|
||||
export HINDSIGHT_API_RERANKER_TEI_URL=http://localhost:8081
|
||||
|
||||
# Cohere - cloud-based reranking
|
||||
export HINDSIGHT_API_RERANKER_PROVIDER=cohere
|
||||
export HINDSIGHT_API_COHERE_API_KEY=your-api-key # shared with embeddings
|
||||
export HINDSIGHT_API_RERANKER_COHERE_API_KEY=your-api-key
|
||||
export HINDSIGHT_API_RERANKER_COHERE_MODEL=rerank-english-v3.0
|
||||
|
||||
# Azure-hosted Cohere - reranking via custom endpoint
|
||||
export HINDSIGHT_API_RERANKER_PROVIDER=cohere
|
||||
export HINDSIGHT_API_COHERE_API_KEY=your-azure-api-key
|
||||
export HINDSIGHT_API_RERANKER_COHERE_API_KEY=your-azure-api-key
|
||||
export HINDSIGHT_API_RERANKER_COHERE_MODEL=rerank-english-v3.0
|
||||
export HINDSIGHT_API_RERANKER_COHERE_BASE_URL=https://your-azure-cohere-endpoint.com
|
||||
|
||||
# LiteLLM proxy - unified gateway for multiple reranking providers
|
||||
export HINDSIGHT_API_RERANKER_PROVIDER=litellm
|
||||
export HINDSIGHT_API_LITELLM_API_BASE=http://localhost:4000
|
||||
export HINDSIGHT_API_LITELLM_API_KEY=your-litellm-key # optional
|
||||
export HINDSIGHT_API_RERANKER_LITELLM_API_BASE=http://localhost:4000
|
||||
export HINDSIGHT_API_RERANKER_LITELLM_API_KEY=your-litellm-key # optional
|
||||
export HINDSIGHT_API_RERANKER_LITELLM_MODEL=cohere/rerank-english-v3.0 # or voyage/rerank-2, together_ai/...
|
||||
```
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@
|
||||
}
|
||||
|
||||
[data-theme='dark'] .cardDescription {
|
||||
color: var(--ifm-color-emphasis-700);
|
||||
color: var(--ifm-color-emphasis-600);
|
||||
}
|
||||
|
||||
.cardTags {
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
.container {
|
||||
margin: 0 0 20px 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.banner {
|
||||
background: linear-gradient(135deg, #0074d9 0%, #005db0 100%);
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
color: white;
|
||||
box-shadow: 0 2px 8px rgba(0, 116, 217, 0.12);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
transition: box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.banner:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 116, 217, 0.2);
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.commandWrapper {
|
||||
margin-top: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.command {
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
font-family: 'JetBrains Mono', 'Fira Code', 'SF Mono', Monaco, Consolas, monospace;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
color: #e6f7f8;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.copyButton {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
padding: 6px 8px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
}
|
||||
|
||||
.copyButton:hover {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.copyButton:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.copyButton svg {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Dark mode - make it stand out even more */
|
||||
html[data-theme='dark'] .banner {
|
||||
box-shadow: 0 4px 12px rgba(0, 116, 217, 0.2),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
html[data-theme='dark'] .banner:hover {
|
||||
box-shadow: 0 6px 16px rgba(0, 116, 217, 0.3),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
/* Light mode adjustments */
|
||||
html[data-theme='light'] .banner {
|
||||
box-shadow: 0 4px 12px rgba(0, 116, 217, 0.15),
|
||||
0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
html[data-theme='light'] .banner:hover {
|
||||
box-shadow: 0 6px 16px rgba(0, 116, 217, 0.25),
|
||||
0 4px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import React, { useState } from 'react';
|
||||
import styles from './SkillBanner.module.css';
|
||||
|
||||
export default function SkillBanner(): JSX.Element {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const command = 'npx skills add https://github.com/vectorize-io/hindsight --skill hindsight-docs';
|
||||
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(command);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.banner}>
|
||||
<div className={styles.icon}>🤖</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.title}>
|
||||
Using a coding agent? Install the docs skill for instant access
|
||||
</div>
|
||||
<div className={styles.commandWrapper}>
|
||||
<code className={styles.command}>
|
||||
{command}
|
||||
</code>
|
||||
<button
|
||||
className={styles.copyButton}
|
||||
onClick={handleCopy}
|
||||
aria-label="Copy command"
|
||||
title={copied ? 'Copied!' : 'Copy to clipboard'}
|
||||
>
|
||||
{copied ? (
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<polyline points="20 6 9 17 4 12"></polyline>
|
||||
</svg>
|
||||
) : (
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
.toastContainer {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
z-index: 9999;
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.toastContainer.show {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.toast {
|
||||
background: linear-gradient(135deg, #0074d9 0%, #005db0 100%);
|
||||
color: white;
|
||||
border-radius: 12px;
|
||||
padding: 16px 20px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
max-width: 420px;
|
||||
min-width: 320px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 24px;
|
||||
flex-shrink: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.message {
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.command {
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-family: 'JetBrains Mono', 'Fira Code', 'SF Mono', Monaco, Consolas, monospace;
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
color: #e6f7f8;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s ease;
|
||||
padding: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.closeButton:hover {
|
||||
opacity: 1;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.closeButton:active {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
/* Mobile responsive */
|
||||
@media (max-width: 768px) {
|
||||
.toastContainer {
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
left: 16px;
|
||||
}
|
||||
|
||||
.toast {
|
||||
min-width: unset;
|
||||
max-width: unset;
|
||||
width: 100%;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.command {
|
||||
font-size: 11px;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark mode adjustments (if needed) */
|
||||
html[data-theme='dark'] .toast {
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import styles from './SkillToast.module.css';
|
||||
|
||||
const STORAGE_KEY = 'hindsight-skill-toast-dismissed';
|
||||
|
||||
export default function SkillToast(): JSX.Element | null {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [isAnimating, setIsAnimating] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Check if user has already dismissed the toast
|
||||
const dismissed = localStorage.getItem(STORAGE_KEY);
|
||||
|
||||
if (!dismissed) {
|
||||
// Show toast after a short delay
|
||||
const timer = setTimeout(() => {
|
||||
setIsVisible(true);
|
||||
setIsAnimating(true);
|
||||
}, 1500);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleDismiss = () => {
|
||||
setIsAnimating(false);
|
||||
setTimeout(() => {
|
||||
setIsVisible(false);
|
||||
localStorage.setItem(STORAGE_KEY, 'true');
|
||||
}, 300); // Match animation duration
|
||||
};
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
return (
|
||||
<div className={`${styles.toastContainer} ${isAnimating ? styles.show : ''}`}>
|
||||
<div className={styles.toast}>
|
||||
<div className={styles.icon}>🤖</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.title}>Building with a coding agent?</div>
|
||||
<div className={styles.message}>
|
||||
Install the Hindsight documentation skill for faster development:
|
||||
</div>
|
||||
<code className={styles.command}>
|
||||
curl -fsSL https://hindsight.vectorize.io/get-skill | bash
|
||||
</code>
|
||||
</div>
|
||||
<button
|
||||
className={styles.closeButton}
|
||||
onClick={handleDismiss}
|
||||
aria-label="Dismiss notification"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import Breadcrumbs from '@theme-original/DocBreadcrumbs';
|
||||
import type BreadcrumbsType from '@theme/DocBreadcrumbs';
|
||||
import type {WrapperProps} from '@docusaurus/types';
|
||||
import SkillBanner from '@site/src/components/SkillBanner';
|
||||
|
||||
type Props = WrapperProps<typeof BreadcrumbsType>;
|
||||
|
||||
export default function BreadcrumbsWrapper(props: Props): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<Breadcrumbs {...props} />
|
||||
<SkillBanner />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import Sidebar from '@theme-original/DocPage/Layout/Sidebar';
|
||||
import type SidebarType from '@theme/DocPage/Layout/Sidebar';
|
||||
import type {WrapperProps} from '@docusaurus/types';
|
||||
import {useLocation} from '@docusaurus/router';
|
||||
import SkillBanner from '@site/src/components/SkillBanner';
|
||||
|
||||
type Props = WrapperProps<typeof SidebarType>;
|
||||
|
||||
@@ -15,5 +16,10 @@ export default function SidebarWrapper(props: Props): JSX.Element | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Sidebar {...props} />;
|
||||
return (
|
||||
<>
|
||||
<SkillBanner />
|
||||
<Sidebar {...props} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import Content from '@theme-original/DocSidebar/Desktop/Content';
|
||||
import type ContentType from '@theme/DocSidebar/Desktop/Content';
|
||||
import type {WrapperProps} from '@docusaurus/types';
|
||||
|
||||
type Props = WrapperProps<typeof ContentType>;
|
||||
|
||||
export default function ContentWrapper(props: Props): JSX.Element {
|
||||
return <Content {...props} />;
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 541 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 698 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 390 KiB |
@@ -270,6 +270,7 @@ function getPluginConfig(api: MoltbotPluginAPI): PluginConfig {
|
||||
// Dynamic bank ID options (default: enabled)
|
||||
dynamicBankId: config.dynamicBankId !== false,
|
||||
bankIdPrefix: config.bankIdPrefix,
|
||||
excludeProviders: Array.isArray(config.excludeProviders) ? config.excludeProviders : [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -549,6 +550,12 @@ export default function (api: MoltbotPluginAPI) {
|
||||
}
|
||||
currentAgentContext = ctx;
|
||||
|
||||
// Check if this provider is excluded
|
||||
if (ctx?.messageProvider && pluginConfig.excludeProviders?.includes(ctx.messageProvider)) {
|
||||
console.log(`[Hindsight] Skipping recall for excluded provider: ${ctx.messageProvider}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Derive bank ID from context
|
||||
const bankId = deriveBankId(ctx, pluginConfig);
|
||||
console.log(`[Hindsight] before_agent_start - bank: ${bankId}, channel: ${ctx?.messageProvider}/${ctx?.channelId}`);
|
||||
@@ -647,6 +654,12 @@ User message: ${prompt}
|
||||
// Use context from this hook, or fall back to context captured in before_agent_start
|
||||
const effectiveCtx = ctx || currentAgentContext;
|
||||
|
||||
// Check if this provider is excluded
|
||||
if (effectiveCtx?.messageProvider && pluginConfig.excludeProviders?.includes(effectiveCtx.messageProvider)) {
|
||||
console.log(`[Hindsight] Skipping retain for excluded provider: ${effectiveCtx.messageProvider}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Derive bank ID from context
|
||||
const bankId = deriveBankId(effectiveCtx, pluginConfig);
|
||||
console.log(`[Hindsight Hook] agent_end triggered - bank: ${bankId}`);
|
||||
|
||||
@@ -42,6 +42,7 @@ export interface PluginConfig {
|
||||
hindsightApiToken?: string; // API token for external Hindsight API authentication
|
||||
dynamicBankId?: boolean; // Enable per-channel memory banks (default: true)
|
||||
bankIdPrefix?: string; // Prefix for bank IDs (e.g. 'prod' -> 'prod-slack-C123')
|
||||
excludeProviders?: string[]; // Message providers to exclude from recall/retain (e.g. ['telegram', 'discord'])
|
||||
}
|
||||
|
||||
export interface ServiceConfig {
|
||||
|
||||
Executable
+280
@@ -0,0 +1,280 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Generate agent skill from Hindsight documentation
|
||||
# Converts docs/ to skills/hindsight-docs/ for AI agent consumption
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
DOCS_DIR="$ROOT_DIR/hindsight-docs/docs"
|
||||
EXAMPLES_DIR="$ROOT_DIR/hindsight-docs/examples"
|
||||
SKILL_DIR="$ROOT_DIR/skills/hindsight-docs"
|
||||
REFS_DIR="$SKILL_DIR/references"
|
||||
|
||||
# Colors
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
print_info() {
|
||||
echo -e "${GREEN}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
print_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
}
|
||||
|
||||
print_info "Generating Hindsight documentation skill..."
|
||||
|
||||
# Clean and recreate skill directory
|
||||
rm -rf "$SKILL_DIR"
|
||||
mkdir -p "$REFS_DIR"
|
||||
|
||||
# Process markdown files
|
||||
process_file() {
|
||||
local src_file="$1"
|
||||
local rel_path="${src_file#$DOCS_DIR/}"
|
||||
local dest_file="$REFS_DIR/$rel_path"
|
||||
|
||||
# Create destination directory
|
||||
mkdir -p "$(dirname "$dest_file")"
|
||||
|
||||
# Process the file
|
||||
if [[ "$src_file" == *.mdx ]]; then
|
||||
# Change .mdx to .md
|
||||
dest_file="${dest_file%.mdx}.md"
|
||||
print_info "Converting: $rel_path"
|
||||
convert_mdx_to_md "$src_file" "$dest_file"
|
||||
else
|
||||
print_info "Copying: $rel_path"
|
||||
cp "$src_file" "$dest_file"
|
||||
fi
|
||||
}
|
||||
|
||||
# Convert MDX to Markdown by:
|
||||
# 1. Removing import statements
|
||||
# 2. Replacing JSX components with markdown equivalents
|
||||
# 3. Inlining code examples from example files
|
||||
convert_mdx_to_md() {
|
||||
local src="$1"
|
||||
local dest="$2"
|
||||
|
||||
# Use Python for more robust processing
|
||||
python3 - "$src" "$dest" "$EXAMPLES_DIR" <<'PYTHON'
|
||||
import sys
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
src_file = Path(sys.argv[1])
|
||||
dest_file = Path(sys.argv[2])
|
||||
examples_dir = Path(sys.argv[3])
|
||||
|
||||
content = src_file.read_text()
|
||||
original_content = content # Keep original for import searches
|
||||
|
||||
# Remove frontmatter
|
||||
content = re.sub(r'^---\n.*?\n---\n', '', content, flags=re.DOTALL)
|
||||
|
||||
# Remove import statements
|
||||
content = re.sub(r'^import .*?;?\n', '', content, flags=re.MULTILINE)
|
||||
|
||||
# Extract code example inlining: <CodeSnippet code={varName} section="..." language="..." />
|
||||
# Replace with actual code content from examples directory
|
||||
def inline_code_snippet(match):
|
||||
var_name = match.group(1)
|
||||
section = match.group(2)
|
||||
language = match.group(3)
|
||||
|
||||
# Find the import that loaded this variable - search in original content
|
||||
import_match = re.search(rf"import {var_name} from '!!raw-loader!@site/(.+?)';", original_content)
|
||||
if not import_match:
|
||||
return f"```{language}\n# Could not find import for: {var_name}\n```"
|
||||
|
||||
# Load the example file
|
||||
# The import path is like "examples/api/quickstart.py", but examples_dir already points to examples/
|
||||
example_rel_path = import_match.group(1)
|
||||
# Strip "examples/" prefix if present since examples_dir already includes it
|
||||
if example_rel_path.startswith("examples/"):
|
||||
example_rel_path = example_rel_path[len("examples/"):]
|
||||
example_path = examples_dir / example_rel_path
|
||||
|
||||
if not example_path.exists():
|
||||
return f"```{language}\n# Example file not found: {example_path}\n```"
|
||||
|
||||
example_content = example_path.read_text()
|
||||
|
||||
# Extract section if specified - examples use comment markers like # [docs:section] or // [docs:section]
|
||||
if section:
|
||||
# Try various comment formats: #, //, etc.
|
||||
# Pattern: (comment) [docs:section] ... (comment) [/docs:section]
|
||||
section_pattern = rf"(?:^|\n)(?:#|//)\s*\[docs:{re.escape(section)}\]\n(.*?)\n(?:#|//)\s*\[/docs:{re.escape(section)}\]"
|
||||
section_match = re.search(section_pattern, example_content, re.DOTALL | re.MULTILINE)
|
||||
|
||||
if not section_match:
|
||||
# Try alternative # section-start / # section-end format
|
||||
section_pattern = rf"(?:^|\n)#\s*{re.escape(section)}-start\n(.*?)\n#\s*{re.escape(section)}-end"
|
||||
section_match = re.search(section_pattern, example_content, re.DOTALL | re.MULTILINE)
|
||||
|
||||
if section_match:
|
||||
example_content = section_match.group(1).strip()
|
||||
else:
|
||||
return f"```{language}\n# Section '{section}' not found in {example_rel_path}\n```"
|
||||
|
||||
return f"```{language}\n{example_content}\n```"
|
||||
|
||||
content = re.sub(
|
||||
r'<CodeSnippet code=\{(\w+)\} section="([^"]+)" language="([^"]+)" />',
|
||||
inline_code_snippet,
|
||||
content
|
||||
)
|
||||
|
||||
# Convert <Tabs> to markdown sections
|
||||
# Replace <Tabs> ... </Tabs> with markdown headers
|
||||
content = re.sub(r'<Tabs>\s*', '', content)
|
||||
content = re.sub(r'</Tabs>\s*', '', content)
|
||||
|
||||
# Convert <TabItem value="x" label="Y"> to ### Y
|
||||
content = re.sub(r'<TabItem value="[^"]*" label="([^"]+)">', r'### \1\n', content)
|
||||
content = re.sub(r'</TabItem>', '', content)
|
||||
|
||||
# Convert :::tip, :::warning, :::note to markdown blockquotes
|
||||
content = re.sub(r':::tip (.+?)\n', r'> **💡 \1**\n> \n', content)
|
||||
content = re.sub(r':::warning (.+?)\n', r'> **⚠️ \1**\n> \n', content)
|
||||
content = re.sub(r':::note (.+?)\n', r'> **📝 \1**\n> \n', content)
|
||||
content = re.sub(r':::\s*\n', '', content)
|
||||
|
||||
# Clean up extra blank lines
|
||||
content = re.sub(r'\n{3,}', '\n\n', content)
|
||||
|
||||
dest_file.write_text(content)
|
||||
PYTHON
|
||||
}
|
||||
|
||||
# Find and process all markdown files
|
||||
print_info "Processing documentation files..."
|
||||
find "$DOCS_DIR" -type f \( -name "*.md" -o -name "*.mdx" \) | while read -r file; do
|
||||
process_file "$file"
|
||||
done
|
||||
|
||||
# Generate SKILL.md
|
||||
print_info "Generating SKILL.md..."
|
||||
cat > "$SKILL_DIR/SKILL.md" <<'EOF'
|
||||
---
|
||||
name: hindsight-docs
|
||||
description: Complete Hindsight documentation for AI agents. Use this to learn about Hindsight architecture, APIs, configuration, and best practices.
|
||||
---
|
||||
|
||||
# Hindsight Documentation Skill
|
||||
|
||||
Complete technical documentation for Hindsight - a biomimetic memory system for AI agents.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when you need to:
|
||||
- Understand Hindsight architecture and core concepts
|
||||
- Learn about retain/recall/reflect operations
|
||||
- Configure memory banks and dispositions
|
||||
- Set up the Hindsight API server (Docker, Kubernetes, pip)
|
||||
- Integrate with Python/Node.js/Rust SDKs
|
||||
- Understand retrieval strategies (semantic, BM25, graph, temporal)
|
||||
- Debug issues or optimize performance
|
||||
- Review API endpoints and parameters
|
||||
- Find cookbook examples and recipes
|
||||
|
||||
## Documentation Structure
|
||||
|
||||
All documentation is in `references/` organized by category:
|
||||
|
||||
```
|
||||
references/
|
||||
├── developer/
|
||||
│ ├── api/ # Core operations: retain, recall, reflect, memory banks
|
||||
│ └── *.md # Architecture, configuration, deployment, performance
|
||||
├── sdks/
|
||||
│ ├── *.md # Python, Node.js, CLI, embedded
|
||||
│ └── integrations/ # LiteLLM, AI SDK, OpenClaw, MCP, skills
|
||||
└── cookbook/
|
||||
├── recipes/ # Usage patterns and examples
|
||||
└── applications/ # Full application demos
|
||||
```
|
||||
|
||||
## How to Find Documentation
|
||||
|
||||
### 1. Find Files by Pattern (use Glob tool)
|
||||
|
||||
```bash
|
||||
# Core API operations
|
||||
references/developer/api/*.md
|
||||
|
||||
# SDK documentation
|
||||
references/sdks/*.md
|
||||
references/sdks/integrations/*.md
|
||||
|
||||
# Cookbook examples
|
||||
references/cookbook/recipes/*.md
|
||||
references/cookbook/applications/*.md
|
||||
|
||||
# Find specific topics
|
||||
references/**/configuration.md
|
||||
references/**/*python*.md
|
||||
references/**/*deployment*.md
|
||||
```
|
||||
|
||||
### 2. Search Content (use Grep tool)
|
||||
|
||||
```bash
|
||||
# Search for concepts
|
||||
pattern: "disposition" # Memory bank configuration
|
||||
pattern: "graph retrieval" # Graph-based search
|
||||
pattern: "helm install" # Kubernetes deployment
|
||||
pattern: "document_id" # Document management
|
||||
pattern: "HINDSIGHT_API_" # Environment variables
|
||||
|
||||
# Search in specific areas
|
||||
path: references/developer/api/
|
||||
pattern: "POST /v1" # Find API endpoints
|
||||
|
||||
path: references/cookbook/
|
||||
pattern: "def |async def " # Find Python examples
|
||||
```
|
||||
|
||||
### 3. Read Full Documentation (use Read tool)
|
||||
|
||||
```
|
||||
references/developer/api/retain.md
|
||||
references/sdks/python.md
|
||||
references/cookbook/recipes/per-user-memory.md
|
||||
```
|
||||
|
||||
## Key Concepts
|
||||
|
||||
- **Memory Banks**: Isolated memory stores (one per user/agent)
|
||||
- **Retain**: Store memories (auto-extracts facts/entities/relationships)
|
||||
- **Recall**: Retrieve memories (4 parallel strategies: semantic, BM25, graph, temporal)
|
||||
- **Reflect**: Disposition-aware reasoning using memories
|
||||
- **document_id**: Groups messages in a conversation (upsert on same ID)
|
||||
- **Dispositions**: Skepticism, literalism, empathy traits (1-5) affecting reflect
|
||||
- **Mental Models**: Consolidated knowledge synthesized from facts
|
||||
|
||||
## Notes
|
||||
|
||||
- Code examples are inlined from working examples
|
||||
- Configuration uses `HINDSIGHT_API_*` environment variables
|
||||
- Database migrations run automatically on startup
|
||||
- Multi-bank queries require client-side orchestration
|
||||
- Use `document_id` for conversation evolution (same ID = upsert)
|
||||
|
||||
---
|
||||
|
||||
**Auto-generated** from `hindsight-docs/docs/`. Run `./scripts/generate-docs-skill.sh` to update.
|
||||
EOF
|
||||
|
||||
print_info "✓ Generated skill at: $SKILL_DIR"
|
||||
print_info "✓ Documentation files: $(find "$REFS_DIR" -type f | wc -l | tr -d ' ')"
|
||||
print_info "✓ SKILL.md created with search guidance"
|
||||
|
||||
echo ""
|
||||
print_info "Usage:"
|
||||
echo " - Agents can use Glob to find files: references/developer/api/*.md"
|
||||
echo " - Agents can use Grep to search content: pattern='disposition'"
|
||||
echo " - Agents can use Read to view full docs"
|
||||
@@ -120,6 +120,11 @@ else
|
||||
print_warn " To update ${MAJOR_MINOR} docs, use patch releases (e.g., ${MAJOR_MINOR}.1)"
|
||||
fi
|
||||
|
||||
# Generate documentation skill for AI agents
|
||||
echo ""
|
||||
print_info "Generating documentation skill for AI agents..."
|
||||
"$SCRIPT_DIR/generate-docs-skill.sh"
|
||||
|
||||
echo ""
|
||||
print_info "Next steps:"
|
||||
echo " 1. Review changes: git diff $DOCS_DIR"
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
---
|
||||
name: hindsight-docs
|
||||
description: Complete Hindsight documentation for AI agents. Use this to learn about Hindsight architecture, APIs, configuration, and best practices.
|
||||
---
|
||||
|
||||
# Hindsight Documentation Skill
|
||||
|
||||
Complete technical documentation for Hindsight - a biomimetic memory system for AI agents.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when you need to:
|
||||
- Understand Hindsight architecture and core concepts
|
||||
- Learn about retain/recall/reflect operations
|
||||
- Configure memory banks and dispositions
|
||||
- Set up the Hindsight API server (Docker, Kubernetes, pip)
|
||||
- Integrate with Python/Node.js/Rust SDKs
|
||||
- Understand retrieval strategies (semantic, BM25, graph, temporal)
|
||||
- Debug issues or optimize performance
|
||||
- Review API endpoints and parameters
|
||||
- Find cookbook examples and recipes
|
||||
|
||||
## Documentation Structure
|
||||
|
||||
All documentation is in `references/` organized by category:
|
||||
|
||||
```
|
||||
references/
|
||||
├── developer/
|
||||
│ ├── api/ # Core operations: retain, recall, reflect, memory banks
|
||||
│ └── *.md # Architecture, configuration, deployment, performance
|
||||
├── sdks/
|
||||
│ ├── *.md # Python, Node.js, CLI, embedded
|
||||
│ └── integrations/ # LiteLLM, AI SDK, OpenClaw, MCP, skills
|
||||
└── cookbook/
|
||||
├── recipes/ # Usage patterns and examples
|
||||
└── applications/ # Full application demos
|
||||
```
|
||||
|
||||
## How to Find Documentation
|
||||
|
||||
### 1. Find Files by Pattern (use Glob tool)
|
||||
|
||||
```bash
|
||||
# Core API operations
|
||||
references/developer/api/*.md
|
||||
|
||||
# SDK documentation
|
||||
references/sdks/*.md
|
||||
references/sdks/integrations/*.md
|
||||
|
||||
# Cookbook examples
|
||||
references/cookbook/recipes/*.md
|
||||
references/cookbook/applications/*.md
|
||||
|
||||
# Find specific topics
|
||||
references/**/configuration.md
|
||||
references/**/*python*.md
|
||||
references/**/*deployment*.md
|
||||
```
|
||||
|
||||
### 2. Search Content (use Grep tool)
|
||||
|
||||
```bash
|
||||
# Search for concepts
|
||||
pattern: "disposition" # Memory bank configuration
|
||||
pattern: "graph retrieval" # Graph-based search
|
||||
pattern: "helm install" # Kubernetes deployment
|
||||
pattern: "document_id" # Document management
|
||||
pattern: "HINDSIGHT_API_" # Environment variables
|
||||
|
||||
# Search in specific areas
|
||||
path: references/developer/api/
|
||||
pattern: "POST /v1" # Find API endpoints
|
||||
|
||||
path: references/cookbook/
|
||||
pattern: "def |async def " # Find Python examples
|
||||
```
|
||||
|
||||
### 3. Read Full Documentation (use Read tool)
|
||||
|
||||
```
|
||||
references/developer/api/retain.md
|
||||
references/sdks/python.md
|
||||
references/cookbook/recipes/per-user-memory.md
|
||||
```
|
||||
|
||||
## Key Concepts
|
||||
|
||||
- **Memory Banks**: Isolated memory stores (one per user/agent)
|
||||
- **Retain**: Store memories (auto-extracts facts/entities/relationships)
|
||||
- **Recall**: Retrieve memories (4 parallel strategies: semantic, BM25, graph, temporal)
|
||||
- **Reflect**: Disposition-aware reasoning using memories
|
||||
- **document_id**: Groups messages in a conversation (upsert on same ID)
|
||||
- **Dispositions**: Skepticism, literalism, empathy traits (1-5) affecting reflect
|
||||
- **Mental Models**: Consolidated knowledge synthesized from facts
|
||||
|
||||
## Notes
|
||||
|
||||
- Code examples are inlined from working examples
|
||||
- Configuration uses `HINDSIGHT_API_*` environment variables
|
||||
- Database migrations run automatically on startup
|
||||
- Multi-bank queries require client-side orchestration
|
||||
- Use `document_id` for conversation evolution (same ID = upsert)
|
||||
|
||||
---
|
||||
|
||||
**Auto-generated** from `hindsight-docs/docs/`. Run `./scripts/generate-docs-skill.sh` to update.
|
||||
@@ -0,0 +1,120 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# Chat Memory App
|
||||
|
||||
|
||||
:::info Complete Application
|
||||
This is a complete, runnable application demonstrating Hindsight integration.
|
||||
[**View source on GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/tree/main/applications/chat-memory)
|
||||
:::
|
||||
|
||||
|
||||
A demo chat application that uses Groq's `qwen/qwen3-32b` model with Hindsight for persistent per-user memory.
|
||||
|
||||
## Features
|
||||
|
||||
- 🧠 **Persistent Memory**: Each user gets their own memory bank that remembers conversations
|
||||
- 🚀 **Fast AI**: Powered by Groq's high-speed inference
|
||||
- 🎯 **Per-User Context**: Isolated memory per user with automatic context retrieval
|
||||
- 💬 **Real-time Chat**: Instant responses with memory-augmented context
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Start Hindsight API
|
||||
|
||||
First, start the Hindsight API server using Docker:
|
||||
|
||||
```bash
|
||||
export GROQ_API_KEY=your_groq_api_key_here
|
||||
|
||||
# Start Hindsight with Groq as the LLM provider
|
||||
docker run --rm -it --pull always -p 8888:8888 -p 9999:9999 \
|
||||
-e HINDSIGHT_API_LLM_PROVIDER=groq \
|
||||
-e HINDSIGHT_API_LLM_API_KEY=$GROQ_API_KEY \
|
||||
-e HINDSIGHT_API_LLM_MODEL="openai/gpt-oss-20b" \
|
||||
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
|
||||
ghcr.io/vectorize-io/hindsight:latest
|
||||
```
|
||||
|
||||
- **API**: http://localhost:8888
|
||||
- **Control Plane UI**: http://localhost:9999
|
||||
|
||||
### 2. Configure Environment
|
||||
|
||||
Copy your Groq API key to the environment file:
|
||||
|
||||
```bash
|
||||
# Update .env.local with your Groq API key
|
||||
echo "GROQ_API_KEY=your_groq_api_key_here" > .env.local
|
||||
echo "HINDSIGHT_API_URL=http://localhost:8888" >> .env.local
|
||||
```
|
||||
|
||||
If you don't have one, you can get a free Groq API key here: https://console.groq.com/home
|
||||
|
||||
### 3. Install Dependencies
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### 4. Run the App
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open http://localhost:3000 in your browser.
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **User Identity**: Each browser session gets a unique user ID
|
||||
2. **Memory Bank Creation**: First message creates a personal memory bank in Hindsight
|
||||
3. **Context Retrieval**: Before responding, relevant memories are retrieved
|
||||
4. **Memory Augmented Response**: Groq generates responses with memory context
|
||||
5. **Conversation Storage**: Each conversation is stored for future context
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
User Message
|
||||
↓
|
||||
Next.js API Route (/api/chat)
|
||||
↓
|
||||
Hindsight.recall() → Get relevant memories
|
||||
↓
|
||||
Groq API → Generate response with memory context
|
||||
↓
|
||||
Hindsight.retain() → Store conversation
|
||||
↓
|
||||
Response to User
|
||||
```
|
||||
|
||||
## Memory Bank Structure
|
||||
|
||||
Each user gets their own isolated memory bank with:
|
||||
- **Name**: "Chat Memory for [userId]"
|
||||
- **Background**: Conversational AI assistant context
|
||||
- **Disposition**: Empathetic (4), Low Skepticism (2), Balanced Literalism (3)
|
||||
|
||||
## Try It Out
|
||||
|
||||
1. **First Conversation**: Tell the assistant about yourself
|
||||
- "Hi! I'm a software engineer from San Francisco. I love Python and machine learning."
|
||||
|
||||
2. **Second Conversation**: Ask what it remembers
|
||||
- "What do you know about me?"
|
||||
- "What programming languages do I like?"
|
||||
|
||||
3. **Context Building**: Continue sharing preferences
|
||||
- "I prefer VS Code over other editors"
|
||||
- "I'm working on a React project"
|
||||
|
||||
4. **Memory Verification**: Visit the Hindsight Control Plane at http://localhost:9999 to see stored memories
|
||||
|
||||
## Development
|
||||
|
||||
- **Groq Model**: Uses `qwen/qwen3-32b` for fast, high-quality responses
|
||||
- **Memory Storage**: Automatic conversation retention with context categorization
|
||||
- **Memory Retrieval**: Semantic search with 2048 token budget for relevant context
|
||||
@@ -0,0 +1,145 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
# Deliveryman Demo
|
||||
|
||||
|
||||
:::info Complete Application
|
||||
This is a complete, runnable application demonstrating Hindsight integration.
|
||||
[**View source on GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/tree/main/applications/deliveryman-demo)
|
||||
:::
|
||||
|
||||
|
||||
A delivery agent simulation that demonstrates Hindsight's long-term memory capabilities. An AI agent navigates a multi-building office complex to deliver packages, learning employee locations and optimal paths over time through mental models.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python 3.11+
|
||||
- Node.js 18+
|
||||
- [uv](https://docs.astral.sh/uv/) (Python package manager)
|
||||
|
||||
## Setup (Fresh Environment)
|
||||
|
||||
### 1. Clone Repositories
|
||||
|
||||
```bash
|
||||
# Clone Hindsight (memory engine)
|
||||
git clone https://github.com/anthropics/hindsight.git
|
||||
|
||||
# Clone the cookbook (contains this demo)
|
||||
git clone https://github.com/anthropics/hindsight-cookbook.git
|
||||
```
|
||||
|
||||
### 2. Start Hindsight API
|
||||
|
||||
```bash
|
||||
cd hindsight
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Edit `.env` with your LLM configuration:
|
||||
|
||||
```bash
|
||||
HINDSIGHT_API_LLM_PROVIDER=groq
|
||||
HINDSIGHT_API_LLM_API_KEY=<your-groq-api-key>
|
||||
HINDSIGHT_API_LLM_MODEL=openai/gpt-oss-120b
|
||||
HINDSIGHT_API_HOST=0.0.0.0
|
||||
HINDSIGHT_API_PORT=8888
|
||||
HINDSIGHT_API_ENABLE_OBSERVATIONS=true
|
||||
|
||||
# Retain extraction settings (improves employee/location extraction)
|
||||
HINDSIGHT_API_RETAIN_EXTRACTION_MODE=custom
|
||||
HINDSIGHT_API_RETAIN_CUSTOM_INSTRUCTIONS="Delivery agent. Remember employee locations, building layout, and optimal paths."
|
||||
|
||||
# Embedded database storage
|
||||
PG0_DATA_DIR=/tmp/hindsight-data
|
||||
```
|
||||
|
||||
Start the API:
|
||||
|
||||
```bash
|
||||
./scripts/dev/start-api.sh
|
||||
# Runs on http://localhost:8888
|
||||
```
|
||||
|
||||
### 3. Start Hindsight Control Plane (Optional)
|
||||
|
||||
The control plane provides a web UI for inspecting memory banks, facts, and mental models.
|
||||
|
||||
```bash
|
||||
cd hindsight
|
||||
./scripts/dev/start-control-plane.sh
|
||||
# Runs on a dynamic port (check terminal output)
|
||||
```
|
||||
|
||||
### 4. Start Demo Backend
|
||||
|
||||
```bash
|
||||
cd hindsight-cookbook/deliveryman-demo/backend
|
||||
|
||||
# Create virtual environment and install dependencies
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
Create `backend/.env`:
|
||||
|
||||
```bash
|
||||
OPENAI_API_KEY=<your-openai-api-key>
|
||||
GROQ_API_KEY=<your-groq-api-key>
|
||||
HINDSIGHT_API_URL=http://localhost:8888
|
||||
LLM_MODEL=openai/gpt-4o
|
||||
```
|
||||
|
||||
Start the backend:
|
||||
|
||||
```bash
|
||||
./run.sh
|
||||
# Or manually:
|
||||
python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --ws wsproto --reload
|
||||
```
|
||||
|
||||
**Note:** The `--ws wsproto` flag is required for WebSocket support. Without it, connections will fail with error 1006.
|
||||
|
||||
### 5. Start Demo Frontend
|
||||
|
||||
```bash
|
||||
cd hindsight-cookbook/deliveryman-demo/frontend
|
||||
npm install
|
||||
npm run dev
|
||||
# Runs on http://localhost:5173
|
||||
```
|
||||
|
||||
### 6. Open the Demo
|
||||
|
||||
Navigate to http://localhost:5173 in your browser.
|
||||
|
||||
## How It Works
|
||||
|
||||
1. The agent receives a delivery task (e.g., "Deliver Package #3954 to Victor Huang")
|
||||
2. It navigates a multi-building complex with floors, elevators, and sky bridges
|
||||
3. Along the way it encounters employees and learns their locations
|
||||
4. After each delivery, the conversation is sent to Hindsight via the **retain** API
|
||||
5. Hindsight extracts facts (employee locations, building layout) and builds **mental models**
|
||||
6. On subsequent deliveries, the agent queries Hindsight to recall what it learned
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Browser (5173) → Frontend (React + Phaser)
|
||||
↓ WebSocket
|
||||
Backend (8000) → FastAPI + Delivery Agent
|
||||
↓ HTTP
|
||||
Hindsight API (8888) → Memory Engine + PostgreSQL
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Solution |
|
||||
|---------|----------|
|
||||
| WebSocket error 1006 | Restart backend with `--ws wsproto` flag |
|
||||
| Mental models missing employees | Check `HINDSIGHT_API_RETAIN_EXTRACTION_MODE=custom` is set |
|
||||
| Hindsight connection refused | Verify Hindsight API is running on port 8888 |
|
||||
| Frontend shows "Disconnected" | Check backend is running on port 8000 |
|
||||
@@ -0,0 +1,206 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
# Memory Approaches Comparison Demo
|
||||
|
||||
|
||||
:::info Complete Application
|
||||
This is a complete, runnable application demonstrating Hindsight integration.
|
||||
[**View source on GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/tree/main/applications/hindsight-litellm-demo)
|
||||
:::
|
||||
|
||||
|
||||
Interactive Streamlit app comparing three memory approaches for LLM applications:
|
||||
|
||||
1. **No Memory** - Each query is independent (baseline)
|
||||
2. **Full Conversation History** - Pass entire conversation (truncated to simulate context limits)
|
||||
3. **Hindsight Memory** - Intelligent semantic memory retrieval
|
||||
|
||||
This demo showcases how Hindsight's semantic memory outperforms traditional approaches, especially as conversations grow longer.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# 1. Set your OpenAI API key
|
||||
export OPENAI_API_KEY=your-key
|
||||
|
||||
# 2. Start Hindsight server
|
||||
docker run -d -p 8888:8888 -p 9999:9999 \
|
||||
-e HINDSIGHT_API_LLM_PROVIDER=openai \
|
||||
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
|
||||
ghcr.io/vectorize-io/hindsight:latest
|
||||
|
||||
# 3. Run the demo
|
||||
./run.sh
|
||||
```
|
||||
|
||||
Then open http://localhost:8501 in your browser.
|
||||
|
||||
## What This Demo Shows
|
||||
|
||||
### The Problem with Traditional Approaches
|
||||
|
||||
| Approach | How it Works | Limitation |
|
||||
|----------|--------------|------------|
|
||||
| **No Memory** | Each query standalone | Forgets everything between messages |
|
||||
| **Full History** | Pass all messages to LLM | Token limits cause truncation - loses early context |
|
||||
| **Hindsight** | Semantic retrieval of relevant facts | Retrieves what's relevant regardless of when it was said |
|
||||
|
||||
### Key Insight
|
||||
|
||||
After 5-10 messages, watch the **Full Conversation History** column start losing early context due to truncation (artificially set to 4 messages to demonstrate this quickly). Meanwhile, **Hindsight Memory** can still recall facts from the beginning because it uses semantic retrieval rather than sequential history.
|
||||
|
||||
## Testing the Demo
|
||||
|
||||
1. **Introduce yourself**:
|
||||
- "Hi, I'm Sarah, a data scientist at Netflix"
|
||||
- "I prefer Python and love machine learning"
|
||||
|
||||
2. **Have several exchanges** about different topics
|
||||
|
||||
3. **Test recall**:
|
||||
- "What programming language should I use?"
|
||||
- "What do you know about me?"
|
||||
|
||||
Watch how the three columns respond differently as the conversation grows.
|
||||
|
||||
## Features
|
||||
|
||||
- **Side-by-side comparison** of all three approaches
|
||||
- **Debug panels** showing what context each approach uses
|
||||
- **Memory explorer** to search Hindsight memories directly
|
||||
- **Configurable settings** for history truncation, max memories, etc.
|
||||
- **Multi-provider support** via LiteLLM (OpenAI, Anthropic, Groq)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python 3.10+
|
||||
- Hindsight server running (Docker recommended)
|
||||
- At least one LLM API key (OpenAI recommended)
|
||||
|
||||
## Setup
|
||||
|
||||
### Using run.sh (Recommended)
|
||||
|
||||
```bash
|
||||
# Set API key
|
||||
export OPENAI_API_KEY=your-key
|
||||
|
||||
# Start Hindsight, then run:
|
||||
./run.sh
|
||||
```
|
||||
|
||||
The script will check and install dependencies automatically.
|
||||
|
||||
### Manual Setup
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
pip install streamlit litellm
|
||||
|
||||
# Install Hindsight packages
|
||||
pip install hindsight-client hindsight-litellm
|
||||
|
||||
# Run the app
|
||||
streamlit run app.py
|
||||
```
|
||||
|
||||
### Starting Hindsight Server
|
||||
|
||||
```bash
|
||||
docker run -d -p 8888:8888 -p 9999:9999 \
|
||||
-e HINDSIGHT_API_LLM_PROVIDER=openai \
|
||||
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
|
||||
ghcr.io/vectorize-io/hindsight:latest
|
||||
|
||||
# Verify it's running
|
||||
curl http://localhost:8888/health
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Sidebar Options
|
||||
|
||||
**Model Selection:**
|
||||
- Provider: OpenAI, Anthropic, Groq
|
||||
- Model: Various models per provider
|
||||
- Custom model ID support
|
||||
|
||||
**Full History Config:**
|
||||
- Max Messages to Keep (default: 4 to demonstrate truncation)
|
||||
|
||||
**Hindsight Config:**
|
||||
- API URL (default: http://localhost:8888)
|
||||
- Bank ID and Entity ID for memory isolation
|
||||
- Max Memories to retrieve
|
||||
- Recall Budget (low/mid/high)
|
||||
|
||||
**Generation Settings:**
|
||||
- Temperature
|
||||
- Max Tokens
|
||||
- System Prompt
|
||||
|
||||
## Supported Models
|
||||
|
||||
### OpenAI
|
||||
- gpt-4o, gpt-4o-mini, gpt-4-turbo, gpt-4, gpt-3.5-turbo
|
||||
|
||||
### Anthropic
|
||||
- claude-3-5-sonnet-20241022, claude-3-5-haiku-20241022
|
||||
- claude-3-opus-20240229, claude-3-sonnet-20240229
|
||||
|
||||
### Groq
|
||||
- groq/llama-3.1-70b-versatile, groq/llama-3.1-8b-instant
|
||||
- groq/mixtral-8x7b-32768
|
||||
|
||||
## Environment Variables
|
||||
|
||||
```bash
|
||||
# Required
|
||||
export OPENAI_API_KEY=sk-...
|
||||
|
||||
# Optional (for other providers)
|
||||
export ANTHROPIC_API_KEY=sk-ant-...
|
||||
export GROQ_API_KEY=gsk_...
|
||||
|
||||
# Optional
|
||||
export HINDSIGHT_URL=http://localhost:8888
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Hindsight server not responding
|
||||
|
||||
```bash
|
||||
# Check if running
|
||||
curl http://localhost:8888/health
|
||||
|
||||
# Start with Docker
|
||||
docker run -d -p 8888:8888 -p 9999:9999 \
|
||||
-e HINDSIGHT_API_LLM_PROVIDER=openai \
|
||||
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
|
||||
ghcr.io/vectorize-io/hindsight:latest
|
||||
```
|
||||
|
||||
### hindsight-litellm not installed
|
||||
|
||||
```bash
|
||||
pip install hindsight-litellm
|
||||
```
|
||||
|
||||
### API key errors
|
||||
|
||||
Make sure the appropriate API key is set:
|
||||
```bash
|
||||
export OPENAI_API_KEY=your-key
|
||||
```
|
||||
|
||||
## Related
|
||||
|
||||
- [Hindsight](https://github.com/vectorize-io/hindsight) - Memory infrastructure for AI applications
|
||||
- [hindsight-litellm](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/litellm) - LiteLLM integration package
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
---
|
||||
|
||||
# Tool Learning Demo
|
||||
|
||||
|
||||
:::info Complete Application
|
||||
This is a complete, runnable application demonstrating Hindsight integration.
|
||||
[**View source on GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/tree/main/applications/hindsight-tool-learning-demo)
|
||||
:::
|
||||
|
||||
|
||||
An interactive Streamlit demo showing how Hindsight helps LLMs learn which tool to use when tool names are ambiguous.
|
||||
|
||||
## The Problem
|
||||
|
||||
When building AI agents with tool/function calling, tool names and descriptions aren't always clear. An LLM might randomly select between similarly-named tools, leading to incorrect behavior.
|
||||
|
||||
## The Scenario
|
||||
|
||||
This demo simulates a **customer service routing system** with two channels:
|
||||
|
||||
| Tool | Description (What the LLM sees) | Actual Purpose (Hidden) |
|
||||
|------|--------------------------------|------------------------|
|
||||
| `route_to_channel_alpha` | "Routes to channel Alpha for appropriate request types" | Financial issues (refunds, billing, payments) |
|
||||
| `route_to_channel_omega` | "Routes to channel Omega for appropriate request types" | Technical issues (bugs, features, errors) |
|
||||
|
||||
The descriptions are **intentionally vague**! Without prior knowledge, the LLM must guess which channel handles what.
|
||||
|
||||
## The Solution: Learning with Hindsight
|
||||
|
||||
With Hindsight memory:
|
||||
1. **Store routing feedback** about which channel handles which request type
|
||||
2. **Retrieve learned knowledge** when making routing decisions
|
||||
3. **Consistently route correctly** based on past experience
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. **Hindsight Server** running (Docker):
|
||||
```bash
|
||||
docker run -d -p 8888:8888 -p 9999:9999 \
|
||||
-e HINDSIGHT_API_LLM_PROVIDER=openai \
|
||||
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
|
||||
-e HINDSIGHT_API_LLM_MODEL=gpt-4o-mini \
|
||||
ghcr.io/vectorize-io/hindsight:latest
|
||||
```
|
||||
|
||||
2. **OpenAI API Key**:
|
||||
```bash
|
||||
export OPENAI_API_KEY=your-key-here
|
||||
```
|
||||
|
||||
### Run the Demo
|
||||
|
||||
```bash
|
||||
./run.sh
|
||||
```
|
||||
|
||||
Or manually:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
streamlit run app.py
|
||||
```
|
||||
|
||||
## How to Use the Demo
|
||||
|
||||
### Step 1: Test Without Memory (Baseline)
|
||||
|
||||
1. Select a **Financial Request** (e.g., "I need a refund...")
|
||||
2. Click **Route Request**
|
||||
3. Observe: The "Without Hindsight" column may route incorrectly
|
||||
|
||||
### Step 2: Route First Customer and Learn
|
||||
|
||||
1. Route a customer → Both LLMs route simultaneously
|
||||
2. Feedback is automatically stored to Hindsight
|
||||
3. Wait ~5 seconds for Hindsight to index the memory
|
||||
|
||||
### Step 3: Test With Memory
|
||||
|
||||
1. Select another request (financial or technical)
|
||||
2. Click **Route Request**
|
||||
3. Observe: The "With Hindsight" column should now route correctly!
|
||||
|
||||
### Step 4: View Statistics
|
||||
|
||||
- See accuracy comparison between "Without Memory" vs "With Hindsight"
|
||||
- Review test history to see the improvement over time
|
||||
|
||||
## Demo Features
|
||||
|
||||
- **Side-by-side comparison**: See routing results with and without memory
|
||||
- **Pre-defined test requests**: Financial and technical scenarios
|
||||
- **Custom requests**: Enter your own customer requests
|
||||
- **Memory Explorer**: Query stored routing knowledge directly
|
||||
- **Live statistics**: Track accuracy improvement
|
||||
|
||||
## Key Insight
|
||||
|
||||
> Even when tool names and descriptions don't reveal their purpose, Hindsight allows the LLM to **learn from experience** which tool to use for which type of request.
|
||||
|
||||
This is especially valuable for:
|
||||
- Enterprise systems with legacy tool names
|
||||
- Multi-tenant systems where tools have generic names
|
||||
- Agents that need to learn organization-specific workflows
|
||||
|
||||
## Configuration
|
||||
|
||||
| Setting | Default | Description |
|
||||
|---------|---------|-------------|
|
||||
| Model | gpt-4o-mini | LLM model for routing decisions |
|
||||
| Temperature (No Memory) | 0.7 | Randomness for baseline tests |
|
||||
| Hindsight API URL | http://localhost:8888 | Hindsight server URL |
|
||||
|
||||
## Files
|
||||
|
||||
- `app.py` - Main Streamlit application
|
||||
- `requirements.txt` - Python dependencies
|
||||
- `run.sh` - Launch script with dependency checking
|
||||
- `README.md` - This file
|
||||
@@ -0,0 +1,315 @@
|
||||
---
|
||||
sidebar_position: 5
|
||||
---
|
||||
|
||||
# OpenAI Agent + Hindsight Memory Integration
|
||||
|
||||
|
||||
:::info Complete Application
|
||||
This is a complete, runnable application demonstrating Hindsight integration.
|
||||
[**View source on GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/tree/main/applications/openai-fitness-coach)
|
||||
:::
|
||||
|
||||
|
||||
A fitness coach example demonstrating how to use **OpenAI Agents** with **Hindsight as a memory backend**.
|
||||
|
||||
## What This Demonstrates
|
||||
|
||||
This example showcases:
|
||||
|
||||
- **OpenAI Assistants** handling conversation logic
|
||||
- **Hindsight** providing sophisticated memory storage & retrieval
|
||||
- **Function calling** to bridge them together
|
||||
- **Streaming responses** for real-time interaction (enabled by default)
|
||||
- **Bidirectional memory** - both user data AND coach observations stored
|
||||
- **System-level post-processing** - automatic opinion storage for reliability
|
||||
- **Temporal-semantic memory** queries via function tools
|
||||
- **Enhanced preference learning** - coach learns and respects user likes/dislikes
|
||||
- **Real-world integration pattern** for adding memory to AI agents
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
User: "I ran 5K today, don't like tempo runs"
|
||||
|
|
||||
OpenAI Assistant
|
||||
|
|
||||
Function Call: store_memory(workout + preference)
|
||||
|
|
||||
Hindsight API (stores as world/agent)
|
||||
|
|
||||
OpenAI Assistant: "What should I focus on?"
|
||||
|
|
||||
Function Call: retrieve_memories("workouts and preferences")
|
||||
|
|
||||
Hindsight API (returns workouts + preferences)
|
||||
|
|
||||
OpenAI Assistant (analyzes, gives advice)
|
||||
|
|
||||
Function Call: store_memory(advice as opinion)
|
||||
|
|
||||
Hindsight API (stores coach's observation)
|
||||
|
|
||||
Personalized Answer
|
||||
```
|
||||
|
||||
## Key Difference from Standard Demo
|
||||
|
||||
| Component | Standard Demo | OpenAI Integration |
|
||||
|-----------|---------------|-------------------|
|
||||
| **Conversation** | Hindsight `/think` endpoint | OpenAI Assistant API |
|
||||
| **Memory** | Hindsight (built-in) | Hindsight (via function calling) |
|
||||
| **LLM** | Configured in Hindsight | OpenAI GPT-4 |
|
||||
| **Opinion Formation** | Automatic in `/think` | Explicit via `store_memory(type="opinion")` |
|
||||
| **Best For** | Hindsight-native apps | Integrating memory into existing OpenAI agents |
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. **OpenAI API Key**
|
||||
```bash
|
||||
export OPENAI_API_KEY=your_openai_api_key
|
||||
```
|
||||
|
||||
2. **Hindsight API running**
|
||||
```bash
|
||||
# Follow Hindsight setup instructions to start the API
|
||||
# Default: http://localhost:8888
|
||||
```
|
||||
|
||||
3. **Install dependencies**
|
||||
```bash
|
||||
pip install openai requests
|
||||
```
|
||||
|
||||
### Run the Conversational Demo
|
||||
|
||||
```bash
|
||||
cd openai-fitness-coach
|
||||
export OPENAI_API_KEY=your_key_here
|
||||
python demo_conversational.py
|
||||
```
|
||||
|
||||
The demo showcases:
|
||||
1. **Natural language workout logging** - Tell the coach what you did conversationally
|
||||
2. **Preference learning** - Express likes/dislikes and watch the coach adapt
|
||||
3. **Goal tracking** - Set goals, track progress, achieve milestones
|
||||
4. **Bidirectional memory** - Both your activities AND coach's advice are stored
|
||||
5. **Streaming responses** - See responses appear in real-time
|
||||
6. **7 interactive phases** - From goal setting to achievement recognition
|
||||
|
||||
The demo uses a separate agent (`fitness-coach-demo`) to avoid mixing with real data.
|
||||
|
||||
## Usage
|
||||
|
||||
### Chat with Your Coach
|
||||
|
||||
**Interactive mode:**
|
||||
```bash
|
||||
python openai_coach.py
|
||||
```
|
||||
|
||||
**Single question:**
|
||||
```bash
|
||||
python openai_coach.py "What did I do for training this week?"
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
### 1. Memory Tools (`memory_tools.py`)
|
||||
|
||||
Defines function tools that the OpenAI Agent can call:
|
||||
|
||||
```python
|
||||
retrieve_memories(query, fact_types, top_k)
|
||||
search_workouts(after_date, before_date, workout_type)
|
||||
get_nutrition_summary(after_date, before_date)
|
||||
get_user_goals()
|
||||
get_coach_opinions(about)
|
||||
```
|
||||
|
||||
Each function makes API calls to Hindsight to fetch relevant memories.
|
||||
|
||||
### 2. OpenAI Agent (`openai_coach.py`)
|
||||
|
||||
Creates an OpenAI Assistant with:
|
||||
- Fitness coaching instructions
|
||||
- Access to memory function tools
|
||||
- Conversation management
|
||||
|
||||
When you ask a question:
|
||||
1. User message is sent to OpenAI Assistant
|
||||
2. Assistant decides which memory functions to call
|
||||
3. Functions fetch data from Hindsight
|
||||
4. Assistant generates response using retrieved context
|
||||
|
||||
### 3. Function Calling Flow
|
||||
|
||||
```python
|
||||
# User asks: "What did I run this week?"
|
||||
|
||||
# OpenAI Assistant decides to call:
|
||||
search_workouts(
|
||||
after_date="2024-11-18",
|
||||
workout_type="running"
|
||||
)
|
||||
|
||||
# Function retrieves from Hindsight:
|
||||
{
|
||||
"results": [
|
||||
{"text": "User completed 45-minute cardio workout: running..."},
|
||||
{"text": "User completed 60-minute cardio workout: running..."}
|
||||
]
|
||||
}
|
||||
|
||||
# OpenAI Assistant generates response:
|
||||
"This week you've done two runs: a 45-minute run on Monday
|
||||
and a longer 60-minute run on Wednesday. Great consistency!"
|
||||
```
|
||||
|
||||
## Example Questions
|
||||
|
||||
Try asking:
|
||||
|
||||
```bash
|
||||
python openai_coach.py "What does my training look like this week?"
|
||||
python openai_coach.py "Based on my workouts, should I rest today?"
|
||||
python openai_coach.py "How is my nutrition supporting my goals?"
|
||||
python openai_coach.py "What's my progress toward my goal?"
|
||||
python openai_coach.py "Compare my training this month to last month"
|
||||
```
|
||||
|
||||
The agent will automatically:
|
||||
1. Identify what memories it needs
|
||||
2. Call the appropriate function tools
|
||||
3. Retrieve data from Hindsight
|
||||
4. Generate a personalized response
|
||||
|
||||
## Memory Types Retrieved
|
||||
|
||||
The OpenAI Agent can retrieve different memory types from Hindsight:
|
||||
|
||||
- **World Facts** (`fact_type: "world"`): Workouts, meals, activities
|
||||
- **Agent Facts** (`fact_type: "agent"`): Goals, intentions
|
||||
- **Opinions** (`fact_type: "opinion"`): Coach's observations about patterns
|
||||
|
||||
## Customization
|
||||
|
||||
### Add New Function Tools
|
||||
|
||||
Edit `memory_tools.py` to add new capabilities:
|
||||
|
||||
```python
|
||||
def get_weekly_summary(week_offset: int = 0):
|
||||
"""Get a summary of a specific week."""
|
||||
# Implementation
|
||||
pass
|
||||
|
||||
# Add to MEMORY_TOOLS list
|
||||
MEMORY_TOOLS.append({
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weekly_summary",
|
||||
"description": "Get training summary for a specific week",
|
||||
# ... parameters
|
||||
}
|
||||
})
|
||||
|
||||
# Add to FUNCTION_MAP
|
||||
FUNCTION_MAP["get_weekly_summary"] = get_weekly_summary
|
||||
```
|
||||
|
||||
### Modify Assistant Instructions
|
||||
|
||||
Edit `openai_coach.py` to change the coach's personality or behavior:
|
||||
|
||||
```python
|
||||
assistant = client.beta.assistants.create(
|
||||
name="Your Custom Coach",
|
||||
instructions="Your custom instructions here...",
|
||||
model="gpt-4o-mini",
|
||||
tools=MEMORY_TOOLS
|
||||
)
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
This pattern works for any application that needs memory:
|
||||
|
||||
1. **Customer Support Agents** - Remember past conversations and issues
|
||||
2. **Personal Assistants** - Remember preferences, schedules, past decisions
|
||||
3. **Educational Tutors** - Track learning progress over time
|
||||
4. **Health Coaches** - Monitor habits, progress, goals (like this example)
|
||||
5. **Sales Assistants** - Remember customer interactions and preferences
|
||||
|
||||
## Integration Pattern
|
||||
|
||||
**To add Hindsight memory to your own OpenAI Agent:**
|
||||
|
||||
1. Define function tools that call Hindsight API
|
||||
2. Register them with your OpenAI Assistant
|
||||
3. Implement function handlers to execute Hindsight queries
|
||||
4. Let OpenAI Assistant decide when to retrieve memories
|
||||
|
||||
The key benefit: **Separation of concerns**
|
||||
- OpenAI = Conversation logic
|
||||
- Hindsight = Memory storage, retrieval, temporal queries, entity linking
|
||||
|
||||
## When to Use This vs. Standard Hindsight
|
||||
|
||||
**Use OpenAI + Hindsight (this example) when:**
|
||||
- You want OpenAI's conversation capabilities
|
||||
- You're already using OpenAI Agents
|
||||
- You want explicit control over when to retrieve memories
|
||||
- You want to combine Hindsight with other OpenAI features
|
||||
|
||||
**Use Hindsight directly when:**
|
||||
- You want a complete memory-first solution
|
||||
- You want automatic memory retrieval and opinion formation
|
||||
- You want to use different LLM providers (not just OpenAI)
|
||||
- You want the `/think` endpoint's integrated approach
|
||||
|
||||
## Learning Points
|
||||
|
||||
After running this demo, you'll understand:
|
||||
|
||||
1. How to add sophisticated memory to any OpenAI Agent
|
||||
2. How function calling bridges LLMs and memory systems
|
||||
3. How temporal-semantic queries work via function tools
|
||||
4. Real-world pattern for LLM + memory integration
|
||||
|
||||
## Core Files
|
||||
|
||||
- `demo_conversational.py` - Conversational demo showcasing preference learning and goal tracking
|
||||
- `openai_coach.py` - OpenAI Assistant wrapper with streaming and memory integration
|
||||
- `memory_tools.py` - Function calling tools that bridge to Hindsight API
|
||||
- `.openai_assistant_id` - Saved assistant ID (auto-generated, gitignored)
|
||||
|
||||
## Common Issues
|
||||
|
||||
**"OPENAI_API_KEY not set"**
|
||||
```bash
|
||||
export OPENAI_API_KEY=your_api_key_here
|
||||
```
|
||||
|
||||
**"Agent not found"**
|
||||
- Make sure the Hindsight fitness-coach agent exists
|
||||
|
||||
**"Connection refused"**
|
||||
- Make sure Hindsight API is running on localhost:8888
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Run the demo to see it in action
|
||||
2. Try chatting with the coach: `python openai_coach.py`
|
||||
3. Log your own workouts and meals
|
||||
4. Experiment with different questions
|
||||
5. Add custom function tools for your use case
|
||||
|
||||
---
|
||||
|
||||
**Built with:**
|
||||
- OpenAI Assistants API
|
||||
- Hindsight (temporal-semantic memory)
|
||||
- Function calling for integration
|
||||
@@ -0,0 +1,371 @@
|
||||
---
|
||||
sidebar_position: 6
|
||||
---
|
||||
|
||||
# Sanity CMS Blog Memory
|
||||
|
||||
|
||||
:::info Complete Application
|
||||
This is a complete, runnable application demonstrating Hindsight integration.
|
||||
[**View source on GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/tree/main/applications/sanity-blog-memory)
|
||||
:::
|
||||
|
||||
|
||||
A Hindsight cookbook recipe demonstrating how to sync blog posts from **Sanity CMS** to Hindsight agent memory, enabling semantic search, temporal queries, and AI-powered content insights.
|
||||
|
||||
## Features
|
||||
|
||||
- **Blog Post Sync**: Automatically sync all blog posts from Sanity to Hindsight
|
||||
- **Document-based Upsert**: Idempotent syncing with `document_id` - re-running sync updates existing content
|
||||
- **Semantic Search**: Find related content using natural language queries
|
||||
- **Temporal Queries**: Ask "What did I write in January 2025?"
|
||||
- **Reflect for Insights**: Generate AI-powered analysis of your blog content
|
||||
- **Related Content Discovery**: Power "Related Posts" features with semantic similarity
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ │ │ │ │ │
|
||||
│ Sanity CMS │───────▶│ Sync Script │───────▶│ Hindsight │
|
||||
│ (Content) │ GROQ │ (TypeScript) │ HTTP │ (Memory) │
|
||||
│ │ │ │ │ │
|
||||
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ │
|
||||
│ Your App │
|
||||
│ - Recall │
|
||||
│ - Reflect │
|
||||
│ │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Start Hindsight
|
||||
|
||||
Choose your preferred LLM provider:
|
||||
|
||||
**Option A: Using Docker Compose (Recommended)**
|
||||
|
||||
```bash
|
||||
# Set your API key
|
||||
export OPENAI_API_KEY=sk-...
|
||||
# OR
|
||||
export GOOGLE_API_KEY=... # Gemini (free tier available)
|
||||
# OR
|
||||
export GROQ_API_KEY=... # Groq (free tier available)
|
||||
|
||||
# Start Hindsight
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
**Option B: Using Docker directly**
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY=sk-...
|
||||
|
||||
docker run --rm -it --pull always -p 8888:8888 -p 9999:9999 \
|
||||
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
|
||||
-e HINDSIGHT_API_LLM_MODEL=gpt-4o-mini \
|
||||
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
|
||||
ghcr.io/vectorize-io/hindsight:latest
|
||||
```
|
||||
|
||||
- **API**: http://localhost:8888
|
||||
- **Control Plane UI**: http://localhost:9999
|
||||
|
||||
### 2. Configure Environment
|
||||
|
||||
```bash
|
||||
# Copy example config
|
||||
cp .env.example .env
|
||||
|
||||
# Edit with your values
|
||||
nano .env
|
||||
```
|
||||
|
||||
Required settings:
|
||||
```bash
|
||||
# Hindsight
|
||||
HINDSIGHT_API_URL=http://localhost:8888
|
||||
HINDSIGHT_BANK_ID=blog-memory
|
||||
|
||||
# Sanity CMS
|
||||
SANITY_PROJECT_ID=your-project-id
|
||||
SANITY_DATASET=production
|
||||
```
|
||||
|
||||
### 3. Install Dependencies
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### 4. Sync Your Blog Posts
|
||||
|
||||
```bash
|
||||
npm run sync
|
||||
```
|
||||
|
||||
Expected output:
|
||||
```
|
||||
=======================================
|
||||
Sanity -> Hindsight Blog Sync
|
||||
=======================================
|
||||
|
||||
Setting up memory bank...
|
||||
Memory bank "blog-memory" ready
|
||||
|
||||
Fetching posts from Sanity CMS...
|
||||
Found 10 posts to sync
|
||||
|
||||
Syncing posts to Hindsight...
|
||||
[1/10] "Why I Chose Qwik"... done
|
||||
[2/10] "Building AI Agents"... done
|
||||
...
|
||||
|
||||
=======================================
|
||||
Sync Complete
|
||||
=======================================
|
||||
Synced: 10 posts
|
||||
```
|
||||
|
||||
### 5. Query Your Content
|
||||
|
||||
```bash
|
||||
npm run query
|
||||
```
|
||||
|
||||
## Query Examples
|
||||
|
||||
### Semantic Search
|
||||
|
||||
Find related content using natural language:
|
||||
|
||||
```typescript
|
||||
import { recallMemory } from './hindsight-client.js';
|
||||
|
||||
// Find posts about AI agents
|
||||
const result = await recallMemory('AI agents and automation', {
|
||||
budget: 'mid',
|
||||
maxTokens: 2048,
|
||||
});
|
||||
|
||||
console.log(`Found ${result.results.length} relevant posts`);
|
||||
```
|
||||
|
||||
### Temporal Queries
|
||||
|
||||
Ask about content from specific time periods:
|
||||
|
||||
```typescript
|
||||
// Posts from January 2025
|
||||
const result = await recallMemory('What did I write about in January 2025?', {
|
||||
queryTimestamp: '2025-01-31T23:59:59Z',
|
||||
});
|
||||
```
|
||||
|
||||
### Reflect for Insights
|
||||
|
||||
Generate AI-powered analysis of your content:
|
||||
|
||||
```typescript
|
||||
import { reflectOnMemory } from './hindsight-client.js';
|
||||
|
||||
// Analyze blog themes
|
||||
const insights = await reflectOnMemory(
|
||||
'What are the main themes of my blog? What topics do I write about most?',
|
||||
{ budget: 'high' }
|
||||
);
|
||||
|
||||
console.log(insights.text);
|
||||
```
|
||||
|
||||
### Related Content Discovery
|
||||
|
||||
Power your "Related Posts" feature:
|
||||
|
||||
```typescript
|
||||
// Find posts similar to a specific article
|
||||
const related = await recallMemory(
|
||||
'Find posts related to "Why I Chose Qwik for My Personal Website"',
|
||||
{ budget: 'mid' }
|
||||
);
|
||||
```
|
||||
|
||||
## Memory Structure
|
||||
|
||||
Each blog post is stored with rich metadata for optimal recall:
|
||||
|
||||
```
|
||||
# Blog Post: {title}
|
||||
|
||||
**Published:** {date}
|
||||
**URL:** {base_url}/blog/{slug}
|
||||
**Tags:** {tags}
|
||||
**Reading Time:** {reading_time}
|
||||
|
||||
## Description
|
||||
{description}
|
||||
|
||||
## Content
|
||||
{full_content}
|
||||
```
|
||||
|
||||
Key features:
|
||||
- **document_id**: `post:{slug}` - Enables upsert on re-sync
|
||||
- **context**: `blog-post` - Categorizes the memory type
|
||||
- **timestamp**: Post publication date - Enables temporal queries
|
||||
|
||||
## Use Cases
|
||||
|
||||
### 1. AI-Powered Blog Search
|
||||
|
||||
Replace keyword search with semantic understanding:
|
||||
|
||||
```typescript
|
||||
// Old: keyword matching
|
||||
const results = posts.filter(p => p.title.includes('React'));
|
||||
|
||||
// New: semantic understanding
|
||||
const result = await recallMemory('frontend framework tutorials');
|
||||
```
|
||||
|
||||
### 2. Content Recommendation Engine
|
||||
|
||||
Generate personalized recommendations:
|
||||
|
||||
```typescript
|
||||
const recommendations = await reflectOnMemory(
|
||||
'Based on a reader interested in "AI automation", recommend related posts'
|
||||
);
|
||||
```
|
||||
|
||||
### 3. Writing Assistant
|
||||
|
||||
Get topic suggestions based on your existing content:
|
||||
|
||||
```typescript
|
||||
const suggestions = await reflectOnMemory(
|
||||
'What topics should I write about next? What gaps exist in my content?'
|
||||
);
|
||||
```
|
||||
|
||||
### 4. Content Analytics
|
||||
|
||||
Analyze your blog's evolution:
|
||||
|
||||
```typescript
|
||||
const analysis = await reflectOnMemory(
|
||||
'How have my writing topics evolved over the past year?'
|
||||
);
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `HINDSIGHT_API_URL` | Hindsight API endpoint | `http://localhost:8888` |
|
||||
| `HINDSIGHT_BANK_ID` | Memory bank identifier | `blog-memory` |
|
||||
| `SANITY_PROJECT_ID` | Your Sanity project ID | (required) |
|
||||
| `SANITY_DATASET` | Sanity dataset name | `production` |
|
||||
| `SANITY_API_TOKEN` | Sanity API token (for private datasets) | (none) |
|
||||
| `SANITY_API_VERSION` | Sanity API version | `2024-01-09` |
|
||||
| `SITE_URL` | Your blog's base URL | `https://example.com` |
|
||||
|
||||
### Memory Bank Disposition
|
||||
|
||||
The memory bank is configured with disposition traits optimized for blog content:
|
||||
|
||||
```typescript
|
||||
{
|
||||
skepticism: 2, // Trusting - blog content is authoritative
|
||||
literalism: 4, // Literal - exact content matters
|
||||
empathy: 3, // Balanced
|
||||
}
|
||||
```
|
||||
|
||||
## Extending for Other CMS Platforms
|
||||
|
||||
This pattern can be adapted for any CMS. The key components:
|
||||
|
||||
### 1. CMS Client
|
||||
|
||||
Replace `sanity-client.ts` with your CMS:
|
||||
|
||||
```typescript
|
||||
// contentful-client.ts
|
||||
import { createClient } from 'contentful';
|
||||
|
||||
export async function getAllPosts(): Promise<BlogPost[]> {
|
||||
const client = createClient({...});
|
||||
const entries = await client.getEntries({ content_type: 'blogPost' });
|
||||
return entries.items.map(transformPost);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Content Transformation
|
||||
|
||||
Ensure your content is formatted for semantic search:
|
||||
|
||||
```typescript
|
||||
function formatPostContent(post: BlogPost): string {
|
||||
return `# ${post.title}
|
||||
|
||||
**Published:** ${post.date}
|
||||
...
|
||||
${post.content}`;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Document ID Strategy
|
||||
|
||||
Use a consistent document ID for upsert behavior:
|
||||
|
||||
```typescript
|
||||
await retainBlogPost(content, {
|
||||
documentId: `post:${post.slug}`, // Unique, stable identifier
|
||||
timestamp: post.date,
|
||||
});
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Connection refused" error
|
||||
|
||||
Make sure Hindsight is running:
|
||||
```bash
|
||||
docker compose up -d
|
||||
curl http://localhost:8888/health
|
||||
```
|
||||
|
||||
### "No posts found" during sync
|
||||
|
||||
Check your Sanity configuration:
|
||||
```bash
|
||||
# Verify project ID
|
||||
echo $SANITY_PROJECT_ID
|
||||
|
||||
# Test GROQ query
|
||||
npx sanity query '*[_type == "post"][0..2]{title}'
|
||||
```
|
||||
|
||||
### Slow recall/reflect responses
|
||||
|
||||
This is normal for the first query as Hindsight builds embeddings. Subsequent queries are faster. Use `budget: 'low'` for faster responses at the cost of recall quality.
|
||||
|
||||
## Resources
|
||||
|
||||
- [Hindsight Documentation](https://hindsight.vectorize.io/)
|
||||
- [Hindsight GitHub](https://github.com/vectorize-io/hindsight)
|
||||
- [Sanity CMS Documentation](https://www.sanity.io/docs)
|
||||
- [Hindsight Cookbook](https://github.com/vectorize-io/hindsight-cookbook)
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -0,0 +1,276 @@
|
||||
---
|
||||
sidebar_position: 7
|
||||
---
|
||||
|
||||
# Stance Tracker
|
||||
|
||||
|
||||
:::info Complete Application
|
||||
This is a complete, runnable application demonstrating Hindsight integration.
|
||||
[**View source on GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/tree/main/applications/stancetracker)
|
||||
:::
|
||||
|
||||
|
||||
An AI-powered application that tracks political candidates' stances on issues over time using Hindsight memory system and web scraping.
|
||||
|
||||
## Features
|
||||
|
||||
- **Geographic Targeting**: Track stances by country, state/province, and city
|
||||
- **Multi-Candidate Tracking**: Monitor multiple candidates simultaneously
|
||||
- **Temporal Analysis**: Historical stance tracking with configurable time ranges
|
||||
- **Automated Scraping**: Periodic content collection with configurable frequencies (hourly/daily/weekly)
|
||||
- **Stance Change Detection**: Automatic detection and highlighting of position changes
|
||||
- **Interactive Timeline**: Visual graph showing stance evolution with reference callouts
|
||||
- **Source Attribution**: All stances linked to verified sources with excerpts
|
||||
|
||||
## Architecture
|
||||
|
||||
### Memory System (Hindsight Integration)
|
||||
|
||||
This app uses the Hindsight memory system from `github.com/vectorize-io/hindsight`:
|
||||
|
||||
1. **Banks**: Each scraper agent has its own memory bank
|
||||
2. **Retain**: Stores candidate statements and web scraping results
|
||||
3. **Recall**: Semantic search to retrieve relevant memories
|
||||
4. **Reflect**: Generates contextual analysis using stored memories
|
||||
5. **Temporal Search**: Queries memories within specific time periods
|
||||
|
||||
### Tech Stack
|
||||
|
||||
- **Frontend**: Next.js 16, React, TypeScript, TailwindCSS
|
||||
- **Visualization**: Recharts for timeline graphs
|
||||
- **Backend**: Next.js API routes
|
||||
- **Memory**: Hindsight (from github.com/vectorize-io/hindsight)
|
||||
- **Database**: JSON file storage (no database required)
|
||||
- **Web Search**: Tavily API
|
||||
- **LLM**: OpenAI/Anthropic/Groq (configurable)
|
||||
- **Scheduling**: node-cron
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Hindsight API** running (from github.com/vectorize-io/hindsight)
|
||||
2. **API Keys**:
|
||||
- Tavily API key (for web search)
|
||||
- LLM provider API key (OpenAI, Anthropic, or Groq)
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Install Dependencies
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### 2. Configure Environment
|
||||
|
||||
Copy `.env.example` to `.env` and fill in your credentials:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Edit `.env`:
|
||||
|
||||
```env
|
||||
# Hindsight API (from github.com/vectorize-io/hindsight)
|
||||
HINDSIGHT_API_URL=http://localhost:8888
|
||||
|
||||
# Tavily API (for web search)
|
||||
TAVILY_API_KEY=your_tavily_api_key_here
|
||||
|
||||
# LLM Provider
|
||||
LLM_PROVIDER=openai # or anthropic, groq
|
||||
LLM_API_KEY=your_llm_api_key_here
|
||||
LLM_MODEL=gpt-4-turbo-preview
|
||||
```
|
||||
|
||||
### 3. Start Hindsight
|
||||
|
||||
Clone and run Hindsight from github.com/vectorize-io/hindsight:
|
||||
|
||||
```bash
|
||||
# Clone and run github.com/vectorize-io/hindsight
|
||||
cd /path/to/hindsight
|
||||
cargo run --bin hindsight-server
|
||||
```
|
||||
|
||||
Verify Hindsight is running at `http://localhost:8888`
|
||||
|
||||
### 4. Run the Application
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Visit `http://localhost:3000`
|
||||
|
||||
## Usage
|
||||
|
||||
### Creating a Tracking Session
|
||||
|
||||
1. **Set Location**: Enter country (required), state/province, and city (optional)
|
||||
2. **Choose Topic**: Specify the issue to track (e.g., "Climate Change Policy")
|
||||
3. **Add Candidates**: Enter names of candidates/politicians to track
|
||||
4. **Configure Time Range**: Set historical start/end dates for initial analysis
|
||||
5. **Set Frequency**: Choose how often to check for updates (hourly/daily/weekly)
|
||||
6. **Start Tracking**: Click "Start Tracking" to begin
|
||||
|
||||
### Viewing Results
|
||||
|
||||
- **Timeline Graph**: Shows confidence levels of each candidate's stance over time
|
||||
- **Stance Changes**: Red circles on the graph indicate detected position changes
|
||||
- **Click Points**: Click any point to see detailed stance information and sources
|
||||
- **Source Links**: Each stance includes links to original references
|
||||
|
||||
### Managing Sessions
|
||||
|
||||
- **Pause/Resume**: Temporarily stop or restart tracking
|
||||
- **Run Now**: Trigger an immediate update outside the schedule
|
||||
- **Status**: View current session status and frequency
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Sessions
|
||||
|
||||
- `POST /api/sessions` - Create new tracking session
|
||||
- `GET /api/sessions?id={id}` - Get session details
|
||||
- `GET /api/sessions` - List all sessions
|
||||
- `PATCH /api/sessions` - Update session status
|
||||
|
||||
### Stances
|
||||
|
||||
- `POST /api/stances` - Process candidate stance
|
||||
- `GET /api/stances?sessionId={id}&candidate={name}` - Get stances
|
||||
|
||||
### Scheduler
|
||||
|
||||
- `POST /api/scheduler` - Control session scheduling
|
||||
- Actions: `start`, `stop`, `run`
|
||||
|
||||
## Hindsight Integration Examples
|
||||
|
||||
### 1. Storing Memories
|
||||
|
||||
```typescript
|
||||
// Store web scraping results
|
||||
await hindsightClient.retain(bankId, articleContent, {
|
||||
context: 'web_search_result',
|
||||
timestamp: articleDate,
|
||||
metadata: { url: articleUrl }
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Semantic Search
|
||||
|
||||
```typescript
|
||||
// Search for relevant memories
|
||||
const results = await hindsightClient.recall(bankId, query, {
|
||||
budget: 'high',
|
||||
maxTokens: 8192
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Temporal Filtering
|
||||
|
||||
```typescript
|
||||
// Query memories up to a specific point in time
|
||||
const results = await hindsightClient.recall(bankId, query, {
|
||||
queryTimestamp: '2024-12-01T00:00:00Z'
|
||||
});
|
||||
```
|
||||
|
||||
### 4. Contextual Analysis
|
||||
|
||||
```typescript
|
||||
// Generate analysis using stored memories
|
||||
const response = await hindsightClient.reflect(bankId,
|
||||
'What is the candidate\'s stance on this issue?',
|
||||
{ budget: 'high' }
|
||||
);
|
||||
```
|
||||
|
||||
## Production Deployment
|
||||
|
||||
### Vercel Deployment
|
||||
|
||||
```bash
|
||||
# Install Vercel CLI
|
||||
npm i -g vercel
|
||||
|
||||
# Deploy
|
||||
vercel
|
||||
|
||||
# Set environment variables in Vercel dashboard:
|
||||
# - HINDSIGHT_API_URL
|
||||
# - TAVILY_API_KEY
|
||||
# - LLM_PROVIDER
|
||||
# - LLM_API_KEY
|
||||
# - LLM_MODEL
|
||||
```
|
||||
|
||||
**Note**: The `data/` directory for JSON storage will be ephemeral on Vercel. For production, consider using a persistent database or object storage.
|
||||
|
||||
## Development
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
stancetracker/
|
||||
├── app/
|
||||
│ ├── api/ # API routes
|
||||
│ ├── globals.css # Global styles
|
||||
│ ├── layout.tsx # Root layout
|
||||
│ └── page.tsx # Main page
|
||||
├── components/ # React components
|
||||
├── lib/
|
||||
│ ├── db/ # JSON database utilities
|
||||
│ ├── hindsight-client.ts # Hindsight API client
|
||||
│ ├── llm-client.ts # LLM provider client
|
||||
│ ├── web-scraper.ts # Tavily web scraper
|
||||
│ ├── scraper-agent.ts # Content scraper
|
||||
│ ├── rag-system.ts # Memory retrieval
|
||||
│ ├── stance-extractor.ts # Stance analysis
|
||||
│ ├── stance-pipeline.ts # Main pipeline
|
||||
│ └── scheduler.ts # Job scheduling
|
||||
└── types/ # TypeScript types
|
||||
```
|
||||
|
||||
### Adding New LLM Providers
|
||||
|
||||
Edit `lib/llm-client.ts` and add a new method:
|
||||
|
||||
```typescript
|
||||
private async newProviderComplete(messages, options) {
|
||||
// Implementation
|
||||
}
|
||||
```
|
||||
|
||||
## Limitations
|
||||
|
||||
- **Web Search**: Uses Tavily API which has rate limits
|
||||
- **Source Verification**: Manual verification recommended for critical applications
|
||||
- **Stance Extraction**: LLM-based, subject to model limitations
|
||||
- **Storage**: JSON file storage is not suitable for high-scale production use
|
||||
- **Rate Limits**: Respect API rate limits for Tavily, Hindsight, and LLM providers
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- [ ] Real-time social media monitoring
|
||||
- [ ] Speech/video transcription analysis
|
||||
- [ ] Multi-language support
|
||||
- [ ] Sentiment analysis integration
|
||||
- [ ] Comparative analysis dashboard
|
||||
- [ ] Export to CSV/PDF
|
||||
- [ ] Email notifications for stance changes
|
||||
- [ ] Public API for third-party integrations
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions, please check:
|
||||
- Hindsight documentation: `github.com/vectorize-io/hindsight/README.md`
|
||||
- Tavily API docs: https://tavily.com/
|
||||
- Project issues: Create an issue in the repository
|
||||
@@ -0,0 +1,122 @@
|
||||
---
|
||||
sidebar_position: 8
|
||||
---
|
||||
|
||||
# Hindsight AI SDK - Personal Chef
|
||||
|
||||
|
||||
:::info Complete Application
|
||||
This is a complete, runnable application demonstrating Hindsight integration.
|
||||
[**View source on GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/tree/main/applications/taste-ai)
|
||||
:::
|
||||
|
||||
|
||||
A personal food assistant demonstrating three key Hindsight integrations using the [Vercel AI SDK v6](https://sdk.vercel.ai/docs).
|
||||
|
||||
## Architecture: Single Bank with User Tags
|
||||
|
||||
This demo uses a **single Hindsight bank** (`taste-ai`) for all users, with each user's data tagged using `user:${username}`.
|
||||
|
||||
```typescript
|
||||
// All users share the same bank
|
||||
const BANK_ID = 'taste-ai';
|
||||
|
||||
// Each memory is tagged with the user
|
||||
await hindsightTools.retain.execute({
|
||||
bankId: BANK_ID,
|
||||
content: userData,
|
||||
tags: [`user:${username}`],
|
||||
});
|
||||
```
|
||||
|
||||
This architecture enables:
|
||||
- **Per-user queries**: Filter by `user:alice` to get personalized results
|
||||
- **Aggregated insights**: Query across all users to find popular recipes or common dietary patterns
|
||||
- **Simplified management**: One bank to maintain instead of per-user banks
|
||||
|
||||
## Three Hindsight Integrations
|
||||
|
||||
### 1. Meal Suggestions with Memory Recall & Reflection
|
||||
|
||||
Uses `recall` and `reflect` tools with AI SDK's agent-based approach to gather personalized context.
|
||||
|
||||
```typescript
|
||||
const contextResult = await generateText({
|
||||
model: llmModel,
|
||||
tools: {
|
||||
recall: hindsightTools.recall,
|
||||
reflect: hindsightTools.reflect,
|
||||
},
|
||||
toolChoice: 'auto',
|
||||
prompt: `You are gathering context for personalized ${mealType} recipe suggestions.
|
||||
|
||||
Use the recall tool to search for the user's food preferences, dislikes, and recent meals.
|
||||
Then use the reflect tool to analyze their dietary patterns and restrictions.
|
||||
|
||||
After gathering context, summarize their preferences and recent eating patterns.`,
|
||||
});
|
||||
```
|
||||
|
||||
The AI agent autonomously:
|
||||
- Searches memory for cuisine preferences and dietary restrictions
|
||||
- Analyzes recent protein consumption for variety
|
||||
- Identifies foods to avoid
|
||||
|
||||
### 2. Goal Progress Tracking with Mental Models
|
||||
|
||||
Uses mental models to automatically maintain updated insights about user progress.
|
||||
|
||||
```typescript
|
||||
// Create a mental model that auto-refreshes after new meals
|
||||
await hindsightTools.createMentalModel.execute({
|
||||
bankId: BANK_ID,
|
||||
mentalModelId: getMentalModelId(username, 'goals'),
|
||||
name: `${username}'s Goal Progress`,
|
||||
sourceQuery: `Analyze ${username}'s dietary goals and eating patterns.
|
||||
Describe their progress towards their stated goals (weight loss, muscle gain, etc.).`,
|
||||
tags: [`user:${username}`],
|
||||
autoRefresh: true, // Refreshes automatically after consolidation
|
||||
});
|
||||
|
||||
// Query the mental model for current insights
|
||||
const result = await hindsightTools.queryMentalModel.execute({
|
||||
bankId: BANK_ID,
|
||||
mentalModelId: mentalModelId,
|
||||
});
|
||||
```
|
||||
|
||||
Mental models automatically:
|
||||
- Track progress towards dietary goals
|
||||
- Update after each new meal is logged
|
||||
- Provide fresh insights without manual refresh
|
||||
|
||||
### 3. Language Enforcement with Directives
|
||||
|
||||
Uses directives to ensure all responses match user's language preference.
|
||||
|
||||
```typescript
|
||||
await hindsightClient.createDirective(BANK_ID, {
|
||||
name: `${username}'s Language Preference`,
|
||||
content: `Always respond in ${language}. All suggestions must be in ${language}.`,
|
||||
priority: 100,
|
||||
tags: [`user:${username}`, 'directive:language'],
|
||||
});
|
||||
```
|
||||
|
||||
Directives are automatically injected when mental models generate insights, ensuring consistent language across all interactions.
|
||||
|
||||
## Running the Demo
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
**Requirements:**
|
||||
- Hindsight server running at `http://localhost:8888` (or set `HINDSIGHT_URL`)
|
||||
- Node.js 18+
|
||||
|
||||
## Learn More
|
||||
|
||||
- [Hindsight AI SDK on npm](https://www.npmjs.com/package/@vectorize-io/hindsight-ai-sdk)
|
||||
- [AI SDK Documentation](https://sdk.vercel.ai/docs)
|
||||
@@ -0,0 +1,138 @@
|
||||
|
||||
|
||||
<div className="cookbook-page">
|
||||
|
||||
# Cookbook
|
||||
|
||||
Learn how to build with Hindsight through practical examples:
|
||||
|
||||
- **[Recipes](#recipes)** - Step-by-step guides and patterns for common use cases
|
||||
- **[Applications](#applications)** - Complete, runnable applications demonstrating Hindsight integration
|
||||
|
||||
<RecipeCarousel
|
||||
title="Recipes"
|
||||
items={[
|
||||
{
|
||||
title: "Hindsight Quickstart",
|
||||
href: "/cookbook/recipes/quickstart",
|
||||
description: "Learn the basics: retain, recall, and reflect",
|
||||
tags: { sdk: "hindsight-client", topic: "Quick Start" }
|
||||
},
|
||||
{
|
||||
title: "Per-User Memory",
|
||||
href: "/cookbook/recipes/per-user-memory",
|
||||
description: "Build a chatbot with per-user memory isolation",
|
||||
tags: { sdk: "hindsight-client", topic: "Learning" }
|
||||
},
|
||||
{
|
||||
title: "Support Agent with Shared Knowledge",
|
||||
href: "/cookbook/recipes/support-agent-shared-knowledge",
|
||||
description: "Combine per-user memory with shared product documentation",
|
||||
tags: { sdk: "hindsight-client", topic: "Learning" }
|
||||
},
|
||||
{
|
||||
title: "Memory with LiteLLM",
|
||||
href: "/cookbook/recipes/litellm-memory-demo",
|
||||
description: "Add automatic memory to any LLM app using LiteLLM callbacks",
|
||||
tags: { sdk: "hindsight-litellm", topic: "Quick Start" }
|
||||
},
|
||||
{
|
||||
title: "Routing Tool Learning",
|
||||
href: "/cookbook/recipes/tool-learning-demo",
|
||||
description: "Teach an LLM which tool to use through feedback and memory",
|
||||
tags: { sdk: "hindsight-litellm", topic: "Learning" }
|
||||
},
|
||||
{
|
||||
title: "Fitness Coach with Hindsight Memory",
|
||||
href: "/cookbook/recipes/fitness_tracker",
|
||||
description: "Track workouts, diet, and progress with a personalized fitness coach",
|
||||
tags: { sdk: "hindsight-client", topic: "Recommendation" }
|
||||
},
|
||||
{
|
||||
title: "Healthcare Assistant with Hindsight Memory",
|
||||
href: "/cookbook/recipes/healthcare_assistant",
|
||||
description: "A supportive chatbot that remembers patient history and preferences",
|
||||
tags: { sdk: "hindsight-client", topic: "Recommendation" }
|
||||
},
|
||||
{
|
||||
title: "Movie Recommendation Assistant with Hindsight Memory",
|
||||
href: "/cookbook/recipes/movie_recommendation",
|
||||
description: "Get personalized movie recommendations that improve over time",
|
||||
tags: { sdk: "hindsight-client", topic: "Recommendation" }
|
||||
},
|
||||
{
|
||||
title: "Personal AI Assistant with Hindsight Memory",
|
||||
href: "/cookbook/recipes/personal_assistant",
|
||||
description: "A general-purpose assistant that remembers your life and preferences",
|
||||
tags: { sdk: "hindsight-client", topic: "Recommendation" }
|
||||
},
|
||||
{
|
||||
title: "Personalized Search Agent with Hindsight Memory",
|
||||
href: "/cookbook/recipes/personalized_search",
|
||||
description: "Search assistant that learns your location, diet, and lifestyle",
|
||||
tags: { sdk: "hindsight-client", topic: "Recommendation" }
|
||||
},
|
||||
{
|
||||
title: "Study Buddy with Hindsight Memory",
|
||||
href: "/cookbook/recipes/study_buddy",
|
||||
description: "Track study sessions, identify knowledge gaps, and get personalized review suggestions",
|
||||
tags: { sdk: "hindsight-client", topic: "Learning" }
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
<RecipeCarousel
|
||||
title="Applications"
|
||||
items={[
|
||||
{
|
||||
title: "Chat Memory App",
|
||||
href: "/cookbook/applications/chat-memory",
|
||||
description: "Real-time chat app with per-user memory using Groq and Hindsight",
|
||||
tags: { sdk: "hindsight-client", topic: "Chat" }
|
||||
},
|
||||
{
|
||||
title: "Deliveryman Demo",
|
||||
href: "/cookbook/applications/deliveryman-demo",
|
||||
description: "Delivery agent simulation demonstrating learning through mental models",
|
||||
tags: { sdk: "hindsight-client", topic: "Learning" }
|
||||
},
|
||||
{
|
||||
title: "Memory Approaches Comparison Demo",
|
||||
href: "/cookbook/applications/hindsight-litellm-demo",
|
||||
description: "Interactive comparison of memory approaches: none, full history, and semantic retrieval",
|
||||
tags: { sdk: "hindsight-litellm", topic: "Quick Start" }
|
||||
},
|
||||
{
|
||||
title: "Tool Learning Demo",
|
||||
href: "/cookbook/applications/hindsight-tool-learning-demo",
|
||||
description: "Show how Hindsight helps LLMs learn which tool to use when names are ambiguous",
|
||||
tags: { sdk: "hindsight-litellm", topic: "Learning" }
|
||||
},
|
||||
{
|
||||
title: "OpenAI Agent + Hindsight Memory Integration",
|
||||
href: "/cookbook/applications/openai-fitness-coach",
|
||||
description: "Fitness coach using OpenAI Assistants with Hindsight as memory backend",
|
||||
tags: { sdk: "hindsight-client", topic: "Recommendation" }
|
||||
},
|
||||
{
|
||||
title: "Sanity CMS Blog Memory",
|
||||
href: "/cookbook/applications/sanity-blog-memory",
|
||||
description: "Sync Sanity CMS blog posts to Hindsight for semantic search and AI insights",
|
||||
tags: { sdk: "hindsight-client", topic: "Learning" }
|
||||
},
|
||||
{
|
||||
title: "Stance Tracker",
|
||||
href: "/cookbook/applications/stancetracker",
|
||||
description: "Track political candidates' stances over time with automated web scraping",
|
||||
tags: { sdk: "hindsight-client", topic: "Recommendation" }
|
||||
},
|
||||
{
|
||||
title: "Hindsight AI SDK - Personal Chef",
|
||||
href: "/cookbook/applications/taste-ai",
|
||||
description: "Personal food assistant with AI SDK v6 showcasing recall, mental models, and directives",
|
||||
tags: { sdk: "@vectorize-io/hindsight-ai-sdk", topic: "Recommendation" }
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,306 @@
|
||||
---
|
||||
sidebar_position: 6
|
||||
---
|
||||
|
||||
# Fitness Coach with Hindsight Memory
|
||||
|
||||
|
||||
:::tip Run this notebook
|
||||
This recipe is available as an interactive Jupyter notebook.
|
||||
[**Open in GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/blob/main/notebooks/fitness_tracker.ipynb)
|
||||
:::
|
||||
|
||||
|
||||
A personalized fitness assistant that tracks your workouts, diet, recovery, and progress over time to give contextual advice.
|
||||
|
||||
## Features
|
||||
- Logs workout sessions with exercises and weights
|
||||
- Tracks meals and dietary preferences
|
||||
- Monitors recovery and sleep patterns
|
||||
- Provides personalized training advice
|
||||
|
||||
## Prerequisites
|
||||
- OpenAI API key
|
||||
- Hindsight running locally via Docker (see setup below)
|
||||
|
||||
## Start Hindsight Locally
|
||||
|
||||
Before running this notebook, start Hindsight in a terminal:
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY="your-openai-api-key"
|
||||
|
||||
docker run --rm -it --pull always -p 8888:8888 -p 9999:9999 \
|
||||
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
|
||||
-e HINDSIGHT_API_LLM_MODEL=gpt-4o-mini \
|
||||
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
|
||||
ghcr.io/vectorize-io/hindsight:latest
|
||||
```
|
||||
|
||||
## 1. Install Dependencies
|
||||
|
||||
|
||||
```python
|
||||
!pip install -q hindsight-client openai nest-asyncio
|
||||
```
|
||||
|
||||
## 2. Configure OpenAI API Key
|
||||
|
||||
Enter your OpenAI API key when prompted (used by both Hindsight and the demo).
|
||||
|
||||
|
||||
```python
|
||||
import getpass
|
||||
import os
|
||||
|
||||
# Set OpenAI API key (used by both Hindsight and the demo)
|
||||
if not os.getenv("OPENAI_API_KEY"):
|
||||
os.environ["OPENAI_API_KEY"] = getpass.getpass("Enter your OpenAI API key: ")
|
||||
|
||||
print("API key configured!")
|
||||
```
|
||||
|
||||
## 3. Initialize Clients
|
||||
|
||||
|
||||
```python
|
||||
import nest_asyncio
|
||||
nest_asyncio.apply()
|
||||
|
||||
from datetime import datetime
|
||||
from openai import OpenAI
|
||||
from hindsight_client import Hindsight
|
||||
|
||||
# Initialize Hindsight client (connects to local Docker instance)
|
||||
hindsight = Hindsight(
|
||||
base_url=os.getenv("HINDSIGHT_BASE_URL", "http://localhost:8888"),
|
||||
)
|
||||
|
||||
openai_client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
|
||||
|
||||
USER_ID = "fitness-user-demo"
|
||||
|
||||
print("Clients initialized!")
|
||||
```
|
||||
|
||||
## 4. Define Helper Functions
|
||||
|
||||
|
||||
```python
|
||||
def log_workout(workout_details: str) -> str:
|
||||
"""Log a workout session with timestamp."""
|
||||
today = datetime.now().strftime("%B %d, %Y")
|
||||
hindsight.retain(
|
||||
bank_id=USER_ID,
|
||||
content=f"{today} - WORKOUT LOG: {workout_details}",
|
||||
metadata={"category": "workout", "date": today},
|
||||
)
|
||||
return f"Logged workout for {today}: {workout_details}"
|
||||
|
||||
|
||||
def log_meal(meal_details: str) -> str:
|
||||
"""Log a meal with timestamp."""
|
||||
today = datetime.now().strftime("%B %d, %Y")
|
||||
hindsight.retain(
|
||||
bank_id=USER_ID,
|
||||
content=f"{today} - MEAL LOG: {meal_details}",
|
||||
metadata={"category": "nutrition", "date": today},
|
||||
)
|
||||
return f"Logged meal for {today}: {meal_details}"
|
||||
|
||||
|
||||
def log_recovery(recovery_details: str) -> str:
|
||||
"""Log recovery information (sleep, soreness, etc.)."""
|
||||
today = datetime.now().strftime("%B %d, %Y")
|
||||
hindsight.retain(
|
||||
bank_id=USER_ID,
|
||||
content=f"{today} - RECOVERY LOG: {recovery_details}",
|
||||
metadata={"category": "recovery", "date": today},
|
||||
)
|
||||
return f"Logged recovery for {today}: {recovery_details}"
|
||||
|
||||
|
||||
def store_user_profile(profile_info: str) -> str:
|
||||
"""Store user profile information."""
|
||||
hindsight.retain(
|
||||
bank_id=USER_ID,
|
||||
content=f"USER PROFILE: {profile_info}",
|
||||
metadata={"category": "profile"},
|
||||
)
|
||||
return f"Stored profile info: {profile_info}"
|
||||
|
||||
|
||||
def fitness_coach(user_query: str) -> str:
|
||||
"""Get personalized fitness advice based on query and user history."""
|
||||
memories = hindsight.recall(
|
||||
bank_id=USER_ID,
|
||||
query=f"fitness workout diet recovery goals {user_query}",
|
||||
budget="high",
|
||||
)
|
||||
|
||||
memory_context = ""
|
||||
if memories and memories.results:
|
||||
memory_context = "\n".join(f"- {m.text}" for m in memories.results[:10])
|
||||
|
||||
system_prompt = f"""You are a knowledgeable and supportive fitness coach.
|
||||
You have access to the user's workout history, diet logs, recovery notes, and personal profile.
|
||||
|
||||
What you know about this user:
|
||||
{memory_context if memory_context else "No history recorded yet."}
|
||||
|
||||
Provide personalized, actionable advice based on their:
|
||||
- Training history and progress
|
||||
- Dietary preferences and restrictions
|
||||
- Recovery patterns
|
||||
- Personal goals
|
||||
|
||||
Be encouraging but realistic. Reference their specific history when relevant."""
|
||||
|
||||
response = openai_client.chat.completions.create(
|
||||
model="gpt-4o-mini",
|
||||
messages=[
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": user_query},
|
||||
],
|
||||
temperature=0.7,
|
||||
max_tokens=600,
|
||||
)
|
||||
|
||||
advice = response.choices[0].message.content
|
||||
|
||||
hindsight.retain(
|
||||
bank_id=USER_ID,
|
||||
content=f"User asked: {user_query}\nCoach advised: {advice[:200]}...",
|
||||
metadata={"category": "coaching"},
|
||||
)
|
||||
|
||||
return advice
|
||||
|
||||
|
||||
def get_progress_report() -> str:
|
||||
"""Generate a progress report based on workout history."""
|
||||
report = hindsight.reflect(
|
||||
bank_id=USER_ID,
|
||||
query="""Analyze this user's fitness journey:
|
||||
1. How consistent have they been with workouts?
|
||||
2. What progress have they made (weight lifted, exercises)?
|
||||
3. How is their recovery and sleep?
|
||||
4. What dietary patterns do you notice?
|
||||
5. What should they focus on next?""",
|
||||
budget="high",
|
||||
)
|
||||
return report.text if hasattr(report, 'text') else str(report)
|
||||
|
||||
print("Helper functions defined!")
|
||||
```
|
||||
|
||||
## 5. Set Up User Profile
|
||||
|
||||
|
||||
```python
|
||||
print("Setting up user profile...")
|
||||
|
||||
profile_data = [
|
||||
"Name: Anish, Age: 26, Height: 5'10\", Weight: 72kg",
|
||||
"Goal: Building lean muscle, started gym 6 months ago",
|
||||
"Routine: Push-pull-legs split, 5x per week",
|
||||
"Rest days: Wednesday and Sunday",
|
||||
"Dietary restriction: Mild lactose intolerance, uses almond milk",
|
||||
"Health note: Occasional knee pain, avoids deep squats",
|
||||
"Supplements: Whey protein (lactose-free), magnesium",
|
||||
"Sleep: Aims for 7+ hours, performance drops under 6 hours",
|
||||
]
|
||||
|
||||
for info in profile_data:
|
||||
store_user_profile(info)
|
||||
print(f" Stored: {info[:50]}...")
|
||||
```
|
||||
|
||||
## 6. Log Workout History
|
||||
|
||||
|
||||
```python
|
||||
print("Logging workout history...")
|
||||
|
||||
workouts = [
|
||||
"Push day: Bench press 3x8 @ 60kg, overhead press 4x12, tricep dips 3x10. Felt strong.",
|
||||
"Pull day: Deadlift 3x5 @ 80kg, barbell rows 4x10, bicep curls 3x12. Good session.",
|
||||
"Leg day: Leg press 4x12, hamstring curls 3x12, glute bridges 3x15. Knee felt okay.",
|
||||
]
|
||||
|
||||
for workout in workouts:
|
||||
print(f" {log_workout(workout)[:60]}...")
|
||||
|
||||
print("\nLogging recent meals...")
|
||||
meals = [
|
||||
"Post-workout: Whey shake with almond milk, banana, oats",
|
||||
"Dinner: Grilled chicken, brown rice, steamed vegetables",
|
||||
"Snack: Greek yogurt (lactose-free) with berries",
|
||||
]
|
||||
|
||||
for meal in meals:
|
||||
print(f" {log_meal(meal)[:60]}...")
|
||||
|
||||
print("\nLogging recovery notes...")
|
||||
recovery = [
|
||||
"Slept 7.5 hours, feeling well rested",
|
||||
"Some DOMS in legs from yesterday, using turmeric milk",
|
||||
]
|
||||
|
||||
for note in recovery:
|
||||
print(f" {log_recovery(note)[:60]}...")
|
||||
```
|
||||
|
||||
## 7. Talk to Your Fitness Coach
|
||||
|
||||
|
||||
```python
|
||||
import time
|
||||
|
||||
print("=" * 60)
|
||||
print(" Talking to your fitness coach...")
|
||||
print("=" * 60)
|
||||
|
||||
queries = [
|
||||
"How much was I lifting for bench press recently?",
|
||||
"I slept poorly last night (only 5 hours). What should I do for today's workout?",
|
||||
"Suggest a post-workout meal that works with my dietary restrictions.",
|
||||
"My knee has been bothering me more. Any exercise modifications?",
|
||||
]
|
||||
|
||||
for query in queries:
|
||||
print(f"\nUser: {query}")
|
||||
print("-" * 40)
|
||||
response = fitness_coach(query)
|
||||
print(f"Coach: {response}")
|
||||
time.sleep(1)
|
||||
```
|
||||
|
||||
## 8. Generate Progress Report
|
||||
|
||||
|
||||
```python
|
||||
print("=" * 60)
|
||||
print(" Progress Report")
|
||||
print("=" * 60)
|
||||
print(get_progress_report())
|
||||
```
|
||||
|
||||
## 9. Try Your Own Query
|
||||
|
||||
|
||||
```python
|
||||
your_query = "What exercises should I do today?" # Change this!
|
||||
|
||||
print(f"You: {your_query}")
|
||||
print("-" * 40)
|
||||
print(f"Coach: {fitness_coach(your_query)}")
|
||||
```
|
||||
|
||||
## 10. Cleanup
|
||||
|
||||
|
||||
```python
|
||||
hindsight.close()
|
||||
print("Client connection closed.")
|
||||
```
|
||||
@@ -0,0 +1,299 @@
|
||||
---
|
||||
sidebar_position: 7
|
||||
---
|
||||
|
||||
# Healthcare Assistant with Hindsight Memory
|
||||
|
||||
|
||||
:::tip Run this notebook
|
||||
This recipe is available as an interactive Jupyter notebook.
|
||||
[**Open in GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/blob/main/notebooks/healthcare_assistant.ipynb)
|
||||
:::
|
||||
|
||||
|
||||
A supportive healthcare chatbot that remembers patient history, symptoms, medications, and preferences to provide personalized guidance.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
**This is a demo application and should NOT be used for actual medical advice. Always consult qualified healthcare professionals.**
|
||||
|
||||
## Features
|
||||
- Tracks symptoms, medications, and allergies
|
||||
- Maintains patient history across conversations
|
||||
- Provides health information and wellness tips
|
||||
- Schedules appointments
|
||||
|
||||
## Prerequisites
|
||||
- OpenAI API key
|
||||
- Hindsight running locally via Docker (see setup below)
|
||||
|
||||
## Start Hindsight Locally
|
||||
|
||||
Before running this notebook, start Hindsight in a terminal:
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY="your-openai-api-key"
|
||||
|
||||
docker run --rm -it --pull always -p 8888:8888 -p 9999:9999 \
|
||||
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
|
||||
-e HINDSIGHT_API_LLM_MODEL=gpt-4o-mini \
|
||||
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
|
||||
ghcr.io/vectorize-io/hindsight:latest
|
||||
```
|
||||
|
||||
## 1. Install Dependencies
|
||||
|
||||
|
||||
```python
|
||||
!pip install -q hindsight-client openai nest-asyncio
|
||||
```
|
||||
|
||||
## 2. Configure OpenAI API Key
|
||||
|
||||
Enter your OpenAI API key when prompted (used by both Hindsight and the demo).
|
||||
|
||||
|
||||
```python
|
||||
import getpass
|
||||
import os
|
||||
|
||||
# Set OpenAI API key (used by both Hindsight and the demo)
|
||||
if not os.getenv("OPENAI_API_KEY"):
|
||||
os.environ["OPENAI_API_KEY"] = getpass.getpass("Enter your OpenAI API key: ")
|
||||
|
||||
print("API key configured!")
|
||||
```
|
||||
|
||||
## 3. Initialize Clients
|
||||
|
||||
|
||||
```python
|
||||
import nest_asyncio
|
||||
nest_asyncio.apply()
|
||||
|
||||
from datetime import datetime
|
||||
import random
|
||||
from openai import OpenAI
|
||||
from hindsight_client import Hindsight
|
||||
|
||||
# Initialize Hindsight client (connects to local Docker instance)
|
||||
hindsight = Hindsight(
|
||||
base_url=os.getenv("HINDSIGHT_BASE_URL", "http://localhost:8888"),
|
||||
)
|
||||
|
||||
openai_client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
|
||||
|
||||
PATIENT_ID = "patient-demo"
|
||||
|
||||
def get_patient_bank_id(patient_id: str) -> str:
|
||||
return f"patient-{patient_id}"
|
||||
|
||||
print("Clients initialized!")
|
||||
```
|
||||
|
||||
## 4. Define Helper Functions
|
||||
|
||||
|
||||
```python
|
||||
def store_patient_info(patient_id: str, info: str, category: str = "general") -> str:
|
||||
"""Store patient information."""
|
||||
bank_id = get_patient_bank_id(patient_id)
|
||||
today = datetime.now().strftime("%B %d, %Y")
|
||||
|
||||
hindsight.retain(
|
||||
bank_id=bank_id,
|
||||
content=f"{today} - {category.upper()}: {info}",
|
||||
metadata={"category": category, "date": today},
|
||||
)
|
||||
|
||||
return f"Recorded {category}: {info}"
|
||||
|
||||
|
||||
def get_patient_history(patient_id: str, query: str) -> str:
|
||||
"""Retrieve relevant patient history."""
|
||||
bank_id = get_patient_bank_id(patient_id)
|
||||
|
||||
memories = hindsight.recall(
|
||||
bank_id=bank_id,
|
||||
query=query,
|
||||
budget="high",
|
||||
)
|
||||
|
||||
if memories and memories.results:
|
||||
return "\n".join(f"- {m.text}" for m in memories.results[:10])
|
||||
return "No relevant history found."
|
||||
|
||||
|
||||
def healthcare_chat(patient_id: str, user_message: str) -> str:
|
||||
"""Chat with the healthcare assistant."""
|
||||
bank_id = get_patient_bank_id(patient_id)
|
||||
|
||||
history = get_patient_history(
|
||||
patient_id,
|
||||
f"symptoms medications allergies conditions {user_message}"
|
||||
)
|
||||
|
||||
system_prompt = f"""You are a supportive healthcare assistant chatbot.
|
||||
|
||||
IMPORTANT DISCLAIMERS:
|
||||
- You are NOT a doctor and cannot provide medical diagnoses
|
||||
- Always recommend consulting healthcare professionals for serious concerns
|
||||
- Never prescribe medications or suggest stopping prescribed treatments
|
||||
|
||||
Your role:
|
||||
- Listen empathetically to patient concerns
|
||||
- Remember and reference their medical history
|
||||
- Provide general health information and wellness tips
|
||||
- Help track symptoms over time
|
||||
- Remind about medications and appointments
|
||||
- Suggest when to seek professional care
|
||||
|
||||
Patient History:
|
||||
{history}
|
||||
|
||||
Guidelines:
|
||||
- Be warm and supportive
|
||||
- Ask clarifying questions when needed
|
||||
- Reference their history when relevant
|
||||
- Flag any concerning symptoms for professional review"""
|
||||
|
||||
response = openai_client.chat.completions.create(
|
||||
model="gpt-4o-mini",
|
||||
messages=[
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": user_message},
|
||||
],
|
||||
temperature=0.7,
|
||||
max_tokens=600,
|
||||
)
|
||||
|
||||
answer = response.choices[0].message.content
|
||||
|
||||
hindsight.retain(
|
||||
bank_id=bank_id,
|
||||
content=f"Patient concern: {user_message}\nGuidance provided: {answer[:200]}...",
|
||||
metadata={"category": "consultation"},
|
||||
)
|
||||
|
||||
return answer
|
||||
|
||||
|
||||
def get_health_summary(patient_id: str) -> str:
|
||||
"""Generate a health summary for the patient."""
|
||||
bank_id = get_patient_bank_id(patient_id)
|
||||
|
||||
summary = hindsight.reflect(
|
||||
bank_id=bank_id,
|
||||
query="""Summarize this patient's health profile:
|
||||
1. Known conditions and diagnoses
|
||||
2. Current medications
|
||||
3. Allergies and sensitivities
|
||||
4. Recent symptoms reported
|
||||
5. Lifestyle factors mentioned
|
||||
6. Any patterns or trends in their health""",
|
||||
budget="high",
|
||||
)
|
||||
return summary.text if hasattr(summary, 'text') else str(summary)
|
||||
|
||||
|
||||
def schedule_appointment(patient_id: str, appointment_type: str, preferred_time: str) -> str:
|
||||
"""Schedule an appointment (demo)."""
|
||||
confirmation_id = f"APT-{random.randint(10000, 99999)}"
|
||||
|
||||
store_patient_info(
|
||||
patient_id,
|
||||
f"Appointment scheduled: {appointment_type} - Preferred time: {preferred_time} - Confirmation: {confirmation_id}",
|
||||
category="appointment"
|
||||
)
|
||||
|
||||
return f"Appointment requested: {appointment_type}\nPreferred time: {preferred_time}\nConfirmation ID: {confirmation_id}\n\nA staff member will confirm the exact time within 24 hours."
|
||||
|
||||
print("Helper functions defined!")
|
||||
```
|
||||
|
||||
## 5. Set Up Patient Profile
|
||||
|
||||
|
||||
```python
|
||||
print("Setting up patient profile...")
|
||||
|
||||
patient_info = [
|
||||
("Age: 45, Male, Height: 5'11\", Weight: 185 lbs", "demographics"),
|
||||
("Allergy: Penicillin - causes hives", "allergies"),
|
||||
("Allergy: Shellfish - causes throat swelling", "allergies"),
|
||||
("Current medication: Lisinopril 10mg daily for blood pressure", "medications"),
|
||||
("Current medication: Metformin 500mg twice daily for Type 2 diabetes", "medications"),
|
||||
("Condition: Diagnosed with Type 2 diabetes in 2020", "conditions"),
|
||||
("Condition: Mild hypertension, well-controlled", "conditions"),
|
||||
("Family history: Father had heart disease", "family_history"),
|
||||
("Lifestyle: Sedentary job, trying to exercise more", "lifestyle"),
|
||||
]
|
||||
|
||||
for info, category in patient_info:
|
||||
result = store_patient_info(PATIENT_ID, info, category)
|
||||
print(f" {result}")
|
||||
```
|
||||
|
||||
## 6. Healthcare Chat
|
||||
|
||||
|
||||
```python
|
||||
import time
|
||||
|
||||
print("=" * 60)
|
||||
print(" Healthcare Chat")
|
||||
print("=" * 60)
|
||||
|
||||
conversations = [
|
||||
"Hi, I've been having headaches for the past few days. Should I be worried?",
|
||||
"The headaches are mostly in the afternoon. I've also been feeling more tired than usual.",
|
||||
"I've been checking my blood sugar and it's been a bit higher lately, around 140-150 fasting.",
|
||||
"Can you remind me what allergies I have? I'm going to a new restaurant.",
|
||||
]
|
||||
|
||||
for message in conversations:
|
||||
print(f"\nPatient: {message}")
|
||||
print("-" * 40)
|
||||
response = healthcare_chat(PATIENT_ID, message)
|
||||
print(f"Assistant: {response}")
|
||||
time.sleep(1)
|
||||
```
|
||||
|
||||
## 7. Schedule Appointment
|
||||
|
||||
|
||||
```python
|
||||
print("=" * 60)
|
||||
print(" Scheduling Appointment")
|
||||
print("=" * 60)
|
||||
print(schedule_appointment(PATIENT_ID, "General checkup", "Next Tuesday afternoon"))
|
||||
```
|
||||
|
||||
## 8. Health Summary
|
||||
|
||||
|
||||
```python
|
||||
print("=" * 60)
|
||||
print(" Patient Health Summary")
|
||||
print("=" * 60)
|
||||
print(get_health_summary(PATIENT_ID))
|
||||
```
|
||||
|
||||
## 9. Try Your Own Question
|
||||
|
||||
|
||||
```python
|
||||
your_question = "Should I adjust my Metformin dose?" # Change this!
|
||||
|
||||
print(f"You: {your_question}")
|
||||
print("-" * 40)
|
||||
print(f"Assistant: {healthcare_chat(PATIENT_ID, your_question)}")
|
||||
```
|
||||
|
||||
## 10. Cleanup
|
||||
|
||||
|
||||
```python
|
||||
hindsight.close()
|
||||
print("Client connection closed.")
|
||||
```
|
||||
@@ -0,0 +1,187 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
---
|
||||
|
||||
# Memory with LiteLLM
|
||||
|
||||
|
||||
:::tip Run this notebook
|
||||
This recipe is available as an interactive Jupyter notebook.
|
||||
[**Open in GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/blob/main/notebooks/04-litellm-memory-demo.ipynb)
|
||||
:::
|
||||
|
||||
|
||||
This notebook demonstrates how to add persistent memory to any LLM app using the `hindsight-litellm` package. Memory storage and injection happen automatically via LiteLLM callbacks - no manual memory management needed!
|
||||
|
||||
**Key features demonstrated:**
|
||||
1. `configure()` + `enable()` - Set up automatic memory integration
|
||||
2. Automatic storage - Conversations are stored after each LLM call
|
||||
3. Automatic injection - Relevant memories are injected into prompts
|
||||
|
||||
The `hindsight-litellm` package hooks into LiteLLM's callback system to:
|
||||
- Store each conversation after successful LLM responses
|
||||
- Inject relevant memories into the system prompt before LLM calls
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Make sure you have Hindsight running:
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY=your-key
|
||||
|
||||
docker run --rm -it --pull always -p 8888:8888 -p 9999:9999 \
|
||||
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
|
||||
-e HINDSIGHT_API_LLM_MODEL=o3-mini \
|
||||
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
|
||||
ghcr.io/vectorize-io/hindsight:latest
|
||||
```
|
||||
|
||||
- API: http://localhost:8888
|
||||
- UI: http://localhost:9999
|
||||
|
||||
## Installation
|
||||
|
||||
|
||||
```python
|
||||
!pip install hindsight-litellm litellm nest_asyncio python-dotenv -U -q
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
```python
|
||||
import os
|
||||
import uuid
|
||||
import time
|
||||
import logging
|
||||
import nest_asyncio
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Apply nest_asyncio for Jupyter compatibility
|
||||
nest_asyncio.apply()
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logging.getLogger("LiteLLM").setLevel(logging.WARNING)
|
||||
logging.getLogger("LiteLLM Router").setLevel(logging.WARNING)
|
||||
logging.getLogger("LiteLLM Proxy").setLevel(logging.WARNING)
|
||||
|
||||
# Import hindsight_litellm
|
||||
import hindsight_litellm
|
||||
|
||||
# Configuration
|
||||
HINDSIGHT_API_URL = os.getenv("HINDSIGHT_API_URL", "http://localhost:8888")
|
||||
|
||||
# Check for API key
|
||||
if not os.getenv("OPENAI_API_KEY"):
|
||||
print("Warning: OPENAI_API_KEY not set")
|
||||
```
|
||||
|
||||
## Configure and Enable Automatic Memory
|
||||
|
||||
This is all you need! After this, all LiteLLM calls will automatically:
|
||||
- Have relevant memories injected into the prompt
|
||||
- Store conversations to Hindsight after the response
|
||||
|
||||
|
||||
```python
|
||||
# Generate a unique bank_id for this demo session
|
||||
bank_id = f"demo-{uuid.uuid4().hex[:8]}"
|
||||
print(f"Using bank_id: {bank_id}")
|
||||
|
||||
# Configure and enable hindsight
|
||||
hindsight_litellm.configure(
|
||||
hindsight_api_url=HINDSIGHT_API_URL,
|
||||
bank_id=bank_id,
|
||||
store_conversations=True, # Automatically store conversations
|
||||
inject_memories=True, # Automatically inject relevant memories
|
||||
verbose=True, # Enable logging to debug memory operations
|
||||
)
|
||||
hindsight_litellm.enable()
|
||||
|
||||
print("Hindsight memory integration enabled!")
|
||||
```
|
||||
|
||||
## Conversation 1: User Introduces Themselves
|
||||
|
||||
In this first conversation, the user shares some information about themselves. This will be automatically stored to Hindsight memory.
|
||||
|
||||
|
||||
```python
|
||||
user_message_1 = "Hi! I'm Alex and I work at Google as a software engineer. I love Python and machine learning."
|
||||
print(f"User: {user_message_1}\n")
|
||||
|
||||
# Use hindsight_litellm.completion() directly
|
||||
response_1 = hindsight_litellm.completion(
|
||||
model="gpt-4o-mini",
|
||||
messages=[
|
||||
{"role": "system", "content": "You are a helpful assistant."},
|
||||
{"role": "user", "content": user_message_1}
|
||||
],
|
||||
)
|
||||
|
||||
assistant_response_1 = response_1.choices[0].message.content
|
||||
print(f"Assistant: {assistant_response_1}")
|
||||
print("\n(Conversation automatically stored to Hindsight)")
|
||||
```
|
||||
|
||||
## Wait for Memory Processing
|
||||
|
||||
Hindsight needs a few seconds to process and extract facts from the conversation.
|
||||
|
||||
|
||||
```python
|
||||
print("Waiting 12 seconds for memory processing...")
|
||||
time.sleep(12)
|
||||
print("Done!")
|
||||
```
|
||||
|
||||
## Conversation 2: Test Memory-Augmented Response
|
||||
|
||||
Now we start a fresh conversation and ask what the assistant remembers. The memories from the previous conversation will be automatically injected into the prompt!
|
||||
|
||||
|
||||
```python
|
||||
user_message_2 = "What do you know about me? What programming language should I use for my next project?"
|
||||
print(f"User: {user_message_2}\n")
|
||||
|
||||
# Memories are automatically injected before this call!
|
||||
response_2 = hindsight_litellm.completion(
|
||||
model="gpt-4o-mini",
|
||||
messages=[
|
||||
{"role": "system", "content": "You are a helpful assistant."},
|
||||
{"role": "user", "content": user_message_2}
|
||||
],
|
||||
)
|
||||
|
||||
print(f"Assistant: {response_2.choices[0].message.content}")
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
The assistant should have remembered that Alex:
|
||||
- Works at Google as a software engineer
|
||||
- Loves Python and machine learning
|
||||
|
||||
And it should have recommended Python based on that knowledge!
|
||||
|
||||
|
||||
```python
|
||||
print(f"Memories stored in bank: {bank_id}")
|
||||
print(f"View in UI: http://localhost:9999/banks/{bank_id}")
|
||||
```
|
||||
|
||||
## Cleanup
|
||||
|
||||
|
||||
```python
|
||||
hindsight_litellm.cleanup()
|
||||
|
||||
# Optional: delete the bank
|
||||
import requests
|
||||
response = requests.delete(f"{HINDSIGHT_API_URL}/v1/default/banks/{bank_id}")
|
||||
print(f"Deleted bank: {response.json()}")
|
||||
```
|
||||
@@ -0,0 +1,246 @@
|
||||
---
|
||||
sidebar_position: 8
|
||||
---
|
||||
|
||||
# Movie Recommendation Assistant with Hindsight Memory
|
||||
|
||||
|
||||
:::tip Run this notebook
|
||||
This recipe is available as an interactive Jupyter notebook.
|
||||
[**Open in GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/blob/main/notebooks/movie_recommendation.ipynb)
|
||||
:::
|
||||
|
||||
|
||||
A personalized movie recommender that remembers your preferences, watch history, and tastes to give better suggestions over time.
|
||||
|
||||
## Features
|
||||
- Remembers favorite genres, directors, and actors
|
||||
- Tracks movies you've watched and enjoyed
|
||||
- Provides contextual recommendations based on mood
|
||||
|
||||
## Prerequisites
|
||||
- OpenAI API key
|
||||
- Hindsight running locally via Docker (see setup below)
|
||||
|
||||
## Start Hindsight Locally
|
||||
|
||||
Before running this notebook, start Hindsight in a terminal:
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY="your-openai-api-key"
|
||||
|
||||
docker run --rm -it --pull always -p 8888:8888 -p 9999:9999 \
|
||||
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
|
||||
-e HINDSIGHT_API_LLM_MODEL=gpt-4o-mini \
|
||||
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
|
||||
ghcr.io/vectorize-io/hindsight:latest
|
||||
```
|
||||
|
||||
## 1. Install Dependencies
|
||||
|
||||
|
||||
```python
|
||||
!pip install -q hindsight-client openai nest-asyncio
|
||||
```
|
||||
|
||||
## 2. Configure OpenAI API Key
|
||||
|
||||
Enter your OpenAI API key when prompted (used by both Hindsight and the demo).
|
||||
|
||||
|
||||
```python
|
||||
import getpass
|
||||
import os
|
||||
|
||||
# Set OpenAI API key (used by both Hindsight and the demo)
|
||||
if not os.getenv("OPENAI_API_KEY"):
|
||||
os.environ["OPENAI_API_KEY"] = getpass.getpass("Enter your OpenAI API key: ")
|
||||
|
||||
print("API key configured!")
|
||||
```
|
||||
|
||||
## 3. Initialize Clients
|
||||
|
||||
|
||||
```python
|
||||
import nest_asyncio
|
||||
nest_asyncio.apply()
|
||||
|
||||
from openai import OpenAI
|
||||
from hindsight_client import Hindsight
|
||||
|
||||
# Initialize Hindsight client (connects to local Docker instance)
|
||||
hindsight = Hindsight(
|
||||
base_url=os.getenv("HINDSIGHT_BASE_URL", "http://localhost:8888"),
|
||||
)
|
||||
|
||||
# Initialize OpenAI client
|
||||
openai_client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
|
||||
|
||||
# Unique identifier for this user's memory bank
|
||||
USER_ID = "movie-fan-demo"
|
||||
|
||||
print("Clients initialized!")
|
||||
```
|
||||
|
||||
## 4. Define Helper Functions
|
||||
|
||||
These functions demonstrate the three core Hindsight operations:
|
||||
- **retain()**: Store memories
|
||||
- **recall()**: Retrieve relevant memories
|
||||
- **reflect()**: Synthesize insights from memories
|
||||
|
||||
|
||||
```python
|
||||
def get_recommendation(user_query: str) -> str:
|
||||
"""
|
||||
Get a movie recommendation based on user query and remembered preferences.
|
||||
"""
|
||||
# Recall relevant memories about this user's movie preferences
|
||||
memories = hindsight.recall(
|
||||
bank_id=USER_ID,
|
||||
query=f"movie preferences tastes genres {user_query}",
|
||||
budget="mid",
|
||||
)
|
||||
|
||||
# Build context from memories
|
||||
memory_context = ""
|
||||
if memories and memories.results:
|
||||
memory_context = "\n".join(
|
||||
f"- {m.text}" for m in memories.results[:5]
|
||||
)
|
||||
|
||||
# Generate recommendation with context
|
||||
system_prompt = f"""You are a helpful movie recommendation assistant.
|
||||
You remember the user's preferences and past conversations to give personalized suggestions.
|
||||
|
||||
What you know about this user:
|
||||
{memory_context if memory_context else "No previous preferences recorded yet."}
|
||||
|
||||
Give thoughtful, personalized recommendations based on their tastes.
|
||||
If they mention new preferences, acknowledge them."""
|
||||
|
||||
response = openai_client.chat.completions.create(
|
||||
model="gpt-4o-mini",
|
||||
messages=[
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": user_query},
|
||||
],
|
||||
temperature=0.7,
|
||||
max_tokens=500,
|
||||
)
|
||||
|
||||
recommendation = response.choices[0].message.content
|
||||
|
||||
# Store this interaction for future context
|
||||
hindsight.retain(
|
||||
bank_id=USER_ID,
|
||||
content=f"User asked: {user_query}\nRecommendation given: {recommendation}",
|
||||
metadata={"category": "movie_recommendation"},
|
||||
)
|
||||
|
||||
return recommendation
|
||||
|
||||
|
||||
def store_preference(preference: str) -> None:
|
||||
"""Store an explicit user preference."""
|
||||
hindsight.retain(
|
||||
bank_id=USER_ID,
|
||||
content=f"User preference: {preference}",
|
||||
metadata={"category": "preference"},
|
||||
)
|
||||
print(f"Stored preference: {preference}")
|
||||
|
||||
|
||||
def get_preference_summary() -> str:
|
||||
"""Get a summary of what we know about the user's movie tastes."""
|
||||
summary = hindsight.reflect(
|
||||
bank_id=USER_ID,
|
||||
query="Summarize this user's movie preferences, favorite genres, actors they like, and movies they've mentioned enjoying or disliking.",
|
||||
budget="high",
|
||||
)
|
||||
return summary.text if hasattr(summary, 'text') else str(summary)
|
||||
|
||||
print("Helper functions defined!")
|
||||
```
|
||||
|
||||
## 5. Run the Demo
|
||||
|
||||
Watch how the assistant learns and remembers preferences across conversations.
|
||||
|
||||
|
||||
```python
|
||||
import time
|
||||
|
||||
print("=" * 60)
|
||||
print(" Movie Recommendation Assistant with Memory")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
# Simulate a conversation over time
|
||||
conversations = [
|
||||
"I'm looking for a movie to watch tonight. Any suggestions?",
|
||||
"I really loved Inception and Interstellar. Christopher Nolan is amazing!",
|
||||
"Can you suggest something similar to those? I like mind-bending plots.",
|
||||
"Actually, I'm not in the mood for something heavy. Something lighter?",
|
||||
"I watched The Grand Budapest Hotel last week and loved it!",
|
||||
"What should I watch tonight? Remember what I like!",
|
||||
]
|
||||
|
||||
for i, query in enumerate(conversations, 1):
|
||||
print(f"\n[Conversation {i}]")
|
||||
print(f"User: {query}")
|
||||
print("-" * 40)
|
||||
|
||||
response = get_recommendation(query)
|
||||
print(f"Assistant: {response}")
|
||||
print()
|
||||
|
||||
time.sleep(1)
|
||||
```
|
||||
|
||||
## 6. View Learned Preferences
|
||||
|
||||
Use `reflect()` to synthesize what Hindsight has learned about your movie tastes.
|
||||
|
||||
|
||||
```python
|
||||
print("=" * 60)
|
||||
print(" What I've learned about your movie tastes:")
|
||||
print("=" * 60)
|
||||
print(get_preference_summary())
|
||||
```
|
||||
|
||||
## 7. Try Your Own Queries
|
||||
|
||||
Experiment with your own movie preferences!
|
||||
|
||||
|
||||
```python
|
||||
# Try your own query!
|
||||
your_query = "I'm in the mood for a sci-fi thriller" # Change this!
|
||||
|
||||
print(f"You: {your_query}")
|
||||
print("-" * 40)
|
||||
print(f"Assistant: {get_recommendation(your_query)}")
|
||||
```
|
||||
|
||||
## 8. Cleanup
|
||||
|
||||
Close the Hindsight client connection.
|
||||
|
||||
|
||||
```python
|
||||
hindsight.close()
|
||||
print("Client connection closed.")
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
|
||||
```
|
||||
@@ -0,0 +1,247 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
# Per-User Memory
|
||||
|
||||
|
||||
:::tip Run this notebook
|
||||
This recipe is available as an interactive Jupyter notebook.
|
||||
[**Open in GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/blob/main/notebooks/02-per-user-memory.ipynb)
|
||||
:::
|
||||
|
||||
|
||||
The simplest pattern: give your agent persistent memory for each user. The agent remembers past conversations, user preferences, and context across sessions.
|
||||
|
||||
## The Problem
|
||||
|
||||
Without memory, every conversation starts from scratch:
|
||||
|
||||
```
|
||||
Session 1: "I prefer dark mode and use Python"
|
||||
Session 2: "What's my preferred language?" → Agent doesn't know
|
||||
```
|
||||
|
||||
## The Solution: One Bank Per User
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ User A Bank │ │ User B Bank │ │ User C Bank │
|
||||
│ │ │ │ │ │
|
||||
│ - Conversations│ │ - Conversations│ │ - Conversations│
|
||||
│ - Preferences │ │ - Preferences │ │ - Preferences │
|
||||
│ - Context │ │ - Context │ │ - Context │
|
||||
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||
│ │ │
|
||||
100% isolated 100% isolated 100% isolated
|
||||
```
|
||||
|
||||
Each user gets their own memory bank. Complete isolation, simple mental model.
|
||||
|
||||
|
||||
```python
|
||||
!pip install hindsight-client nest_asyncio openai python-dotenv -U
|
||||
```
|
||||
|
||||
## 1. Create a Bank When User Signs Up
|
||||
|
||||
|
||||
```python
|
||||
# Jupyter notebooks already run an asyncio event loop. The hindsight client
|
||||
# uses loop.run_until_complete() internally, but Python doesn't allow nested
|
||||
# event loops by default. nest_asyncio patches this to allow nesting.
|
||||
import nest_asyncio
|
||||
nest_asyncio.apply()
|
||||
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from openai import OpenAI as OpenAIClient
|
||||
|
||||
# Load environment variables from .env file
|
||||
# Copy .env.example to .env and fill in your values
|
||||
load_dotenv()
|
||||
|
||||
# Configuration (override with env vars if set)
|
||||
HINDSIGHT_API_URL = os.getenv("HINDSIGHT_API_URL", "http://localhost:8888")
|
||||
HINDSIGHT_UI_URL = os.getenv("HINDSIGHT_UI_URL", "http://localhost:9999")
|
||||
|
||||
from hindsight_client import Hindsight
|
||||
|
||||
client = Hindsight(base_url=HINDSIGHT_API_URL)
|
||||
llm = OpenAIClient() # Uses OPENAI_API_KEY from .env
|
||||
|
||||
def on_user_signup(user_id: str):
|
||||
client.create_bank(
|
||||
bank_id=f"user-{user_id}",
|
||||
name=f"Memory for {user_id}"
|
||||
)
|
||||
print(f"View bank: {HINDSIGHT_UI_URL}/banks/user-{user_id}?view=documents")
|
||||
```
|
||||
|
||||
## 2. Manage Conversation Sessions
|
||||
|
||||
Use `document_id` to group messages belonging to the same conversation. When you retain with the same `document_id`, Hindsight replaces the previous version (upsert behavior), keeping the memory up-to-date as the conversation evolves.
|
||||
|
||||
|
||||
```python
|
||||
import uuid
|
||||
import json
|
||||
|
||||
class ConversationSession:
|
||||
def __init__(self, user_id: str):
|
||||
self.user_id = user_id
|
||||
self.session_id = str(uuid.uuid4()) # Unique ID for this conversation
|
||||
self.messages = []
|
||||
|
||||
def add_message(self, role: str, content: str):
|
||||
self.messages.append({"role": role, "content": content})
|
||||
|
||||
def save(self, client: Hindsight):
|
||||
"""Save the entire conversation. Replaces previous version if session_id exists."""
|
||||
# Convert messages to string format for retain
|
||||
content = "\n".join([f"{m['role']}: {m['content']}" for m in self.messages])
|
||||
client.retain(
|
||||
bank_id=f"user-{self.user_id}",
|
||||
content=content,
|
||||
document_id=self.session_id # Same ID = upsert (replace old version)
|
||||
)
|
||||
```
|
||||
|
||||
## 3. Recall Context Before Responding
|
||||
|
||||
|
||||
```python
|
||||
def get_context(user_id: str, query: str):
|
||||
result = client.recall(
|
||||
bank_id=f"user-{user_id}",
|
||||
query=query
|
||||
)
|
||||
return result.results
|
||||
```
|
||||
|
||||
## 4. Complete Agent Loop
|
||||
|
||||
|
||||
```python
|
||||
def format_results(results):
|
||||
"""Format recall results for the prompt."""
|
||||
if not results:
|
||||
return "No relevant memories found."
|
||||
return "\n".join([f"- {r.text}" for r in results])
|
||||
|
||||
def format_messages(messages):
|
||||
"""Format conversation messages for the prompt."""
|
||||
return "\n".join([f"{m['role']}: {m['content']}" for m in messages])
|
||||
|
||||
def handle_message(session: ConversationSession, user_message: str):
|
||||
# 1. Add user message to session
|
||||
session.add_message("user", user_message)
|
||||
|
||||
# 2. Recall relevant context from past conversations
|
||||
context = client.recall(
|
||||
bank_id=f"user-{session.user_id}",
|
||||
query=user_message
|
||||
)
|
||||
|
||||
# 3. Build system prompt with memory
|
||||
system_prompt = f"""You are a helpful assistant with memory of past conversations.
|
||||
|
||||
## What you remember about this user
|
||||
{format_results(context.results)}
|
||||
|
||||
Respond helpfully and reference relevant memories when appropriate."""
|
||||
|
||||
# 4. Generate response using OpenAI
|
||||
response = llm.chat.completions.create(
|
||||
model="gpt-4o-mini",
|
||||
messages=[
|
||||
{"role": "system", "content": system_prompt},
|
||||
*[{"role": m["role"], "content": m["content"]} for m in session.messages]
|
||||
]
|
||||
)
|
||||
assistant_response = response.choices[0].message.content
|
||||
|
||||
# 5. Add assistant response to session
|
||||
session.add_message("assistant", assistant_response)
|
||||
|
||||
# 6. Save the updated conversation (upserts based on session_id)
|
||||
session.save(client)
|
||||
|
||||
print(f"User: {user_message}")
|
||||
print(f"Assistant: {assistant_response}\n")
|
||||
|
||||
return assistant_response
|
||||
```
|
||||
|
||||
## 5. Starting a New Conversation
|
||||
|
||||
|
||||
```python
|
||||
# Create the user's bank
|
||||
on_user_signup("alice")
|
||||
|
||||
# Each new conversation gets a new session with a unique ID
|
||||
session = ConversationSession(user_id="alice")
|
||||
|
||||
# Multiple exchanges in the same conversation
|
||||
handle_message(session, "Hi! I'm working on a Python project")
|
||||
handle_message(session, "Can you help me with async/await?")
|
||||
|
||||
# View the stored conversation in the UI.
|
||||
# Each message updates the same document (via document_id), so you'll see
|
||||
# the full conversation history in a single document rather than separate entries.
|
||||
print(f"\nView documents: {HINDSIGHT_UI_URL}/banks/user-alice?view=documents")
|
||||
```
|
||||
|
||||
## How Document ID Works
|
||||
|
||||
The `document_id` parameter is key to managing evolving conversations:
|
||||
|
||||
| Scenario | Behavior |
|
||||
|----------|----------|
|
||||
| First retain with `document_id="session_123"` | Creates new document |
|
||||
| Retain again with same `document_id="session_123"` | **Replaces** previous version (upsert) |
|
||||
| Retain with different `document_id="session_456"` | Creates separate document |
|
||||
| Retain without `document_id` | Creates new document each time |
|
||||
|
||||
This upsert behavior means:
|
||||
- You always retain the **full conversation** state
|
||||
- Facts are re-extracted from the complete conversation
|
||||
- No duplicate or stale facts from old versions
|
||||
- Memory stays consistent as conversations evolve
|
||||
|
||||
## What Gets Remembered
|
||||
|
||||
Hindsight automatically extracts and connects:
|
||||
|
||||
- **Facts**: "User prefers Python", "User is building a CLI tool"
|
||||
- **Entities**: People, projects, technologies mentioned
|
||||
- **Relationships**: How entities relate to each other
|
||||
- **Temporal context**: When things happened
|
||||
|
||||
You don't need to manually extract or structure this - just retain the conversations.
|
||||
|
||||
## When to Use This Pattern
|
||||
|
||||
**Good fit:**
|
||||
- Chatbots and assistants
|
||||
- Personal AI companions
|
||||
- Any 1:1 user-to-agent interaction
|
||||
|
||||
**Consider adding shared knowledge if:**
|
||||
- You have product docs or FAQs to reference
|
||||
- Multiple users need access to the same information
|
||||
- See the Support Agent with Shared Knowledge notebook
|
||||
|
||||
## Cleanup
|
||||
|
||||
Delete the banks created during this notebook:
|
||||
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
# Delete the user-alice bank
|
||||
response = requests.delete(f"{HINDSIGHT_API_URL}/v1/default/banks/user-alice")
|
||||
print(f"Deleted user-alice: {response.json()}")
|
||||
```
|
||||
@@ -0,0 +1,266 @@
|
||||
---
|
||||
sidebar_position: 9
|
||||
---
|
||||
|
||||
# Personal AI Assistant with Hindsight Memory
|
||||
|
||||
|
||||
:::tip Run this notebook
|
||||
This recipe is available as an interactive Jupyter notebook.
|
||||
[**Open in GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/blob/main/notebooks/personal_assistant.ipynb)
|
||||
:::
|
||||
|
||||
|
||||
A general-purpose personal assistant that remembers your preferences, schedule, family, work context, and past conversations.
|
||||
|
||||
## Features
|
||||
- Remembers family, work, and personal details
|
||||
- Tracks preferences and habits
|
||||
- Helps with scheduling and reminders
|
||||
- Maintains context across conversations
|
||||
|
||||
## Prerequisites
|
||||
- OpenAI API key
|
||||
- Hindsight running locally via Docker (see setup below)
|
||||
|
||||
## Start Hindsight Locally
|
||||
|
||||
Before running this notebook, start Hindsight in a terminal:
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY="your-openai-api-key"
|
||||
|
||||
docker run --rm -it --pull always -p 8888:8888 -p 9999:9999 \
|
||||
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
|
||||
-e HINDSIGHT_API_LLM_MODEL=gpt-4o-mini \
|
||||
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
|
||||
ghcr.io/vectorize-io/hindsight:latest
|
||||
```
|
||||
|
||||
## 1. Install Dependencies
|
||||
|
||||
|
||||
```python
|
||||
!pip install -q hindsight-client openai nest-asyncio
|
||||
```
|
||||
|
||||
## 2. Configure OpenAI API Key
|
||||
|
||||
Enter your OpenAI API key when prompted (used by both Hindsight and the demo).
|
||||
|
||||
|
||||
```python
|
||||
import getpass
|
||||
import os
|
||||
|
||||
# Set OpenAI API key (used by both Hindsight and the demo)
|
||||
if not os.getenv("OPENAI_API_KEY"):
|
||||
os.environ["OPENAI_API_KEY"] = getpass.getpass("Enter your OpenAI API key: ")
|
||||
|
||||
print("API key configured!")
|
||||
```
|
||||
|
||||
## 3. Initialize Clients
|
||||
|
||||
|
||||
```python
|
||||
import nest_asyncio
|
||||
nest_asyncio.apply()
|
||||
|
||||
from datetime import datetime
|
||||
from openai import OpenAI
|
||||
from hindsight_client import Hindsight
|
||||
|
||||
# Initialize Hindsight client (connects to local Docker instance)
|
||||
hindsight = Hindsight(
|
||||
base_url=os.getenv("HINDSIGHT_BASE_URL", "http://localhost:8888"),
|
||||
)
|
||||
|
||||
openai_client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
|
||||
|
||||
USER_ID = "assistant-user-demo"
|
||||
|
||||
print("Clients initialized!")
|
||||
```
|
||||
|
||||
## 4. Define Helper Functions
|
||||
|
||||
|
||||
```python
|
||||
def remember(info: str, category: str = "general") -> str:
|
||||
"""Store information to remember."""
|
||||
today = datetime.now().strftime("%B %d, %Y")
|
||||
|
||||
hindsight.retain(
|
||||
bank_id=USER_ID,
|
||||
content=f"{today}: {info}",
|
||||
metadata={"category": category, "date": today},
|
||||
)
|
||||
|
||||
return f"I'll remember: {info}"
|
||||
|
||||
|
||||
def recall_context(query: str) -> str:
|
||||
"""Recall relevant memories for context."""
|
||||
memories = hindsight.recall(
|
||||
bank_id=USER_ID,
|
||||
query=query,
|
||||
budget="high",
|
||||
)
|
||||
|
||||
if memories and memories.results:
|
||||
return "\n".join(f"- {m.text}" for m in memories.results[:8])
|
||||
return ""
|
||||
|
||||
|
||||
def chat(user_message: str) -> str:
|
||||
"""Chat with the personal assistant."""
|
||||
context = recall_context(user_message)
|
||||
|
||||
system_prompt = f"""You are a helpful personal AI assistant with long-term memory.
|
||||
You remember the user's preferences, schedule, family, work context, and past conversations.
|
||||
|
||||
What you remember about this user:
|
||||
{context if context else "No memories recorded yet."}
|
||||
|
||||
Your capabilities:
|
||||
- Remember things when asked ("Remember that...", "Don't forget...")
|
||||
- Recall past information ("What did I tell you about...", "When is...")
|
||||
- Provide personalized suggestions based on known preferences
|
||||
- Help with scheduling and reminders
|
||||
- Have natural conversations while maintaining context
|
||||
|
||||
Be helpful, proactive, and reference relevant memories naturally."""
|
||||
|
||||
response = openai_client.chat.completions.create(
|
||||
model="gpt-4o-mini",
|
||||
messages=[
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": user_message},
|
||||
],
|
||||
temperature=0.7,
|
||||
max_tokens=500,
|
||||
)
|
||||
|
||||
answer = response.choices[0].message.content
|
||||
|
||||
# Check if user is asking to remember something
|
||||
lower_msg = user_message.lower()
|
||||
if any(phrase in lower_msg for phrase in ["remember that", "don't forget", "remind me", "note that"]):
|
||||
hindsight.retain(
|
||||
bank_id=USER_ID,
|
||||
content=f"User asked to remember: {user_message}",
|
||||
metadata={"category": "reminder"},
|
||||
)
|
||||
|
||||
# Store the interaction
|
||||
hindsight.retain(
|
||||
bank_id=USER_ID,
|
||||
content=f"Conversation - User: {user_message[:100]} | Assistant: {answer[:100]}",
|
||||
metadata={"category": "conversation"},
|
||||
)
|
||||
|
||||
return answer
|
||||
|
||||
|
||||
def get_summary(topic: str = None) -> str:
|
||||
"""Get a summary of memories."""
|
||||
query = f"Summarize what you know about {topic}" if topic else \
|
||||
"Summarize everything you know about this user"
|
||||
|
||||
summary = hindsight.reflect(
|
||||
bank_id=USER_ID,
|
||||
query=query,
|
||||
budget="high",
|
||||
)
|
||||
return summary.text if hasattr(summary, 'text') else str(summary)
|
||||
|
||||
print("Helper functions defined!")
|
||||
```
|
||||
|
||||
## 5. Build Context
|
||||
|
||||
|
||||
```python
|
||||
print("Building context...")
|
||||
|
||||
initial_context = [
|
||||
("My name is Alex and I work as a product manager at TechCorp", "personal"),
|
||||
("My wife's name is Sarah and we have two kids: Emma (7) and Jack (4)", "family"),
|
||||
("I prefer morning meetings and try to keep afternoons for deep work", "preference"),
|
||||
("My mom's birthday is March 15th", "event"),
|
||||
("I'm trying to read more - currently reading 'Atomic Habits'", "hobby"),
|
||||
("I have a weekly team standup every Monday at 10am", "schedule"),
|
||||
("I'm allergic to cats", "health"),
|
||||
("My favorite coffee is a flat white with oat milk", "preference"),
|
||||
("I'm training for a half marathon in April", "goal"),
|
||||
]
|
||||
|
||||
for info, category in initial_context:
|
||||
result = remember(info, category)
|
||||
print(f" {result}")
|
||||
```
|
||||
|
||||
## 6. Have a Conversation
|
||||
|
||||
|
||||
```python
|
||||
import time
|
||||
|
||||
print("=" * 60)
|
||||
print(" Conversation")
|
||||
print("=" * 60)
|
||||
|
||||
conversations = [
|
||||
"Hey, what's my wife's name again?",
|
||||
"Remember that my Q1 review is next Thursday at 2pm",
|
||||
"I need a gift idea for my mom's birthday",
|
||||
"What time is my Monday standup?",
|
||||
"Can you recommend a coffee order for me?",
|
||||
"What books am I reading?",
|
||||
]
|
||||
|
||||
for message in conversations:
|
||||
print(f"\nAlex: {message}")
|
||||
print("-" * 40)
|
||||
response = chat(message)
|
||||
print(f"Assistant: {response}")
|
||||
time.sleep(1)
|
||||
```
|
||||
|
||||
## 7. View Summary
|
||||
|
||||
|
||||
```python
|
||||
print("=" * 60)
|
||||
print(" What I Know About You")
|
||||
print("=" * 60)
|
||||
print(get_summary())
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
print("=" * 60)
|
||||
print(" Your Family")
|
||||
print("=" * 60)
|
||||
print(get_summary("family"))
|
||||
```
|
||||
|
||||
## 8. Try Your Own Message
|
||||
|
||||
|
||||
```python
|
||||
your_message = "What should I focus on this month with my training?" # Change this!
|
||||
|
||||
print(f"You: {your_message}")
|
||||
print("-" * 40)
|
||||
print(f"Assistant: {chat(your_message)}")
|
||||
```
|
||||
|
||||
## 9. Cleanup
|
||||
|
||||
|
||||
```python
|
||||
hindsight.close()
|
||||
print("Client connection closed.")
|
||||
```
|
||||
@@ -0,0 +1,299 @@
|
||||
---
|
||||
sidebar_position: 10
|
||||
---
|
||||
|
||||
# Personalized Search Agent with Hindsight Memory
|
||||
|
||||
|
||||
:::tip Run this notebook
|
||||
This recipe is available as an interactive Jupyter notebook.
|
||||
[**Open in GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/blob/main/notebooks/personalized_search.ipynb)
|
||||
:::
|
||||
|
||||
|
||||
A search assistant that learns your preferences, location, dietary needs, and lifestyle to provide contextually relevant search results.
|
||||
|
||||
## Features
|
||||
- Learns location, dietary restrictions, and lifestyle
|
||||
- Personalizes search queries based on context
|
||||
- Remembers past searches and preferences
|
||||
- Integrates with Tavily for real web search (optional)
|
||||
|
||||
## Prerequisites
|
||||
- OpenAI API key
|
||||
- Hindsight running locally via Docker (see setup below)
|
||||
- Tavily API key (optional, for real web search)
|
||||
|
||||
## Start Hindsight Locally
|
||||
|
||||
Before running this notebook, start Hindsight in a terminal:
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY="your-openai-api-key"
|
||||
|
||||
docker run --rm -it --pull always -p 8888:8888 -p 9999:9999 \
|
||||
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
|
||||
-e HINDSIGHT_API_LLM_MODEL=gpt-4o-mini \
|
||||
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
|
||||
ghcr.io/vectorize-io/hindsight:latest
|
||||
```
|
||||
|
||||
## 1. Install Dependencies
|
||||
|
||||
|
||||
```python
|
||||
# Tavily is optional - demo works with simulated results if not installed
|
||||
!pip install -q hindsight-client openai tavily-python nest-asyncio
|
||||
```
|
||||
|
||||
## 2. Configure API Keys
|
||||
|
||||
Enter your API keys when prompted. Tavily is optional - press Enter to skip for simulated search results.
|
||||
|
||||
|
||||
```python
|
||||
import getpass
|
||||
import os
|
||||
|
||||
# Set OpenAI API key (used by both Hindsight and the demo)
|
||||
if not os.getenv("OPENAI_API_KEY"):
|
||||
os.environ["OPENAI_API_KEY"] = getpass.getpass("Enter your OpenAI API key: ")
|
||||
|
||||
# Tavily is optional - for real web search
|
||||
if not os.getenv("TAVILY_API_KEY"):
|
||||
tavily_key = getpass.getpass("Enter your Tavily API key (or press Enter to skip): ")
|
||||
if tavily_key:
|
||||
os.environ["TAVILY_API_KEY"] = tavily_key
|
||||
|
||||
print("API keys configured!")
|
||||
```
|
||||
|
||||
## 3. Initialize Clients
|
||||
|
||||
|
||||
```python
|
||||
import nest_asyncio
|
||||
nest_asyncio.apply()
|
||||
|
||||
from openai import OpenAI
|
||||
from hindsight_client import Hindsight
|
||||
|
||||
# Initialize Hindsight client (connects to local Docker instance)
|
||||
hindsight = Hindsight(
|
||||
base_url=os.getenv("HINDSIGHT_BASE_URL", "http://localhost:8888"),
|
||||
)
|
||||
|
||||
openai_client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
|
||||
|
||||
# Optional: Tavily for real web search
|
||||
try:
|
||||
from tavily import TavilyClient
|
||||
tavily = TavilyClient(api_key=os.getenv("TAVILY_API_KEY"))
|
||||
HAS_TAVILY = True
|
||||
print("Tavily configured - using real web search!")
|
||||
except (ImportError, Exception) as e:
|
||||
HAS_TAVILY = False
|
||||
print("Note: Using simulated search results (Tavily not configured)")
|
||||
|
||||
USER_ID = "search-user-demo"
|
||||
|
||||
print("Clients initialized!")
|
||||
```
|
||||
|
||||
## 4. Define Helper Functions
|
||||
|
||||
|
||||
```python
|
||||
def store_preference(preference: str) -> str:
|
||||
"""Store a user preference."""
|
||||
hindsight.retain(
|
||||
bank_id=USER_ID,
|
||||
content=f"User preference: {preference}",
|
||||
metadata={"category": "preference"},
|
||||
)
|
||||
return f"Learned: {preference}"
|
||||
|
||||
|
||||
def store_interaction(query: str, response: str) -> None:
|
||||
"""Store a search interaction."""
|
||||
hindsight.retain(
|
||||
bank_id=USER_ID,
|
||||
content=f"Search query: {query}\nResult highlights: {response[:200]}",
|
||||
metadata={"category": "search_history"},
|
||||
)
|
||||
|
||||
|
||||
def get_user_context(query: str) -> str:
|
||||
"""Retrieve relevant user context."""
|
||||
memories = hindsight.recall(
|
||||
bank_id=USER_ID,
|
||||
query=f"preferences location dietary lifestyle {query}",
|
||||
budget="mid",
|
||||
)
|
||||
|
||||
if memories and memories.results:
|
||||
return "\n".join(f"- {m.text}" for m in memories.results[:6])
|
||||
return ""
|
||||
|
||||
|
||||
def personalized_search(query: str) -> str:
|
||||
"""Perform a personalized search."""
|
||||
user_context = get_user_context(query)
|
||||
|
||||
enhancement_prompt = f"""Given this user's preferences and the search query, suggest how to enhance the search.
|
||||
|
||||
User preferences:
|
||||
{user_context if user_context else "No preferences recorded yet."}
|
||||
|
||||
Search query: {query}
|
||||
|
||||
Return a JSON object with:
|
||||
- "enhanced_query": The improved search query incorporating relevant preferences
|
||||
- "filters": Any specific filters to apply (e.g., "vegetarian", "within 5 miles")
|
||||
- "reasoning": Brief explanation of personalizations applied"""
|
||||
|
||||
enhancement = openai_client.chat.completions.create(
|
||||
model="gpt-4o-mini",
|
||||
messages=[{"role": "user", "content": enhancement_prompt}],
|
||||
temperature=0.3,
|
||||
max_tokens=300,
|
||||
)
|
||||
|
||||
enhanced_info = enhancement.choices[0].message.content
|
||||
|
||||
# Perform the search
|
||||
if HAS_TAVILY:
|
||||
search_results = tavily.search(
|
||||
query=query,
|
||||
search_depth="advanced",
|
||||
max_results=5,
|
||||
)
|
||||
results_text = "\n".join(
|
||||
f"- {r['title']}: {r['content'][:150]}..."
|
||||
for r in search_results.get('results', [])
|
||||
)
|
||||
else:
|
||||
results_text = f"[Simulated search results for: {query}]"
|
||||
|
||||
response_prompt = f"""Based on the search results and user preferences, provide a personalized summary.
|
||||
|
||||
User preferences:
|
||||
{user_context if user_context else "No preferences recorded yet."}
|
||||
|
||||
Query: {query}
|
||||
|
||||
Search enhancement applied:
|
||||
{enhanced_info}
|
||||
|
||||
Search results:
|
||||
{results_text}
|
||||
|
||||
Provide a helpful, personalized response that takes into account their preferences."""
|
||||
|
||||
response = openai_client.chat.completions.create(
|
||||
model="gpt-4o-mini",
|
||||
messages=[{"role": "user", "content": response_prompt}],
|
||||
temperature=0.7,
|
||||
max_tokens=500,
|
||||
)
|
||||
|
||||
answer = response.choices[0].message.content
|
||||
store_interaction(query, answer)
|
||||
|
||||
return answer
|
||||
|
||||
|
||||
def get_preference_profile() -> str:
|
||||
"""Get a summary of the user's preference profile."""
|
||||
profile = hindsight.reflect(
|
||||
bank_id=USER_ID,
|
||||
query="""Summarize what we know about this user:
|
||||
- Location and neighborhood
|
||||
- Dietary preferences and restrictions
|
||||
- Work style and schedule
|
||||
- Hobbies and interests
|
||||
- Family situation
|
||||
- Shopping preferences""",
|
||||
budget="high",
|
||||
)
|
||||
return profile.text if hasattr(profile, 'text') else str(profile)
|
||||
|
||||
print("Helper functions defined!")
|
||||
```
|
||||
|
||||
## 5. Build User Profile
|
||||
|
||||
|
||||
```python
|
||||
print("Learning user preferences...")
|
||||
|
||||
preferences = [
|
||||
"Lives in San Francisco, Mission District",
|
||||
"Works remotely as a software engineer",
|
||||
"Vegetarian, prefers organic food when possible",
|
||||
"Has a 5-year-old daughter named Emma",
|
||||
"Enjoys hiking and outdoor activities on weekends",
|
||||
"Prefers quiet coffee shops for remote work",
|
||||
"Lactose intolerant, uses oat milk",
|
||||
"Interested in sustainable and eco-friendly products",
|
||||
"Usually free on Tuesday and Thursday afternoons",
|
||||
"Husband is allergic to nuts",
|
||||
]
|
||||
|
||||
for pref in preferences:
|
||||
result = store_preference(pref)
|
||||
print(f" {result}")
|
||||
```
|
||||
|
||||
## 6. Personalized Search Results
|
||||
|
||||
|
||||
```python
|
||||
import time
|
||||
|
||||
print("=" * 60)
|
||||
print(" Personalized Search Results")
|
||||
print("=" * 60)
|
||||
|
||||
searches = [
|
||||
"Find a good coffee shop for working remotely",
|
||||
"Restaurant recommendations for a family dinner",
|
||||
"Birthday gift ideas for a 5-year-old",
|
||||
]
|
||||
|
||||
for query in searches:
|
||||
print(f"\nSearch: {query}")
|
||||
print("-" * 40)
|
||||
result = personalized_search(query)
|
||||
print(result)
|
||||
time.sleep(1)
|
||||
```
|
||||
|
||||
## 7. View Preference Profile
|
||||
|
||||
|
||||
```python
|
||||
print("=" * 60)
|
||||
print(" User Preference Profile")
|
||||
print("=" * 60)
|
||||
print(get_preference_profile())
|
||||
```
|
||||
|
||||
## 8. Try Your Own Search
|
||||
|
||||
|
||||
```python
|
||||
your_search = "Best hiking trails near me" # Change this!
|
||||
|
||||
print(f"Search: {your_search}")
|
||||
print("-" * 40)
|
||||
print(personalized_search(your_search))
|
||||
```
|
||||
|
||||
## 9. Cleanup
|
||||
|
||||
|
||||
```python
|
||||
hindsight.close()
|
||||
print("Client connection closed.")
|
||||
```
|
||||
@@ -0,0 +1,162 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# Hindsight Quickstart
|
||||
|
||||
|
||||
:::tip Run this notebook
|
||||
This recipe is available as an interactive Jupyter notebook.
|
||||
[**Open in GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/blob/main/notebooks/01-quickstart.ipynb)
|
||||
:::
|
||||
|
||||
|
||||
This notebook covers the basics of using Hindsight:
|
||||
- **Retain**: Store information in memory
|
||||
- **Recall**: Retrieve memories matching a query
|
||||
- **Reflect**: Generate insights from memories
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Make sure you have Hindsight running. The easiest way is via Docker:
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY=your-key
|
||||
|
||||
docker run --rm -it --pull always -p 8888:8888 -p 9999:9999 \
|
||||
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
|
||||
-e HINDSIGHT_API_LLM_MODEL=o3-mini \
|
||||
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
|
||||
ghcr.io/vectorize-io/hindsight:latest
|
||||
```
|
||||
|
||||
- API: http://localhost:8888
|
||||
- UI: http://localhost:9999
|
||||
|
||||
## Installation
|
||||
|
||||
Install the Hindsight Python client:
|
||||
|
||||
|
||||
```python
|
||||
!pip install hindsight-client nest_asyncio python-dotenv -U
|
||||
```
|
||||
|
||||
## Connect to Hindsight
|
||||
|
||||
|
||||
```python
|
||||
# Jupyter notebooks already run an asyncio event loop. The hindsight client
|
||||
# uses loop.run_until_complete() internally, but Python doesn't allow nested
|
||||
# event loops by default. nest_asyncio patches this to allow nesting.
|
||||
import nest_asyncio
|
||||
nest_asyncio.apply()
|
||||
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
# Copy .env.example to .env and fill in your values
|
||||
load_dotenv()
|
||||
|
||||
# Configuration (override with env vars if set)
|
||||
HINDSIGHT_API_URL = os.getenv("HINDSIGHT_API_URL", "http://localhost:8888")
|
||||
HINDSIGHT_UI_URL = os.getenv("HINDSIGHT_UI_URL", "http://localhost:9999")
|
||||
|
||||
from hindsight_client import Hindsight
|
||||
|
||||
client = Hindsight(base_url=HINDSIGHT_API_URL)
|
||||
```
|
||||
|
||||
## Retain: Store Information
|
||||
|
||||
The `retain` operation is used to push new memories into Hindsight. It tells Hindsight to _retain_ the information you pass in.
|
||||
|
||||
Behind the scenes, the retain operation uses an LLM to extract key facts, temporal data, entities, and relationships.
|
||||
|
||||
|
||||
```python
|
||||
# Simple retain
|
||||
client.retain(
|
||||
bank_id="my-bank",
|
||||
content="Alice works at Google as a software engineer"
|
||||
)
|
||||
|
||||
# View the stored document in the UI:
|
||||
print(f"View documents: {HINDSIGHT_UI_URL}/banks/my-bank?view=documents")
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
# Retain with context and timestamp
|
||||
client.retain(
|
||||
bank_id="my-bank",
|
||||
content="Alice got promoted to senior engineer",
|
||||
context="career update",
|
||||
timestamp="2025-06-15T10:00:00Z"
|
||||
)
|
||||
```
|
||||
|
||||
## Recall: Retrieve Memories
|
||||
|
||||
The `recall` operation retrieves memories matching a query. It performs 4 retrieval strategies in parallel:
|
||||
- **Semantic**: Vector similarity
|
||||
- **Keyword**: BM25 exact matching
|
||||
- **Graph**: Entity/temporal/causal links
|
||||
- **Temporal**: Time range filtering
|
||||
|
||||
|
||||
```python
|
||||
# Simple recall
|
||||
results = client.recall(bank_id="my-bank", query="What does Alice do?")
|
||||
|
||||
print("Memories:")
|
||||
for r in results.results:
|
||||
print(f" - {r.text}")
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
# Temporal recall
|
||||
results = client.recall(bank_id="my-bank", query="What happened in June?")
|
||||
|
||||
print("Memories:")
|
||||
for r in results.results:
|
||||
print(f" - {r.text}")
|
||||
```
|
||||
|
||||
## Reflect: Generate Insights
|
||||
|
||||
The `reflect` operation performs a more thorough analysis of existing memories. This allows the agent to form new connections between memories which are then persisted as opinions and/or observations.
|
||||
|
||||
Example use cases:
|
||||
- An AI Project Manager reflecting on what risks need to be mitigated
|
||||
- A Sales Agent reflecting on why certain outreach messages have gotten responses
|
||||
- A Support Agent reflecting on opportunities where customers have unanswered questions
|
||||
|
||||
|
||||
```python
|
||||
response = client.reflect(bank_id="my-bank", query="What should I know about Alice?")
|
||||
print(response)
|
||||
```
|
||||
|
||||
## Memory Types
|
||||
|
||||
Hindsight organizes memory into four networks to mimic human memory:
|
||||
|
||||
- **World**: Facts about the world ("The stove gets hot")
|
||||
- **Experiences**: Agent's own experiences ("I touched the stove and it really hurt")
|
||||
- **Opinion**: Beliefs with confidence scores ("I shouldn't touch the stove again" - .99 confidence)
|
||||
- **Observation**: Complex mental models derived by reflecting on facts and experiences
|
||||
|
||||
## Cleanup
|
||||
|
||||
Delete the bank created during this notebook:
|
||||
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
response = requests.delete(f"{HINDSIGHT_API_URL}/v1/default/banks/my-bank")
|
||||
print(f"Deleted my-bank: {response.json()}")
|
||||
```
|
||||
@@ -0,0 +1,335 @@
|
||||
---
|
||||
sidebar_position: 11
|
||||
---
|
||||
|
||||
# Study Buddy with Hindsight Memory
|
||||
|
||||
|
||||
:::tip Run this notebook
|
||||
This recipe is available as an interactive Jupyter notebook.
|
||||
[**Open in GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/blob/main/notebooks/study_buddy.ipynb)
|
||||
:::
|
||||
|
||||
|
||||
A personalized study assistant that tracks what you've learned, identifies knowledge gaps, and helps with spaced repetition.
|
||||
|
||||
## Features
|
||||
- Tracks study sessions and topics covered
|
||||
- Monitors confidence levels per topic
|
||||
- Identifies knowledge gaps
|
||||
- Suggests topics for spaced repetition review
|
||||
|
||||
## Prerequisites
|
||||
- OpenAI API key
|
||||
- Hindsight running locally via Docker (see setup below)
|
||||
|
||||
## Start Hindsight Locally
|
||||
|
||||
Before running this notebook, start Hindsight in a terminal:
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY="your-openai-api-key"
|
||||
|
||||
docker run --rm -it --pull always -p 8888:8888 -p 9999:9999 \
|
||||
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
|
||||
-e HINDSIGHT_API_LLM_MODEL=gpt-4o-mini \
|
||||
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
|
||||
ghcr.io/vectorize-io/hindsight:latest
|
||||
```
|
||||
|
||||
## 1. Install Dependencies
|
||||
|
||||
|
||||
```python
|
||||
!pip install -q hindsight-client openai nest-asyncio
|
||||
```
|
||||
|
||||
## 2. Configure OpenAI API Key
|
||||
|
||||
Enter your OpenAI API key when prompted (used by both Hindsight and the demo).
|
||||
|
||||
|
||||
```python
|
||||
import getpass
|
||||
import os
|
||||
|
||||
# Set OpenAI API key (used by both Hindsight and the demo)
|
||||
if not os.getenv("OPENAI_API_KEY"):
|
||||
os.environ["OPENAI_API_KEY"] = getpass.getpass("Enter your OpenAI API key: ")
|
||||
|
||||
print("API key configured!")
|
||||
```
|
||||
|
||||
## 3. Initialize Clients
|
||||
|
||||
|
||||
```python
|
||||
import nest_asyncio
|
||||
nest_asyncio.apply()
|
||||
|
||||
from datetime import datetime
|
||||
from openai import OpenAI
|
||||
from hindsight_client import Hindsight
|
||||
|
||||
# Initialize Hindsight client (connects to local Docker instance)
|
||||
hindsight = Hindsight(
|
||||
base_url=os.getenv("HINDSIGHT_BASE_URL", "http://localhost:8888"),
|
||||
)
|
||||
|
||||
openai_client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
|
||||
|
||||
USER_ID = "student-demo"
|
||||
|
||||
print("Clients initialized!")
|
||||
```
|
||||
|
||||
## 4. Define Helper Functions
|
||||
|
||||
|
||||
```python
|
||||
def record_study_session(topic: str, notes: str, confidence: str = "medium") -> str:
|
||||
"""Record a study session with topic, notes, and self-assessed confidence."""
|
||||
today = datetime.now().strftime("%B %d, %Y")
|
||||
|
||||
content = f"""{today} - STUDY SESSION
|
||||
Topic: {topic}
|
||||
Confidence Level: {confidence}
|
||||
Notes: {notes}"""
|
||||
|
||||
hindsight.retain(
|
||||
bank_id=USER_ID,
|
||||
content=content,
|
||||
metadata={
|
||||
"category": "study_session",
|
||||
"topic": topic,
|
||||
"confidence": confidence,
|
||||
"date": today,
|
||||
},
|
||||
)
|
||||
|
||||
return f"Recorded study session on '{topic}' (confidence: {confidence})"
|
||||
|
||||
|
||||
def record_question(topic: str, question: str, understood: bool) -> str:
|
||||
"""Record a question asked during study."""
|
||||
today = datetime.now().strftime("%B %d, %Y")
|
||||
|
||||
content = f"""{today} - QUESTION
|
||||
Topic: {topic}
|
||||
Question: {question}
|
||||
Understood: {"Yes" if understood else "No - needs review"}"""
|
||||
|
||||
hindsight.retain(
|
||||
bank_id=USER_ID,
|
||||
content=content,
|
||||
metadata={
|
||||
"category": "question",
|
||||
"topic": topic,
|
||||
"understood": str(understood),
|
||||
},
|
||||
)
|
||||
|
||||
return f"Recorded question on '{topic}'"
|
||||
|
||||
|
||||
def study_buddy(user_query: str) -> str:
|
||||
"""Interact with the study buddy."""
|
||||
memories = hindsight.recall(
|
||||
bank_id=USER_ID,
|
||||
query=f"study session topic notes questions {user_query}",
|
||||
budget="high",
|
||||
)
|
||||
|
||||
memory_context = ""
|
||||
if memories and memories.results:
|
||||
memory_context = "\n".join(f"- {m.text}" for m in memories.results[:8])
|
||||
|
||||
system_prompt = f"""You are a helpful study buddy and tutor.
|
||||
You have access to the student's study history, including:
|
||||
- Topics they've studied and their notes
|
||||
- Their self-assessed confidence levels
|
||||
- Questions they've asked and whether they understood the answers
|
||||
|
||||
Study History:
|
||||
{memory_context if memory_context else "No study history recorded yet."}
|
||||
|
||||
Your role:
|
||||
1. Answer questions about topics they're studying
|
||||
2. Identify knowledge gaps based on their history
|
||||
3. Suggest topics to review (spaced repetition)
|
||||
4. Provide encouragement and study tips
|
||||
5. Connect new concepts to things they've already learned
|
||||
|
||||
Be supportive and pedagogical. Reference their previous learning when relevant."""
|
||||
|
||||
response = openai_client.chat.completions.create(
|
||||
model="gpt-4o-mini",
|
||||
messages=[
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": user_query},
|
||||
],
|
||||
temperature=0.7,
|
||||
max_tokens=800,
|
||||
)
|
||||
|
||||
answer = response.choices[0].message.content
|
||||
|
||||
hindsight.retain(
|
||||
bank_id=USER_ID,
|
||||
content=f"Student asked: {user_query}\nExplanation given: {answer[:300]}...",
|
||||
metadata={"category": "tutoring"},
|
||||
)
|
||||
|
||||
return answer
|
||||
|
||||
|
||||
def get_review_suggestions() -> str:
|
||||
"""Get suggestions for topics to review."""
|
||||
suggestions = hindsight.reflect(
|
||||
bank_id=USER_ID,
|
||||
query="""Analyze this student's study history and suggest:
|
||||
1. Topics with low confidence that need more review
|
||||
2. Topics studied a while ago that should be revisited
|
||||
3. Questions that weren't fully understood
|
||||
4. Connections between topics they might have missed
|
||||
|
||||
Prioritize by what would most improve their understanding.""",
|
||||
budget="high",
|
||||
)
|
||||
return suggestions.text if hasattr(suggestions, 'text') else str(suggestions)
|
||||
|
||||
|
||||
def get_knowledge_summary(topic: str = None) -> str:
|
||||
"""Get a summary of what the student knows."""
|
||||
query = f"Summarize what this student knows about {topic}" if topic else \
|
||||
"Summarize this student's overall knowledge and progress"
|
||||
|
||||
summary = hindsight.reflect(
|
||||
bank_id=USER_ID,
|
||||
query=query,
|
||||
budget="high",
|
||||
)
|
||||
return summary.text if hasattr(summary, 'text') else str(summary)
|
||||
|
||||
print("Helper functions defined!")
|
||||
```
|
||||
|
||||
## 5. Record Study Sessions
|
||||
|
||||
|
||||
```python
|
||||
print("Recording study sessions...")
|
||||
|
||||
sessions = [
|
||||
{
|
||||
"topic": "Classical Mechanics - Newton's Laws",
|
||||
"notes": "Covered F=ma, action-reaction pairs, inertia. Solved problems on inclined planes.",
|
||||
"confidence": "high",
|
||||
},
|
||||
{
|
||||
"topic": "Classical Mechanics - Conservation of Momentum",
|
||||
"notes": "Elastic vs inelastic collisions. Struggled with 2D collision problems.",
|
||||
"confidence": "low",
|
||||
},
|
||||
{
|
||||
"topic": "Classical Mechanics - Generalized Coordinates",
|
||||
"notes": "Introduction to Lagrangian mechanics. Degrees of freedom concept.",
|
||||
"confidence": "medium",
|
||||
},
|
||||
{
|
||||
"topic": "Waves - Simple Harmonic Motion",
|
||||
"notes": "SHM equations, period, frequency. Connected to springs and pendulums.",
|
||||
"confidence": "high",
|
||||
},
|
||||
{
|
||||
"topic": "Waves - Frequency Domain",
|
||||
"notes": "Started Fourier transforms. Math is confusing, need more practice.",
|
||||
"confidence": "low",
|
||||
},
|
||||
]
|
||||
|
||||
for session in sessions:
|
||||
result = record_study_session(**session)
|
||||
print(f" {result}")
|
||||
```
|
||||
|
||||
## 6. Record Questions
|
||||
|
||||
|
||||
```python
|
||||
print("Recording questions...")
|
||||
|
||||
questions = [
|
||||
("Conservation of Momentum", "Why is momentum conserved in collisions?", True),
|
||||
("Conservation of Momentum", "How do I solve 2D collision problems?", False),
|
||||
("Generalized Coordinates", "What's the advantage of Lagrangian over Newtonian?", True),
|
||||
("Frequency Domain", "When do I use Fourier transforms vs Laplace?", False),
|
||||
]
|
||||
|
||||
for topic, question, understood in questions:
|
||||
result = record_question(topic, question, understood)
|
||||
print(f" {result}")
|
||||
```
|
||||
|
||||
## 7. Interactive Study Session
|
||||
|
||||
|
||||
```python
|
||||
import time
|
||||
|
||||
print("=" * 60)
|
||||
print(" Study Session")
|
||||
print("=" * 60)
|
||||
|
||||
queries = [
|
||||
"Can you explain generalized coordinates again? I remember we covered it but I'm fuzzy on the details.",
|
||||
"What topics should I review before my exam next week?",
|
||||
"I'm still confused about 2D collision problems. Can you walk me through an example?",
|
||||
]
|
||||
|
||||
for query in queries:
|
||||
print(f"\nStudent: {query}")
|
||||
print("-" * 40)
|
||||
response = study_buddy(query)
|
||||
print(f"Study Buddy: {response}")
|
||||
time.sleep(1)
|
||||
```
|
||||
|
||||
## 8. Get Review Suggestions
|
||||
|
||||
|
||||
```python
|
||||
print("=" * 60)
|
||||
print(" Recommended Review Topics")
|
||||
print("=" * 60)
|
||||
print(get_review_suggestions())
|
||||
```
|
||||
|
||||
## 9. Knowledge Summary
|
||||
|
||||
|
||||
```python
|
||||
print("=" * 60)
|
||||
print(" Knowledge Summary")
|
||||
print("=" * 60)
|
||||
print(get_knowledge_summary())
|
||||
```
|
||||
|
||||
## 10. Try Your Own Question
|
||||
|
||||
|
||||
```python
|
||||
your_question = "What are my biggest knowledge gaps right now?" # Change this!
|
||||
|
||||
print(f"You: {your_question}")
|
||||
print("-" * 40)
|
||||
print(f"Study Buddy: {study_buddy(your_question)}")
|
||||
```
|
||||
|
||||
## 11. Cleanup
|
||||
|
||||
|
||||
```python
|
||||
hindsight.close()
|
||||
print("Client connection closed.")
|
||||
```
|
||||
@@ -0,0 +1,315 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
# Support Agent with Shared Knowledge
|
||||
|
||||
|
||||
:::tip Run this notebook
|
||||
This recipe is available as an interactive Jupyter notebook.
|
||||
[**Open in GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/blob/main/notebooks/03-support-agent-shared-knowledge.ipynb)
|
||||
:::
|
||||
|
||||
|
||||
This pattern shows how to build a support agent that combines **per-user memory** with **shared product knowledge** (RAG), giving users personalized support while leveraging a single source of truth for documentation.
|
||||
|
||||
## The Problem
|
||||
|
||||
You're building a support agent that needs to:
|
||||
- Remember each user's history, preferences, and past issues
|
||||
- Access shared product documentation
|
||||
- Keep user data completely isolated from other users
|
||||
|
||||
A naive approach would index product docs into each user's memory bank, but this is expensive and wasteful (N copies for N users).
|
||||
|
||||
## The Solution: Multi-Bank Architecture
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ User A Bank │ │ User B Bank │ │ Shared Docs │
|
||||
│ │ │ │ │ Bank │
|
||||
│ - Conversations│ │ - Conversations│ │ │
|
||||
│ - Preferences │ │ - Preferences │ │ - Product docs │
|
||||
│ - Past issues │ │ - Past issues │ │ - FAQs │
|
||||
│ - Solutions │ │ - Solutions │ │ - Guides │
|
||||
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
|
||||
│ │ │
|
||||
└───────────────────────┴───────────────────────┘
|
||||
│
|
||||
Agent queries
|
||||
multiple banks
|
||||
```
|
||||
|
||||
**Key benefits:**
|
||||
- Product docs indexed once, shared by all users
|
||||
- User memory is 100% isolated
|
||||
- Simple mental model, no complex filtering
|
||||
|
||||
|
||||
```python
|
||||
!pip install hindsight-client nest_asyncio openai python-dotenv -U
|
||||
```
|
||||
|
||||
## 1. Set Up Memory Banks
|
||||
|
||||
Create three types of banks:
|
||||
|
||||
|
||||
```python
|
||||
# Jupyter notebooks already run an asyncio event loop. The hindsight client
|
||||
# uses loop.run_until_complete() internally, but Python doesn't allow nested
|
||||
# event loops by default. nest_asyncio patches this to allow nesting.
|
||||
import nest_asyncio
|
||||
nest_asyncio.apply()
|
||||
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from openai import OpenAI as OpenAIClient
|
||||
|
||||
# Load environment variables from .env file
|
||||
# Copy .env.example to .env and fill in your values
|
||||
load_dotenv()
|
||||
|
||||
# Configuration (override with env vars if set)
|
||||
HINDSIGHT_API_URL = os.getenv("HINDSIGHT_API_URL", "http://localhost:8888")
|
||||
HINDSIGHT_UI_URL = os.getenv("HINDSIGHT_UI_URL", "http://localhost:9999")
|
||||
|
||||
from hindsight_client import Hindsight
|
||||
|
||||
client = Hindsight(base_url=HINDSIGHT_API_URL)
|
||||
llm = OpenAIClient() # Uses OPENAI_API_KEY from .env
|
||||
|
||||
# Shared knowledge bank (created once)
|
||||
shared_bank = client.create_bank(
|
||||
bank_id="product-docs",
|
||||
name="Product Documentation"
|
||||
)
|
||||
|
||||
# Per-user banks (created when user signs up)
|
||||
def create_user_bank(user_id: str):
|
||||
return client.create_bank(
|
||||
bank_id=f"user-{user_id}",
|
||||
name=f"Memory for {user_id}"
|
||||
)
|
||||
```
|
||||
|
||||
## 2. Index Product Documentation
|
||||
|
||||
Index your product docs into the shared bank (do this once, or on doc updates):
|
||||
|
||||
|
||||
```python
|
||||
# Index product documentation - retain each doc separately
|
||||
client.retain(
|
||||
bank_id="product-docs",
|
||||
content="# Pricing Tiers\n\nBasic: $10/mo, Pro: $25/mo, Enterprise: Contact us"
|
||||
)
|
||||
|
||||
client.retain(
|
||||
bank_id="product-docs",
|
||||
content="# Getting Started\n\nTo set up your account, visit the dashboard and click 'New Project'"
|
||||
)
|
||||
|
||||
# View the stored documents in the UI:
|
||||
print(f"View documents: {HINDSIGHT_UI_URL}/banks/product-docs?view=documents")
|
||||
```
|
||||
|
||||
## 3. Store User Conversations
|
||||
|
||||
After each support interaction, retain it in the user's bank:
|
||||
|
||||
|
||||
```python
|
||||
def save_conversation(user_id: str, messages: list):
|
||||
# Convert messages to string format
|
||||
content = "\n".join([f"{m['role']}: {m['content']}" for m in messages])
|
||||
client.retain(
|
||||
bank_id=f"user-{user_id}",
|
||||
content=content
|
||||
)
|
||||
```
|
||||
|
||||
## 4. Query Multiple Banks at Support Time
|
||||
|
||||
When handling a user query, retrieve context from both banks:
|
||||
|
||||
|
||||
```python
|
||||
def get_support_context(user_id: str, query: str):
|
||||
# Get user's personal context
|
||||
user_context = client.recall(
|
||||
bank_id=f"user-{user_id}",
|
||||
query=query
|
||||
)
|
||||
|
||||
# Get relevant product documentation
|
||||
docs_context = client.recall(
|
||||
bank_id="product-docs",
|
||||
query=query
|
||||
)
|
||||
|
||||
return {
|
||||
"user_history": user_context.results,
|
||||
"documentation": docs_context.results
|
||||
}
|
||||
```
|
||||
|
||||
## 5. Build the Agent Prompt
|
||||
|
||||
Combine both contexts in your agent's prompt:
|
||||
|
||||
|
||||
```python
|
||||
def format_results(results):
|
||||
"""Format recall results for the prompt."""
|
||||
if not results:
|
||||
return "No relevant information found."
|
||||
return "\n".join([f"- {r.text}" for r in results])
|
||||
|
||||
def build_prompt(query: str, context: dict) -> str:
|
||||
return f"""You are a helpful support agent.
|
||||
|
||||
## User's History
|
||||
{format_results(context["user_history"])}
|
||||
|
||||
## Product Documentation
|
||||
{format_results(context["documentation"])}
|
||||
|
||||
## Current Question
|
||||
{query}
|
||||
|
||||
Use the user's history to personalize your response and the documentation
|
||||
for accurate product information. If you find a solution, remember it for
|
||||
future reference.
|
||||
"""
|
||||
```
|
||||
|
||||
## Promoting Learnings to Shared Knowledge
|
||||
|
||||
When the agent discovers a solution that's not in the docs, you can optionally promote it to a "learnings" bank:
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ User A Bank │ │ Shared Docs │ │ Learnings │
|
||||
│ │ │ Bank │ │ Bank │
|
||||
│ - Conversations│ │ │ │ │
|
||||
│ - Preferences │ │ - Product docs │ │ - Verified │
|
||||
│ - Past issues │ │ - FAQs │ │ solutions │
|
||||
│ - Solutions │ │ - Guides │ │ - Workarounds │
|
||||
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
|
||||
│ │ │
|
||||
└───────────────────────┴───────────────────────┘
|
||||
│
|
||||
Agent queries
|
||||
all three banks
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
# Optional: Create a curated learnings bank
|
||||
learnings_bank = client.create_bank(
|
||||
bank_id="support-learnings",
|
||||
name="Curated Support Learnings"
|
||||
)
|
||||
|
||||
# After a successful resolution
|
||||
def promote_learning(insight: str):
|
||||
client.retain(
|
||||
bank_id="support-learnings",
|
||||
content=insight
|
||||
)
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
|
||||
```python
|
||||
def format_results(results):
|
||||
if not results:
|
||||
return "No relevant information found."
|
||||
return "\n".join([f"- {r.text}" for r in results])
|
||||
|
||||
def handle_support_request(user_id: str, query: str):
|
||||
# 1. Recall from user's memory
|
||||
user_recall = client.recall(
|
||||
bank_id=f"user-{user_id}",
|
||||
query=query
|
||||
)
|
||||
|
||||
# 2. Recall from shared docs
|
||||
docs_recall = client.recall(
|
||||
bank_id="product-docs",
|
||||
query=query
|
||||
)
|
||||
|
||||
# 3. Recall from learnings (optional)
|
||||
learnings_recall = client.recall(
|
||||
bank_id="support-learnings",
|
||||
query=query
|
||||
)
|
||||
|
||||
# 4. Build system prompt with context
|
||||
system_prompt = f"""You are a helpful support agent. Use the context below to answer the user's question.
|
||||
|
||||
## User's History
|
||||
{format_results(user_recall.results)}
|
||||
|
||||
## Product Documentation
|
||||
{format_results(docs_recall.results)}
|
||||
|
||||
## Known Solutions
|
||||
{format_results(learnings_recall.results)}
|
||||
|
||||
Provide helpful, accurate responses based on the documentation. Reference the user's history when relevant."""
|
||||
|
||||
# 5. Generate response using OpenAI
|
||||
response = llm.chat.completions.create(
|
||||
model="gpt-4o-mini",
|
||||
messages=[
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": query}
|
||||
]
|
||||
)
|
||||
assistant_response = response.choices[0].message.content
|
||||
|
||||
# 6. Save the conversation to user's memory
|
||||
conversation = f"user: {query}\nassistant: {assistant_response}"
|
||||
client.retain(
|
||||
bank_id=f"user-{user_id}",
|
||||
content=conversation
|
||||
)
|
||||
|
||||
return assistant_response
|
||||
|
||||
# Test the function
|
||||
create_user_bank("bob")
|
||||
print("User: How do I get started?")
|
||||
result = handle_support_request("bob", "How do I get started?")
|
||||
print(f"Assistant: {result}")
|
||||
print(f"\nView user memory: {HINDSIGHT_UI_URL}/banks/user-bob?view=documents")
|
||||
```
|
||||
|
||||
## When to Use This Pattern
|
||||
|
||||
**Good fit:**
|
||||
- Support agents with shared documentation
|
||||
- Multi-tenant applications with shared reference data
|
||||
- Any scenario needing user isolation + shared knowledge
|
||||
|
||||
**Consider alternatives if:**
|
||||
- You need cross-user learning (users benefiting from other users' solutions)
|
||||
- Entity relationships must span across users and docs
|
||||
|
||||
## Cleanup
|
||||
|
||||
Delete the banks created during this notebook:
|
||||
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
# Delete all banks created in this notebook
|
||||
for bank_id in ["product-docs", "support-learnings", "user-bob"]:
|
||||
response = requests.delete(f"{HINDSIGHT_API_URL}/v1/default/banks/{bank_id}")
|
||||
print(f"Deleted {bank_id}: {response.json()}")
|
||||
```
|
||||
@@ -0,0 +1,372 @@
|
||||
---
|
||||
sidebar_position: 5
|
||||
---
|
||||
|
||||
# Routing Tool Learning
|
||||
|
||||
|
||||
:::tip Run this notebook
|
||||
This recipe is available as an interactive Jupyter notebook.
|
||||
[**Open in GitHub →**](https://github.com/vectorize-io/hindsight-cookbook/blob/main/notebooks/05-tool-learning-demo.ipynb)
|
||||
:::
|
||||
|
||||
|
||||
This notebook demonstrates how Hindsight helps an LLM learn which tool to use when tool names are ambiguous. Without memory, the LLM might randomly select between similarly-named tools. With Hindsight, it learns from past interactions and consistently makes the correct choice.
|
||||
|
||||
## The Scenario
|
||||
|
||||
We have a task routing system with two tools:
|
||||
- `route_to_channel_alpha` - Routes to processing channel Alpha
|
||||
- `route_to_channel_omega` - Routes to processing channel Omega
|
||||
|
||||
The tool names and descriptions are **intentionally vague**. In reality:
|
||||
- Channel Alpha handles **FINANCIAL/PAYMENT** tasks (refunds, billing, etc.)
|
||||
- Channel Omega handles **TECHNICAL/SUPPORT** tasks (bugs, features, etc.)
|
||||
|
||||
**Without Hindsight:** The LLM guesses randomly based on vague descriptions
|
||||
**With Hindsight:** The LLM learns from feedback which channel handles what
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Make sure you have Hindsight running:
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY=your-key
|
||||
|
||||
docker run --rm -it --pull always -p 8888:8888 -p 9999:9999 \
|
||||
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
|
||||
-e HINDSIGHT_API_LLM_MODEL=o3-mini \
|
||||
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
|
||||
ghcr.io/vectorize-io/hindsight:latest
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
|
||||
```python
|
||||
!pip install hindsight-litellm hindsight-client litellm nest_asyncio python-dotenv -U -q
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
```python
|
||||
import os
|
||||
import json
|
||||
import uuid
|
||||
import time
|
||||
import logging
|
||||
import nest_asyncio
|
||||
from typing import Optional
|
||||
from dotenv import load_dotenv
|
||||
|
||||
nest_asyncio.apply()
|
||||
load_dotenv()
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logging.getLogger("LiteLLM").setLevel(logging.WARNING)
|
||||
logging.getLogger("LiteLLM Router").setLevel(logging.WARNING)
|
||||
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||
|
||||
import litellm
|
||||
import hindsight_litellm
|
||||
from hindsight_client import Hindsight
|
||||
|
||||
HINDSIGHT_API_URL = os.getenv("HINDSIGHT_API_URL", "http://localhost:8888")
|
||||
|
||||
if not os.getenv("OPENAI_API_KEY"):
|
||||
print("Warning: OPENAI_API_KEY not set")
|
||||
```
|
||||
|
||||
## Define Tools
|
||||
|
||||
These tool definitions are **intentionally ambiguous** - the descriptions don't reveal which channel handles what type of request.
|
||||
|
||||
|
||||
```python
|
||||
TOOLS = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "route_to_channel_alpha",
|
||||
"description": "Routes the customer request to processing channel Alpha. Use this channel for appropriate request types.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"request_summary": {
|
||||
"type": "string",
|
||||
"description": "A brief summary of the customer's request"
|
||||
},
|
||||
"priority": {
|
||||
"type": "string",
|
||||
"enum": ["low", "medium", "high"],
|
||||
"description": "Priority level of the request"
|
||||
}
|
||||
},
|
||||
"required": ["request_summary"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "route_to_channel_omega",
|
||||
"description": "Routes the customer request to processing channel Omega. Use this channel for appropriate request types.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"request_summary": {
|
||||
"type": "string",
|
||||
"description": "A brief summary of the customer's request"
|
||||
},
|
||||
"priority": {
|
||||
"type": "string",
|
||||
"enum": ["low", "medium", "high"],
|
||||
"description": "Priority level of the request"
|
||||
}
|
||||
},
|
||||
"required": ["request_summary"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Test Scenarios
|
||||
|
||||
A mix of financial and technical requests to test routing accuracy.
|
||||
|
||||
|
||||
```python
|
||||
TEST_SCENARIOS = [
|
||||
{
|
||||
"type": "financial",
|
||||
"request": "I was charged twice for my subscription last month. I need a refund for the duplicate charge.",
|
||||
"correct_tool": "route_to_channel_alpha"
|
||||
},
|
||||
{
|
||||
"type": "technical",
|
||||
"request": "The app keeps crashing when I try to upload a file larger than 10MB. This bug is blocking my work.",
|
||||
"correct_tool": "route_to_channel_omega"
|
||||
},
|
||||
{
|
||||
"type": "financial",
|
||||
"request": "My invoice shows an incorrect amount. The billing department needs to fix this.",
|
||||
"correct_tool": "route_to_channel_alpha"
|
||||
},
|
||||
{
|
||||
"type": "technical",
|
||||
"request": "I'd like to request a new feature: the ability to export reports as PDF.",
|
||||
"correct_tool": "route_to_channel_omega"
|
||||
},
|
||||
{
|
||||
"type": "financial",
|
||||
"request": "I need to update my payment method and understand why my last payment failed.",
|
||||
"correct_tool": "route_to_channel_alpha"
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
## Helper Functions
|
||||
|
||||
|
||||
```python
|
||||
SYSTEM_PROMPT = """You are a customer service routing agent. Your job is to route customer requests to the appropriate processing channel.
|
||||
|
||||
You have access to two routing channels:
|
||||
- route_to_channel_alpha: Routes to channel Alpha
|
||||
- route_to_channel_omega: Routes to channel Omega
|
||||
|
||||
Analyze the customer's request and route it to the most appropriate channel. You must call one of the routing functions to process the request.
|
||||
|
||||
Important: Base your routing decision on what you know about each channel's purpose. If you have learned from previous interactions which channel handles specific types of requests, use that knowledge."""
|
||||
|
||||
|
||||
def make_routing_request(user_request: str, use_hindsight: bool, bank_id: Optional[str] = None):
|
||||
"""Make a routing request and return the tool called."""
|
||||
messages = [
|
||||
{"role": "system", "content": SYSTEM_PROMPT},
|
||||
{"role": "user", "content": f"Customer Request: {user_request}"}
|
||||
]
|
||||
|
||||
if use_hindsight and bank_id:
|
||||
response = hindsight_litellm.completion(
|
||||
model="gpt-4o-mini",
|
||||
messages=messages,
|
||||
tools=TOOLS,
|
||||
tool_choice="required",
|
||||
temperature=0.0,
|
||||
)
|
||||
else:
|
||||
response = litellm.completion(
|
||||
model="gpt-4o-mini",
|
||||
messages=messages,
|
||||
tools=TOOLS,
|
||||
tool_choice="required",
|
||||
temperature=0.7,
|
||||
)
|
||||
|
||||
if response.choices[0].message.tool_calls:
|
||||
tool_call = response.choices[0].message.tool_calls[0]
|
||||
return tool_call.function.name
|
||||
return None
|
||||
|
||||
|
||||
def store_feedback(bank_id: str, request: str, correct_tool: str, request_type: str):
|
||||
"""Store feedback about which tool was correct for a request type."""
|
||||
client = Hindsight(base_url=HINDSIGHT_API_URL, timeout=60.0)
|
||||
|
||||
feedback_content = f"""ROUTING FEEDBACK:
|
||||
Request type: {request_type}
|
||||
Customer request: "{request}"
|
||||
Correct routing: {correct_tool}
|
||||
|
||||
LEARNED RULE: {request_type.upper()} requests (like refunds, billing, payments, charges, invoices) should ALWAYS be routed to {correct_tool}.
|
||||
This is important institutional knowledge for routing decisions."""
|
||||
|
||||
client.retain(
|
||||
bank_id=bank_id,
|
||||
content=feedback_content,
|
||||
context=f"routing:feedback:{request_type}",
|
||||
metadata={"request_type": request_type, "correct_tool": correct_tool}
|
||||
)
|
||||
```
|
||||
|
||||
## Phase 1: Without Hindsight (No Memory)
|
||||
|
||||
The LLM has no prior knowledge about which channel handles what. With ambiguous tool descriptions, it may route incorrectly.
|
||||
|
||||
|
||||
```python
|
||||
print("=" * 60)
|
||||
print("PHASE 1: WITHOUT HINDSIGHT (No Memory)")
|
||||
print("=" * 60)
|
||||
|
||||
phase1_results = []
|
||||
for i, scenario in enumerate(TEST_SCENARIOS[:3], 1):
|
||||
print(f"\n--- Test {i}: {scenario['type'].upper()} Request ---")
|
||||
print(f"Request: \"{scenario['request'][:60]}...\"")
|
||||
|
||||
tool_name = make_routing_request(scenario['request'], use_hindsight=False)
|
||||
|
||||
is_correct = tool_name == scenario['correct_tool']
|
||||
phase1_results.append(is_correct)
|
||||
|
||||
print(f"LLM chose: {tool_name}")
|
||||
print(f"Correct tool: {scenario['correct_tool']}")
|
||||
print(f"Result: {'✓ CORRECT' if is_correct else '✗ INCORRECT'}")
|
||||
|
||||
phase1_accuracy = sum(phase1_results) / len(phase1_results) * 100
|
||||
print(f"\n>>> Phase 1 Accuracy: {phase1_accuracy:.0f}% ({sum(phase1_results)}/{len(phase1_results)})")
|
||||
```
|
||||
|
||||
## Phase 2: Teaching Phase
|
||||
|
||||
Now we provide feedback about correct routing to build memory. This simulates a human supervisor correcting the AI's routing decisions.
|
||||
|
||||
|
||||
```python
|
||||
bank_id = f"tool-learning-{uuid.uuid4().hex[:8]}"
|
||||
print(f"Using bank_id: {bank_id}")
|
||||
|
||||
# Configure and enable Hindsight
|
||||
hindsight_litellm.configure(
|
||||
hindsight_api_url=HINDSIGHT_API_URL,
|
||||
bank_id=bank_id,
|
||||
store_conversations=True,
|
||||
inject_memories=True,
|
||||
max_memories=10,
|
||||
recall_budget="high",
|
||||
verbose=False,
|
||||
)
|
||||
hindsight_litellm.enable()
|
||||
|
||||
print("\nStoring routing feedback...")
|
||||
|
||||
feedback_examples = [
|
||||
("I need a refund for an incorrect charge on my account.", "route_to_channel_alpha", "financial"),
|
||||
("There's a bug in the system causing data loss.", "route_to_channel_omega", "technical"),
|
||||
("My billing statement has errors that need correction.", "route_to_channel_alpha", "financial"),
|
||||
("I want to request a new feature for the dashboard.", "route_to_channel_omega", "technical"),
|
||||
]
|
||||
|
||||
for request, correct_tool, req_type in feedback_examples:
|
||||
print(f" Storing: {req_type.upper()} → {correct_tool}")
|
||||
store_feedback(bank_id, request, correct_tool, req_type)
|
||||
|
||||
print("\nWaiting 15 seconds for Hindsight to process memories...")
|
||||
time.sleep(15)
|
||||
print("Done!")
|
||||
```
|
||||
|
||||
## Phase 3: With Hindsight (Memory-Augmented)
|
||||
|
||||
The LLM now has access to learned routing knowledge via Hindsight. It should route requests correctly based on past feedback.
|
||||
|
||||
|
||||
```python
|
||||
print("=" * 60)
|
||||
print("PHASE 3: WITH HINDSIGHT (Memory-Augmented)")
|
||||
print("=" * 60)
|
||||
|
||||
phase3_results = []
|
||||
for i, scenario in enumerate(TEST_SCENARIOS, 1):
|
||||
print(f"\n--- Test {i}: {scenario['type'].upper()} Request ---")
|
||||
print(f"Request: \"{scenario['request'][:60]}...\"")
|
||||
|
||||
tool_name = make_routing_request(
|
||||
scenario['request'],
|
||||
use_hindsight=True,
|
||||
bank_id=bank_id
|
||||
)
|
||||
|
||||
is_correct = tool_name == scenario['correct_tool']
|
||||
phase3_results.append(is_correct)
|
||||
|
||||
print(f"LLM chose: {tool_name}")
|
||||
print(f"Correct tool: {scenario['correct_tool']}")
|
||||
print(f"Result: {'✓ CORRECT' if is_correct else '✗ INCORRECT'}")
|
||||
|
||||
phase3_accuracy = sum(phase3_results) / len(phase3_results) * 100
|
||||
print(f"\n>>> Phase 3 Accuracy: {phase3_accuracy:.0f}% ({sum(phase3_results)}/{len(phase3_results)})")
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
|
||||
```python
|
||||
print("=" * 60)
|
||||
print("SUMMARY")
|
||||
print("=" * 60)
|
||||
print(f"\nPhase 1 (No Memory): {phase1_accuracy:.0f}% accuracy")
|
||||
print(f"Phase 3 (With Hindsight): {phase3_accuracy:.0f}% accuracy")
|
||||
|
||||
improvement = phase3_accuracy - phase1_accuracy
|
||||
if improvement > 0:
|
||||
print(f"\n🎉 Improvement: +{improvement:.0f}% accuracy with Hindsight!")
|
||||
elif improvement == 0:
|
||||
print(f"\nNote: Results may vary. Run again to see learning effect.")
|
||||
else:
|
||||
print(f"\nNote: Phase 1 got lucky! Run again to see typical behavior.")
|
||||
|
||||
print(f"\nMemories stored in bank: {bank_id}")
|
||||
print(f"View in UI: http://localhost:9999/banks/{bank_id}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("KEY INSIGHT")
|
||||
print("=" * 60)
|
||||
print("Hindsight allows the LLM to learn from experience which tool")
|
||||
print("to use, even when tool names/descriptions are ambiguous.")
|
||||
```
|
||||
|
||||
## Cleanup
|
||||
|
||||
|
||||
```python
|
||||
hindsight_litellm.cleanup()
|
||||
|
||||
# Optional: delete the bank
|
||||
import requests
|
||||
response = requests.delete(f"{HINDSIGHT_API_URL}/v1/default/banks/{bank_id}")
|
||||
print(f"Deleted bank: {response.json()}")
|
||||
```
|
||||
@@ -0,0 +1,192 @@
|
||||
# Admin CLI
|
||||
|
||||
The `hindsight-admin` CLI provides administrative commands for managing your Hindsight deployment, including database migrations, backup, and restore operations.
|
||||
|
||||
## Installation
|
||||
|
||||
The admin CLI is included with the `hindsight-api` package:
|
||||
|
||||
```bash
|
||||
pip install hindsight-api
|
||||
# or
|
||||
uv add hindsight-api
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
### run-db-migration
|
||||
|
||||
Run database migrations to the latest version. This is useful when you want to run migrations separately from API startup (e.g., in CI/CD pipelines or before deploying a new version).
|
||||
|
||||
```bash
|
||||
hindsight-admin run-db-migration [OPTIONS]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
| Option | Description | Default |
|
||||
|--------|-------------|---------|
|
||||
| `--schema`, `-s` | Database schema to run migrations on | `public` |
|
||||
|
||||
**Examples:**
|
||||
|
||||
```bash
|
||||
# Run migrations on the default public schema
|
||||
hindsight-admin run-db-migration
|
||||
|
||||
# Run migrations on a specific tenant schema
|
||||
hindsight-admin run-db-migration --schema tenant_acme
|
||||
```
|
||||
|
||||
:::tip Disabling Auto-Migrations
|
||||
To disable automatic migrations on API startup, set `HINDSIGHT_API_RUN_MIGRATIONS_ON_STARTUP=false`. This is useful when you want to run migrations as a separate step in your deployment pipeline.
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
### backup
|
||||
|
||||
Create a backup of all Hindsight data to a zip file.
|
||||
|
||||
```bash
|
||||
hindsight-admin backup OUTPUT [OPTIONS]
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
|
||||
| Argument | Description |
|
||||
|----------|-------------|
|
||||
| `OUTPUT` | Output file path (will add `.zip` extension if not present) |
|
||||
|
||||
**Options:**
|
||||
|
||||
| Option | Description | Default |
|
||||
|--------|-------------|---------|
|
||||
| `--schema`, `-s` | Database schema to backup | `public` |
|
||||
|
||||
**Examples:**
|
||||
|
||||
```bash
|
||||
# Backup to a file
|
||||
hindsight-admin backup /backups/hindsight-2024-01-15.zip
|
||||
|
||||
# Backup a specific tenant schema
|
||||
hindsight-admin backup /backups/tenant-acme.zip --schema tenant_acme
|
||||
```
|
||||
|
||||
The backup includes:
|
||||
- Memory banks and their configuration
|
||||
- Documents and chunks
|
||||
- Entities and their relationships
|
||||
- Memory units (facts, experiences, observations)
|
||||
- Entity cooccurrences and memory links
|
||||
|
||||
:::note Consistency
|
||||
Backups are created within a database transaction with `REPEATABLE READ` isolation, ensuring a consistent snapshot across all tables.
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
### restore
|
||||
|
||||
Restore data from a backup file. **Warning: This deletes all existing data in the target schema.**
|
||||
|
||||
```bash
|
||||
hindsight-admin restore INPUT [OPTIONS]
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
|
||||
| Argument | Description |
|
||||
|----------|-------------|
|
||||
| `INPUT` | Input backup file (.zip) |
|
||||
|
||||
**Options:**
|
||||
|
||||
| Option | Description | Default |
|
||||
|--------|-------------|---------|
|
||||
| `--schema`, `-s` | Database schema to restore to | `public` |
|
||||
| `--yes`, `-y` | Skip confirmation prompt | `false` |
|
||||
|
||||
**Examples:**
|
||||
|
||||
```bash
|
||||
# Restore with confirmation prompt
|
||||
hindsight-admin restore /backups/hindsight-2024-01-15.zip
|
||||
|
||||
# Restore without confirmation (for scripts)
|
||||
hindsight-admin restore /backups/hindsight-2024-01-15.zip --yes
|
||||
|
||||
# Restore to a specific tenant schema
|
||||
hindsight-admin restore /backups/tenant-acme.zip --schema tenant_acme --yes
|
||||
```
|
||||
|
||||
:::warning Data Loss
|
||||
Restore will **delete all existing data** in the target schema before importing the backup. Always verify you have a recent backup before performing a restore.
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
### decommission-worker
|
||||
|
||||
Release all tasks owned by a worker, resetting them from "processing" back to "pending" status so they can be picked up by other workers.
|
||||
|
||||
```bash
|
||||
hindsight-admin decommission-worker WORKER_ID [OPTIONS]
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
|
||||
| Argument | Description |
|
||||
|----------|-------------|
|
||||
| `WORKER_ID` | ID of the worker to decommission |
|
||||
|
||||
**Options:**
|
||||
|
||||
| Option | Description | Default |
|
||||
|--------|-------------|---------|
|
||||
| `--schema`, `-s` | Database schema | `public` |
|
||||
|
||||
**Examples:**
|
||||
|
||||
```bash
|
||||
# Before scaling down - release tasks from workers being removed
|
||||
hindsight-admin decommission-worker hindsight-worker-4
|
||||
hindsight-admin decommission-worker hindsight-worker-3
|
||||
|
||||
# Release tasks from a crashed worker
|
||||
hindsight-admin decommission-worker worker-2
|
||||
|
||||
# For a specific tenant schema
|
||||
hindsight-admin decommission-worker worker-1 --schema tenant_acme
|
||||
```
|
||||
|
||||
**When to Use:**
|
||||
|
||||
- **Scaling down**: Before removing worker replicas in Kubernetes
|
||||
- **Graceful removal**: When taking a worker offline for maintenance
|
||||
- **Crash recovery**: If a worker crashed while processing tasks
|
||||
- **Stuck worker**: When a worker is unresponsive
|
||||
|
||||
:::tip Finding Worker IDs
|
||||
Worker IDs default to the hostname. In Kubernetes StatefulSets, this is the pod name (e.g., `hindsight-worker-0`). You can also set a custom ID with `HINDSIGHT_API_WORKER_ID` or `--worker-id`.
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables
|
||||
|
||||
The admin CLI uses the same environment variables as the API service. The most important one is:
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `HINDSIGHT_API_DATABASE_URL` | PostgreSQL connection string | `pg0` (embedded) |
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
# Use a specific database
|
||||
export HINDSIGHT_API_DATABASE_URL=postgresql://user:pass@localhost:5432/hindsight
|
||||
hindsight-admin backup /backups/mybackup.zip
|
||||
```
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
|
||||
# Documents
|
||||
|
||||
Track and manage document sources in your memory bank. Documents provide traceability — knowing where memories came from.
|
||||
|
||||
{/* Import raw source files */}
|
||||
|
||||
> **💡 Prerequisites**
|
||||
>
|
||||
Make sure you've completed the [Quick Start](./quickstart) and understand [how retain works](./retain).
|
||||
## What Are Documents?
|
||||
|
||||
Documents are containers for retained content. They help you:
|
||||
|
||||
- **Track sources** — Know which PDF, conversation, or file a memory came from
|
||||
- **Update content** — Re-retain a document to update its facts
|
||||
- **Delete in bulk** — Remove all memories from a document at once
|
||||
- **Organize memories** — Group related facts by source
|
||||
|
||||
## Chunks
|
||||
|
||||
When you retain content, Hindsight splits it into chunks before extracting facts. These chunks are stored alongside the extracted memories, preserving the original text segments.
|
||||
|
||||
**Why chunks matter:**
|
||||
- **Context preservation** — Chunks contain the raw text that generated facts, useful when you need the exact wording
|
||||
- **Richer recall** — Including chunks in recall provides surrounding context for matched facts
|
||||
|
||||
> **💡 Include Chunks in Recall**
|
||||
>
|
||||
Use `include_chunks=True` in your recall calls to get the original text chunks alongside fact results. See [Recall](./recall) for details.
|
||||
## Retain with Document ID
|
||||
|
||||
Associate retained content with a document:
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
# Retain with document ID
|
||||
client.retain(
|
||||
bank_id="my-bank",
|
||||
content="Alice presented the Q4 roadmap...",
|
||||
document_id="meeting-2024-03-15"
|
||||
)
|
||||
|
||||
# Batch retain for a document
|
||||
client.retain_batch(
|
||||
bank_id="my-bank",
|
||||
items=[
|
||||
{"content": "Item 1: Product launch delayed to Q2"},
|
||||
{"content": "Item 2: New hiring targets announced"},
|
||||
{"content": "Item 3: Budget approved for ML team"}
|
||||
],
|
||||
document_id="meeting-2024-03-15"
|
||||
)
|
||||
```
|
||||
|
||||
### Node.js
|
||||
|
||||
```javascript
|
||||
// Retain with document ID
|
||||
await client.retain('my-bank', 'Alice presented the Q4 roadmap...', {
|
||||
document_id: 'meeting-2024-03-15'
|
||||
});
|
||||
|
||||
// Batch retain
|
||||
await client.retainBatch('my-bank', [
|
||||
{ content: 'Item 1: Product launch delayed to Q2' },
|
||||
{ content: 'Item 2: New hiring targets announced' },
|
||||
{ content: 'Item 3: Budget approved for ML team' }
|
||||
], { documentId: 'meeting-2024-03-15' });
|
||||
```
|
||||
|
||||
### CLI
|
||||
|
||||
```bash
|
||||
# Retain content with document ID
|
||||
hindsight memory retain my-bank "Meeting notes content..." --doc-id notes-2024-03-15
|
||||
|
||||
# Batch retain from files
|
||||
hindsight memory retain-files my-bank docs/
|
||||
```
|
||||
|
||||
## Update Documents
|
||||
|
||||
Re-retaining with the same document_id **replaces** the old content:
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
# Original
|
||||
client.retain(
|
||||
bank_id="my-bank",
|
||||
content="Project deadline: March 31",
|
||||
document_id="project-plan"
|
||||
)
|
||||
|
||||
# Update (deletes old facts, creates new ones)
|
||||
client.retain(
|
||||
bank_id="my-bank",
|
||||
content="Project deadline: April 15 (extended)",
|
||||
document_id="project-plan"
|
||||
)
|
||||
```
|
||||
|
||||
### Node.js
|
||||
|
||||
```javascript
|
||||
// Original
|
||||
await client.retain('my-bank', 'Project deadline: March 31', {
|
||||
document_id: 'project-plan'
|
||||
});
|
||||
|
||||
// Update
|
||||
await client.retain('my-bank', 'Project deadline: April 15 (extended)', {
|
||||
document_id: 'project-plan'
|
||||
});
|
||||
```
|
||||
|
||||
### CLI
|
||||
|
||||
```bash
|
||||
# Original
|
||||
hindsight memory retain my-bank "Project deadline: March 31" --doc-id project-plan
|
||||
|
||||
# Update
|
||||
hindsight memory retain my-bank "Project deadline: April 15 (extended)" --doc-id project-plan
|
||||
```
|
||||
|
||||
## Get Document
|
||||
|
||||
Retrieve a document's original text and metadata. This is useful for expanding document context after a recall operation returns memories with document references.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from hindsight_client_api import ApiClient, Configuration
|
||||
from hindsight_client_api.api import DocumentsApi
|
||||
|
||||
async def get_document_example():
|
||||
config = Configuration(host="http://localhost:8888")
|
||||
api_client = ApiClient(config)
|
||||
api = DocumentsApi(api_client)
|
||||
|
||||
# Get document to expand context from recall results
|
||||
doc = await api.get_document(
|
||||
bank_id="my-bank",
|
||||
document_id="meeting-2024-03-15"
|
||||
)
|
||||
|
||||
print(f"Document: {doc.id}")
|
||||
print(f"Original text: {doc.original_text}")
|
||||
print(f"Memory count: {doc.memory_unit_count}")
|
||||
print(f"Created: {doc.created_at}")
|
||||
|
||||
asyncio.run(get_document_example())
|
||||
```
|
||||
|
||||
### Node.js
|
||||
|
||||
```javascript
|
||||
const apiClient = createClient(createConfig({ baseUrl: 'http://localhost:8888' }));
|
||||
|
||||
// Get document to expand context from recall results
|
||||
const { data: doc } = await sdk.getDocument({
|
||||
client: apiClient,
|
||||
path: { bank_id: 'my-bank', document_id: 'meeting-2024-03-15' }
|
||||
});
|
||||
|
||||
console.log(`Document: ${doc.id}`);
|
||||
console.log(`Original text: ${doc.original_text}`);
|
||||
console.log(`Memory count: ${doc.memory_unit_count}`);
|
||||
console.log(`Created: ${doc.created_at}`);
|
||||
```
|
||||
|
||||
### CLI
|
||||
|
||||
```bash
|
||||
hindsight document get my-bank meeting-2024-03-15
|
||||
```
|
||||
|
||||
## Delete Document
|
||||
|
||||
Remove a document and all its associated memories:
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
from hindsight_client_api import ApiClient, Configuration
|
||||
from hindsight_client_api.api import DocumentsApi
|
||||
|
||||
async def delete_document_example():
|
||||
config = Configuration(host="http://localhost:8888")
|
||||
api_client = ApiClient(config)
|
||||
api = DocumentsApi(api_client)
|
||||
|
||||
# Delete document and all its memories
|
||||
result = await api.delete_document(
|
||||
bank_id="my-bank",
|
||||
document_id="meeting-2024-03-15"
|
||||
)
|
||||
|
||||
print(f"Deleted {result.memory_units_deleted} memories")
|
||||
|
||||
asyncio.run(delete_document_example())
|
||||
```
|
||||
|
||||
### Node.js
|
||||
|
||||
```javascript
|
||||
// Delete document and all its memories
|
||||
const { data: deleteResult } = await sdk.deleteDocument({
|
||||
client: apiClient,
|
||||
path: { bank_id: 'my-bank', document_id: 'meeting-2024-03-15' }
|
||||
});
|
||||
|
||||
console.log(`Deleted ${deleteResult.memory_units_deleted} memories`);
|
||||
```
|
||||
|
||||
### CLI
|
||||
|
||||
```bash
|
||||
hindsight document delete my-bank meeting-2024-03-15
|
||||
```
|
||||
|
||||
:::warning
|
||||
Deleting a document permanently removes all memories extracted from it. This action cannot be undone.
|
||||
## Document Response Format
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "meeting-2024-03-15",
|
||||
"bank_id": "my-bank",
|
||||
"original_text": "Alice presented the Q4 roadmap...",
|
||||
"content_hash": "abc123def456",
|
||||
"memory_unit_count": 12,
|
||||
"created_at": "2024-03-15T14:00:00Z",
|
||||
"updated_at": "2024-03-15T14:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [**Operations**](./operations) — Monitor background tasks
|
||||
- [**Memory Banks**](./memory-banks) — Configure bank settings
|
||||
@@ -0,0 +1,272 @@
|
||||
|
||||
# Main Methods
|
||||
|
||||
Hindsight provides three core operations: **retain**, **recall**, and **reflect**.
|
||||
|
||||
{/* Import raw source files */}
|
||||
|
||||
> **💡 Prerequisites**
|
||||
>
|
||||
Make sure you've [installed Hindsight](../installation) and completed the [Quick Start](./quickstart).
|
||||
## Retain: Store Information
|
||||
|
||||
Store conversations, documents, and facts into a memory bank.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
# Store a single fact
|
||||
client.retain(
|
||||
bank_id="my-bank",
|
||||
content="Alice joined Google in March 2024 as a Senior ML Engineer"
|
||||
)
|
||||
|
||||
# Store a conversation
|
||||
conversation = """
|
||||
User: What did you work on today?
|
||||
Assistant: I reviewed the new ML pipeline architecture.
|
||||
User: How did it look?
|
||||
Assistant: Promising, but needs better error handling.
|
||||
"""
|
||||
|
||||
client.retain(
|
||||
bank_id="my-bank",
|
||||
content=conversation,
|
||||
context="Daily standup conversation"
|
||||
)
|
||||
|
||||
# Batch retain multiple items
|
||||
client.retain_batch(
|
||||
bank_id="my-bank",
|
||||
items=[
|
||||
{"content": "Bob prefers Python for data science"},
|
||||
{"content": "Alice recommends using pytest for testing"},
|
||||
{"content": "The team uses GitHub for code reviews"}
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
### Node.js
|
||||
|
||||
```javascript
|
||||
// Store a single fact
|
||||
await client.retain('my-bank', 'Alice joined Google in March 2024 as a Senior ML Engineer');
|
||||
|
||||
// Store a conversation
|
||||
const conversation = `
|
||||
User: What did you work on today?
|
||||
Assistant: I reviewed the new ML pipeline architecture.
|
||||
User: How did it look?
|
||||
Assistant: Promising, but needs better error handling.
|
||||
`;
|
||||
|
||||
await client.retain('my-bank', conversation, {
|
||||
context: 'Daily standup conversation'
|
||||
});
|
||||
|
||||
// Batch retain multiple items
|
||||
await client.retainBatch('my-bank', [
|
||||
{ content: 'Bob prefers Python for data science' },
|
||||
{ content: 'Alice recommends using pytest for testing' },
|
||||
{ content: 'The team uses GitHub for code reviews' }
|
||||
]);
|
||||
```
|
||||
|
||||
### CLI
|
||||
|
||||
```bash
|
||||
# Store a single fact
|
||||
hindsight retain my-bank "Alice joined Google in March 2024 as a Senior ML Engineer"
|
||||
|
||||
# Store from a file
|
||||
hindsight retain my-bank --file conversation.txt --context "Daily standup"
|
||||
|
||||
# Store multiple files
|
||||
hindsight retain my-bank --files docs/*.md
|
||||
```
|
||||
|
||||
**What happens:** Content is processed by an LLM to extract rich facts, identify entities, and build connections in a knowledge graph.
|
||||
|
||||
**See:** [Retain Details](./retain) for advanced options and parameters.
|
||||
|
||||
---
|
||||
|
||||
## Recall: Search Memories
|
||||
|
||||
Search for relevant memories using multi-strategy retrieval.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
# Basic search
|
||||
results = client.recall(
|
||||
bank_id="my-bank",
|
||||
query="What does Alice do at Google?"
|
||||
)
|
||||
|
||||
for result in results.results:
|
||||
print(f"- {result.text}")
|
||||
|
||||
# Search with options
|
||||
results = client.recall(
|
||||
bank_id="my-bank",
|
||||
query="What happened last spring?",
|
||||
budget="high", # More thorough graph traversal
|
||||
max_tokens=8192, # Return more context
|
||||
types=["world"] # Only world facts
|
||||
)
|
||||
|
||||
# Include source chunks for more context
|
||||
results = client.recall(
|
||||
bank_id="my-bank",
|
||||
query="Tell me about Alice",
|
||||
include_chunks=True,
|
||||
max_chunk_tokens=500
|
||||
)
|
||||
|
||||
# Check chunk details (chunks are on response level, keyed by memory ID)
|
||||
for result in results.results:
|
||||
print(f"Memory: {result.text}")
|
||||
if results.chunks and result.id in results.chunks:
|
||||
chunk = results.chunks[result.id]
|
||||
print(f" Source: {chunk.text[:100]}...")
|
||||
```
|
||||
|
||||
### Node.js
|
||||
|
||||
```javascript
|
||||
// Basic search
|
||||
const results = await client.recall('my-bank', 'What does Alice do at Google?');
|
||||
|
||||
for (const result of results.results) {
|
||||
console.log(`- ${result.text}`);
|
||||
}
|
||||
|
||||
// Search with options
|
||||
const filteredResults = await client.recall('my-bank', 'What happened last spring?', {
|
||||
budget: 'high',
|
||||
maxTokens: 8192,
|
||||
types: ['world']
|
||||
});
|
||||
|
||||
// Include entity information
|
||||
const entityResults = await client.recall('my-bank', 'Tell me about Alice', {
|
||||
includeEntities: true,
|
||||
maxEntityTokens: 500
|
||||
});
|
||||
|
||||
// Check entity details
|
||||
for (const [entityId, entity] of Object.entries(entityResults.entities || {})) {
|
||||
console.log(`Entity: ${entity.canonical_name}`);
|
||||
console.log(`Observations: ${entity.observations}`);
|
||||
}
|
||||
```
|
||||
|
||||
### CLI
|
||||
|
||||
```bash
|
||||
# Basic search
|
||||
hindsight recall my-bank "What does Alice do at Google?"
|
||||
|
||||
# Search with options
|
||||
hindsight recall my-bank "What happened last spring?" \
|
||||
--budget high \
|
||||
--max-tokens 8192 \
|
||||
--fact-type world
|
||||
|
||||
# Verbose output (shows weights and sources)
|
||||
hindsight recall my-bank "Tell me about Alice" -v
|
||||
```
|
||||
|
||||
**What happens:** Four search strategies (semantic, keyword, graph, temporal) run in parallel, results are fused and reranked.
|
||||
|
||||
**See:** [Recall Details](./recall) for tuning quality vs latency.
|
||||
|
||||
---
|
||||
|
||||
## Reflect: Reason with Disposition
|
||||
|
||||
Generate disposition-aware responses using memories and observations.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
# Basic reflect
|
||||
response = client.reflect(
|
||||
bank_id="my-bank",
|
||||
query="Should we adopt TypeScript for our backend?"
|
||||
)
|
||||
|
||||
print(response.text)
|
||||
print("\nBased on:", len(response.based_on or []), "facts")
|
||||
|
||||
# Reflect with options
|
||||
response = client.reflect(
|
||||
bank_id="my-bank",
|
||||
query="What are Alice's strengths for the team lead role?",
|
||||
budget="high" # More thorough reasoning
|
||||
)
|
||||
|
||||
# See which facts influenced the response
|
||||
for fact in response.based_on or []:
|
||||
print(f"- {fact.text}")
|
||||
```
|
||||
|
||||
### Node.js
|
||||
|
||||
```javascript
|
||||
// Basic reflect
|
||||
const response = await client.reflect('my-bank', 'Should we adopt TypeScript for our backend?');
|
||||
|
||||
console.log(response.text);
|
||||
console.log('\nBased on:', (response.based_on || []).length, 'facts');
|
||||
|
||||
// Reflect with options
|
||||
const detailedResponse = await client.reflect('my-bank', "What are Alice's strengths for the team lead role?", {
|
||||
budget: 'high'
|
||||
});
|
||||
|
||||
// See which facts influenced the response
|
||||
for (const fact of detailedResponse.based_on || []) {
|
||||
console.log(`- ${fact.text}`);
|
||||
}
|
||||
```
|
||||
|
||||
### CLI
|
||||
|
||||
```bash
|
||||
# Basic reflect
|
||||
hindsight reflect my-bank "Should we adopt TypeScript for our backend?"
|
||||
|
||||
# Verbose output (shows sources and observations)
|
||||
hindsight reflect my-bank "What are Alice's strengths for the team lead role?" -v
|
||||
|
||||
# With higher reasoning budget
|
||||
hindsight reflect my-bank "Analyze our tech stack" --budget high
|
||||
```
|
||||
|
||||
**What happens:** Memories and observations are recalled, bank disposition is applied, and the LLM reasons through the evidence to generate a response.
|
||||
|
||||
**See:** [Reflect Details](./reflect) for disposition configuration.
|
||||
|
||||
---
|
||||
|
||||
## Comparison
|
||||
|
||||
| Feature | Retain | Recall | Reflect |
|
||||
|---------|--------|--------|---------|
|
||||
| **Purpose** | Store information | Find information | Reason about information |
|
||||
| **Input** | Raw text/documents | Search query | Question/prompt |
|
||||
| **Output** | Memory IDs | Ranked facts + observations | Reasoned response |
|
||||
| **Uses LLM** | Yes (extraction) | No | Yes (generation) |
|
||||
| **Uses observations** | No | Yes | Yes |
|
||||
| **Disposition** | No | No | Yes |
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [**Retain**](./retain) — Advanced options for storing memories
|
||||
- [**Recall**](./recall) — Tuning search quality and performance
|
||||
- [**Reflect**](./reflect) — Configuring disposition
|
||||
- [**Memory Banks**](./memory-banks) — Managing memory bank disposition
|
||||
@@ -0,0 +1,229 @@
|
||||
|
||||
# Memory Banks
|
||||
|
||||
Memory banks are isolated containers that store all memory-related data for a specific context or use case.
|
||||
|
||||
{/* Import raw source files */}
|
||||
|
||||
## What is a Memory Bank?
|
||||
|
||||
A memory bank is a complete, isolated storage unit containing:
|
||||
|
||||
- **Memories** — Facts and information retained from conversations
|
||||
- **Documents** — Files and content indexed for retrieval
|
||||
- **Entities** — People, places, concepts extracted from memories
|
||||
- **Relationships** — Connections between entities in the knowledge graph
|
||||
- **Directives** — Hard rules the agent must follow during reflect operations
|
||||
|
||||
Banks are completely isolated from each other — memories stored in one bank are not visible to another.
|
||||
|
||||
You don't need to pre-create a bank. Hindsight will automatically create it with default settings when you first use it.
|
||||
|
||||
> **💡 Prerequisites**
|
||||
>
|
||||
Make sure you've completed the [Quick Start](./quickstart) to install the client and start the server.
|
||||
## Creating a Memory Bank
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
client.create_bank(
|
||||
bank_id="my-bank",
|
||||
name="Research Assistant",
|
||||
mission="You're a research assistant specializing in machine learning - keep track of papers, methods, and findings.",
|
||||
disposition={
|
||||
"skepticism": 4,
|
||||
"literalism": 3,
|
||||
"empathy": 3
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### Node.js
|
||||
|
||||
```javascript
|
||||
await client.createBank('my-bank', {
|
||||
name: 'Research Assistant',
|
||||
mission: 'I am a research assistant specializing in machine learning',
|
||||
disposition: {
|
||||
skepticism: 4,
|
||||
literalism: 3,
|
||||
empathy: 3
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### CLI
|
||||
|
||||
```bash
|
||||
# Set mission
|
||||
hindsight bank mission my-bank "I am a research assistant specializing in ML"
|
||||
|
||||
# Set disposition
|
||||
hindsight bank disposition my-bank \
|
||||
--skepticism 4 \
|
||||
--literalism 3 \
|
||||
--empathy 3
|
||||
```
|
||||
|
||||
## Mission and Disposition
|
||||
|
||||
Mission and disposition are optional settings that influence how the bank reasons during [reflect](./reflect) operations.
|
||||
|
||||
:::info
|
||||
Mission and disposition only affect the `reflect` operation. They do not impact `retain`, `recall`, or other memory operations.
|
||||
### Mission
|
||||
|
||||
The mission is a first-person narrative providing context for reasoning:
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
client.create_bank(
|
||||
bank_id="financial-advisor",
|
||||
name="Financial Advisor",
|
||||
mission="""You're a conservative financial advisor - keep track of client risk tolerance,
|
||||
investment preferences, and market conditions. Prioritize capital preservation over growth."""
|
||||
)
|
||||
```
|
||||
|
||||
### Node.js
|
||||
|
||||
```javascript
|
||||
await client.createBank('financial-advisor', {
|
||||
name: 'Financial Advisor',
|
||||
mission: `I am a conservative financial advisor with 20 years of experience.
|
||||
I prioritize capital preservation over aggressive growth.
|
||||
I have seen multiple market crashes and believe in diversification.`
|
||||
});
|
||||
```
|
||||
|
||||
### Disposition Traits
|
||||
|
||||
Disposition traits influence how reasoning is performed during reflection. Each trait is scored 1 to 5:
|
||||
|
||||
| Trait | Low (1) | High (5) |
|
||||
|-------|---------|----------|
|
||||
| **Skepticism** | Trusting, accepts information at face value | Skeptical, questions and doubts claims |
|
||||
| **Literalism** | Flexible interpretation, reads between the lines | Literal interpretation, takes things exactly as stated |
|
||||
| **Empathy** | Detached, focuses on facts and logic | Empathetic, considers emotional context |
|
||||
|
||||
## Directives
|
||||
|
||||
Directives are hard rules that the agent must follow during [reflect](./reflect) operations. Unlike disposition traits which influence *how* the agent reasons, directives are explicit instructions that are *always* enforced.
|
||||
|
||||
:::info
|
||||
Directives only affect the `reflect` operation. They are injected into prompts and the agent is required to comply with them in all responses.
|
||||
### When to Use Directives
|
||||
|
||||
Use directives for rules that must never be violated:
|
||||
|
||||
- **Language/style constraints**: "Always respond in formal English"
|
||||
- **Privacy rules**: "Never share personal data with third parties"
|
||||
- **Domain constraints**: "Prefer conservative investment recommendations"
|
||||
- **Behavioral guardrails**: "Always cite sources when making claims"
|
||||
|
||||
### Creating Directives
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
# Create a directive (hard rule for reflect)
|
||||
directive = client.create_directive(
|
||||
bank_id=BANK_ID,
|
||||
name="Formal Language",
|
||||
content="Always respond in formal English, avoiding slang and colloquialisms."
|
||||
)
|
||||
|
||||
print(f"Created directive: {directive.id}")
|
||||
```
|
||||
|
||||
### Node.js
|
||||
|
||||
```javascript
|
||||
// Create a directive (hard rule for reflect)
|
||||
const directive = await client.createDirective(
|
||||
BANK_ID,
|
||||
'Formal Language',
|
||||
'Always respond in formal English, avoiding slang and colloquialisms.'
|
||||
);
|
||||
|
||||
console.log(`Created directive: ${directive.id}`);
|
||||
```
|
||||
|
||||
### Listing Directives
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
# List all directives in a bank
|
||||
directives = client.list_directives(bank_id=BANK_ID)
|
||||
|
||||
for d in directives.items:
|
||||
print(f"- {d.name}: {d.content[:50]}...")
|
||||
```
|
||||
|
||||
### Node.js
|
||||
|
||||
```javascript
|
||||
// List all directives in a bank
|
||||
const directives = await client.listDirectives(BANK_ID);
|
||||
|
||||
for (const d of directives.items) {
|
||||
console.log(`- ${d.name}: ${d.content.slice(0, 50)}...`);
|
||||
}
|
||||
```
|
||||
|
||||
### Updating Directives
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
# Update a directive (e.g., disable without deleting)
|
||||
updated = client.update_directive(
|
||||
bank_id=BANK_ID,
|
||||
directive_id=directive_id,
|
||||
is_active=False
|
||||
)
|
||||
|
||||
print(f"Directive active: {updated.is_active}")
|
||||
```
|
||||
|
||||
### Node.js
|
||||
|
||||
```javascript
|
||||
// Update a directive (e.g., disable without deleting)
|
||||
const updated = await client.updateDirective(BANK_ID, directiveId, {
|
||||
isActive: false
|
||||
});
|
||||
|
||||
console.log(`Directive active: ${updated.is_active}`);
|
||||
```
|
||||
|
||||
### Deleting Directives
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
# Delete a directive
|
||||
client.delete_directive(
|
||||
bank_id=BANK_ID,
|
||||
directive_id=directive_id
|
||||
)
|
||||
```
|
||||
|
||||
### Node.js
|
||||
|
||||
```javascript
|
||||
// Delete a directive
|
||||
await client.deleteDirective(BANK_ID, directiveId);
|
||||
```
|
||||
|
||||
### Directives vs Disposition
|
||||
|
||||
| Aspect | Directives | Disposition |
|
||||
|--------|------------|-------------|
|
||||
| **Nature** | Hard rules, must be followed | Soft influence on reasoning style |
|
||||
| **Enforcement** | Strict — responses are rejected if violated | Flexible — shapes interpretation |
|
||||
| **Use case** | Compliance, guardrails, constraints | Personality, character, tone |
|
||||
| **Example** | "Never recommend specific stocks" | High skepticism: questions claims |
|
||||
@@ -0,0 +1,267 @@
|
||||
|
||||
# Mental Models
|
||||
|
||||
User-curated summaries that provide high-quality, pre-computed answers for common queries.
|
||||
|
||||
{/* Import raw source files */}
|
||||
|
||||
## What Are Mental Models?
|
||||
|
||||
Mental models are **saved reflect responses** that you curate for your memory bank. When you create a mental model, Hindsight runs a reflect operation with your source query and stores the result. During future reflect calls, these pre-computed summaries are checked first — providing faster, more consistent answers.
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[Create Mental Model] --> B[Run Reflect]
|
||||
B --> C[Store Result]
|
||||
C --> D[Future Queries]
|
||||
D --> E{Match Found?}
|
||||
E -->|Yes| F[Return Mental Model]
|
||||
E -->|No| G[Run Full Reflect]
|
||||
```
|
||||
|
||||
### Why Use Mental Models?
|
||||
|
||||
| Benefit | Description |
|
||||
|---------|-------------|
|
||||
| **Consistency** | Same answer every time for common questions |
|
||||
| **Speed** | Pre-computed responses are returned instantly |
|
||||
| **Quality** | Manually curated summaries you've reviewed |
|
||||
| **Control** | Define exactly how key topics should be answered |
|
||||
|
||||
### Hierarchical Retrieval
|
||||
|
||||
During reflect, the agent checks sources in priority order:
|
||||
|
||||
1. **Mental Models** — User-curated summaries (highest priority)
|
||||
2. **Observations** — Consolidated knowledge
|
||||
3. **Raw Facts** — Ground truth memories
|
||||
|
||||
Mental models are checked first because they represent your explicitly curated knowledge.
|
||||
|
||||
---
|
||||
|
||||
## Create a Mental Model
|
||||
|
||||
Creating a mental model runs a reflect operation in the background and saves the result:
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
# Create a mental model (runs reflect in background)
|
||||
result = client.create_mental_model(
|
||||
bank_id=BANK_ID,
|
||||
name="Team Communication Preferences",
|
||||
source_query="How does the team prefer to communicate?",
|
||||
tags=["team", "communication"]
|
||||
)
|
||||
|
||||
# Returns an operation_id - check operations endpoint for completion
|
||||
print(f"Operation ID: {result.operation_id}")
|
||||
```
|
||||
|
||||
### CLI
|
||||
|
||||
```bash
|
||||
# Create a mental model (async operation)
|
||||
curl -X POST "http://localhost:8888/v1/default/banks/my-bank/mental-models" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Team Communication Preferences",
|
||||
"source_query": "How does the team prefer to communicate?",
|
||||
"tags": ["team"]
|
||||
}'
|
||||
|
||||
# Response: {"operation_id": "op-123"}
|
||||
# Use the operations endpoint to check completion
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `name` | string | Yes | Human-readable name for the mental model |
|
||||
| `source_query` | string | Yes | The query to run to generate content |
|
||||
| `tags` | list | No | Tags for filtering during retrieval |
|
||||
| `max_tokens` | int | No | Maximum tokens for the mental model content |
|
||||
| `trigger` | object | No | Trigger settings (see [Automatic Refresh](#automatic-refresh)) |
|
||||
|
||||
---
|
||||
|
||||
## Automatic Refresh
|
||||
|
||||
Mental models can be configured to **automatically refresh** when observations are updated. This keeps them in sync with the latest knowledge without manual intervention.
|
||||
|
||||
### Trigger Settings
|
||||
|
||||
| Setting | Type | Default | Description |
|
||||
|---------|------|---------|-------------|
|
||||
| `refresh_after_consolidation` | bool | false | Automatically refresh after observations consolidation |
|
||||
|
||||
When `refresh_after_consolidation` is enabled, the mental model will be re-generated every time the bank's observations are consolidated — ensuring it always reflects the latest synthesized knowledge.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
# Create a mental model with automatic refresh enabled
|
||||
result = client.create_mental_model(
|
||||
bank_id=BANK_ID,
|
||||
name="Project Status",
|
||||
source_query="What is the current project status?",
|
||||
trigger={"refresh_after_consolidation": True}
|
||||
)
|
||||
|
||||
# This mental model will automatically refresh when observations are updated
|
||||
print(f"Operation ID: {result.operation_id}")
|
||||
```
|
||||
|
||||
### CLI
|
||||
|
||||
```bash
|
||||
# Create a mental model with automatic refresh enabled
|
||||
curl -X POST "http://localhost:8888/v1/default/banks/my-bank/mental-models" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Project Status",
|
||||
"source_query": "What is the current project status?",
|
||||
"trigger": {"refresh_after_consolidation": true}
|
||||
}'
|
||||
```
|
||||
|
||||
### When to Use Automatic Refresh
|
||||
|
||||
| Use Case | Automatic Refresh | Why |
|
||||
|----------|-------------------|-----|
|
||||
| **Real-time dashboards** | ✅ Enabled | Status should always be current |
|
||||
| **Policy summaries** | ❌ Disabled | Policies change infrequently, manual refresh preferred |
|
||||
| **User preferences** | ✅ Enabled | Preferences evolve with new interactions |
|
||||
| **FAQ answers** | ❌ Disabled | Answers are curated, should be reviewed before updating |
|
||||
|
||||
:::tip
|
||||
Enable automatic refresh for mental models that need to stay current. Disable it for curated content where you want to review changes before they go live.
|
||||
---
|
||||
|
||||
## List Mental Models
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
# List all mental models in a bank
|
||||
mental_models = client.list_mental_models(bank_id=BANK_ID)
|
||||
|
||||
for mental_model in mental_models.items:
|
||||
print(f"- {mental_model.name}: {mental_model.source_query}")
|
||||
```
|
||||
|
||||
### CLI
|
||||
|
||||
```bash
|
||||
curl "http://localhost:8888/v1/default/banks/my-bank/mental-models"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Get a Mental Model
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
# Section 'get-mental-model' not found in api/mental-models.py
|
||||
```
|
||||
|
||||
### CLI
|
||||
|
||||
```bash
|
||||
curl "http://localhost:8888/v1/default/banks/my-bank/mental-models/{mental_model_id}"
|
||||
```
|
||||
|
||||
### Response Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `id` | string | Unique mental model ID |
|
||||
| `bank_id` | string | Memory bank ID |
|
||||
| `name` | string | Human-readable name |
|
||||
| `source_query` | string | The query used to generate content |
|
||||
| `content` | string | The generated mental model text |
|
||||
| `tags` | list | Tags for filtering |
|
||||
| `last_refreshed_at` | string | When the mental model was last updated |
|
||||
| `created_at` | string | When the mental model was created |
|
||||
| `reflect_response` | object | Full reflect response including `based_on` facts |
|
||||
|
||||
---
|
||||
|
||||
## Refresh a Mental Model
|
||||
|
||||
Re-run the source query to update the mental model with current knowledge:
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
# Section 'refresh-mental-model' not found in api/mental-models.py
|
||||
```
|
||||
|
||||
### CLI
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:8888/v1/default/banks/my-bank/mental-models/{mental_model_id}/refresh"
|
||||
```
|
||||
|
||||
Refreshing is useful when:
|
||||
- New memories have been retained that affect the topic
|
||||
- Observations have been updated
|
||||
- You want to ensure the mental model reflects current knowledge
|
||||
|
||||
---
|
||||
|
||||
## Update a Mental Model
|
||||
|
||||
Update the mental model's name:
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
# Section 'update-mental-model' not found in api/mental-models.py
|
||||
```
|
||||
|
||||
### CLI
|
||||
|
||||
```bash
|
||||
curl -X PATCH "http://localhost:8888/v1/default/banks/my-bank/mental-models/{mental_model_id}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name": "Updated Team Communication Preferences"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Delete a Mental Model
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
# Section 'delete-mental-model' not found in api/mental-models.py
|
||||
```
|
||||
|
||||
### CLI
|
||||
|
||||
```bash
|
||||
curl -X DELETE "http://localhost:8888/v1/default/banks/my-bank/mental-models/{mental_model_id}"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Use Cases
|
||||
|
||||
| Use Case | Example |
|
||||
|----------|---------|
|
||||
| **FAQ Answers** | Pre-compute answers to common customer questions |
|
||||
| **Onboarding Summaries** | "What should new team members know?" |
|
||||
| **Status Reports** | "What's the current project status?" refreshed weekly |
|
||||
| **Policy Summaries** | "What are our security policies?" |
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [**Reflect**](./reflect) — How the agentic loop uses mental models
|
||||
- [**Observations**](/developer/observations) — How knowledge is consolidated
|
||||
- [**Operations**](./operations) — Track async mental model creation
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
sidebar_position: 9
|
||||
---
|
||||
|
||||
# Operations
|
||||
|
||||
Background tasks that Hindsight executes asynchronously.
|
||||
|
||||
:::tip Prerequisites
|
||||
Make sure you've completed the [Quick Start](./quickstart) and understand [how retain works](./retain).
|
||||
:::
|
||||
|
||||
## How Operations Work
|
||||
|
||||
Hindsight processes several types of tasks in the background to maintain memory quality and consistency. These operations run automatically—you don't need to trigger them manually.
|
||||
|
||||
By default, all background operations are executed in-process within the API service.
|
||||
|
||||
:::note Kafka Integration
|
||||
Support for external streaming platforms like Kafka for scale-out processing is planned but **not available out of the box** in the current release.
|
||||
:::
|
||||
|
||||
## Operation Types
|
||||
|
||||
| Operation | Trigger | Description |
|
||||
|-----------|---------|-------------|
|
||||
| **batch_retain** | `retain_batch` with `async=True` | Processes large content batches in the background |
|
||||
| **consolidate** | After `retain` | Consolidates new facts into observations |
|
||||
|
||||
## Async Retain Example
|
||||
|
||||
When retaining large batches of memories, use `async=true` to process in the background. The response includes an `operation_id` that you can use to poll for completion.
|
||||
|
||||
### 1. Submit async retain request
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:8000/v1/default/banks/my-bank/memories" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"items": [
|
||||
{"content": "Alice joined Google in 2023"},
|
||||
{"content": "Bob prefers Python over JavaScript"}
|
||||
],
|
||||
"async": true
|
||||
}'
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"bank_id": "my-bank",
|
||||
"items_count": 2,
|
||||
"async": true,
|
||||
"operation_id": "550e8400-e29b-41d4-a716-446655440000"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Poll for operation status
|
||||
|
||||
```bash
|
||||
curl "http://localhost:8000/v1/default/banks/my-bank/operations"
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"bank_id": "my-bank",
|
||||
"operations": [
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"task_type": "retain",
|
||||
"items_count": 2,
|
||||
"document_id": null,
|
||||
"created_at": "2024-01-15T10:30:00Z",
|
||||
"status": "completed",
|
||||
"error_message": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Operation Status Values
|
||||
|
||||
| Status | Description |
|
||||
|--------|-------------|
|
||||
| `pending` | Operation is queued and waiting to be processed |
|
||||
| `completed` | Operation finished successfully |
|
||||
| `failed` | Operation failed (check `error_message` for details) |
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [**Documents**](./documents) — Track document sources
|
||||
- [**Memory Banks**](./memory-banks) — Configure bank settings
|
||||
@@ -0,0 +1,122 @@
|
||||
|
||||
# Quick Start
|
||||
|
||||
Get up and running with Hindsight in 60 seconds.
|
||||
|
||||
{/* Import raw source files */}
|
||||
|
||||
## Start the API Server
|
||||
|
||||
### pip (API only)
|
||||
|
||||
```bash
|
||||
pip install hindsight-api
|
||||
export OPENAI_API_KEY=sk-xxx
|
||||
export HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY
|
||||
|
||||
hindsight-api
|
||||
```
|
||||
|
||||
API available at [http://localhost:8888](http://localhost:8888/docs)
|
||||
|
||||
### Docker (Full Experience)
|
||||
|
||||
```bash
|
||||
|
||||
export OPENAI_API_KEY=sk-xxx
|
||||
|
||||
docker run --rm -it --pull always -p 8888:8888 -p 9999:9999 \
|
||||
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
|
||||
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
|
||||
ghcr.io/vectorize-io/hindsight:latest
|
||||
```
|
||||
|
||||
- **API**: http://localhost:8888
|
||||
- **Control Plane** (Web UI): http://localhost:9999
|
||||
|
||||
> **💡 LLM Provider**
|
||||
>
|
||||
Hindsight requires an LLM with structured output support. Recommended: **Groq** with `gpt-oss-20b` for fast, cost-effective inference.
|
||||
See [LLM Providers](/developer/models#llm) for more details.
|
||||
---
|
||||
|
||||
## Use the Client
|
||||
|
||||
### Python
|
||||
|
||||
```bash
|
||||
pip install hindsight-client
|
||||
```
|
||||
|
||||
```python
|
||||
from hindsight_client import Hindsight
|
||||
|
||||
client = Hindsight(base_url="http://localhost:8888")
|
||||
|
||||
# Retain: Store information
|
||||
client.retain(bank_id="my-bank", content="Alice works at Google as a software engineer")
|
||||
|
||||
# Recall: Search memories
|
||||
client.recall(bank_id="my-bank", query="What does Alice do?")
|
||||
|
||||
# Reflect: Generate disposition-aware response
|
||||
client.reflect(bank_id="my-bank", query="Tell me about Alice")
|
||||
```
|
||||
|
||||
### Node.js
|
||||
|
||||
```bash
|
||||
npm install @vectorize-io/hindsight-client
|
||||
```
|
||||
|
||||
```javascript
|
||||
import { HindsightClient } from '@vectorize-io/hindsight-client';
|
||||
|
||||
const client = new HindsightClient({ baseUrl: 'http://localhost:8888' });
|
||||
|
||||
// Retain: Store information
|
||||
await client.retain('my-bank', 'Alice works at Google as a software engineer');
|
||||
|
||||
// Recall: Search memories
|
||||
await client.recall('my-bank', 'What does Alice do?');
|
||||
|
||||
// Reflect: Generate response
|
||||
await client.reflect('my-bank', 'Tell me about Alice');
|
||||
```
|
||||
|
||||
### CLI
|
||||
|
||||
```bash
|
||||
curl -fsSL https://hindsight.vectorize.io/get-cli | bash
|
||||
```
|
||||
|
||||
```bash
|
||||
# Retain: Store information
|
||||
hindsight memory retain my-bank "Alice works at Google as a software engineer"
|
||||
|
||||
# Recall: Search memories
|
||||
hindsight memory recall my-bank "What does Alice do?"
|
||||
|
||||
# Reflect: Generate response
|
||||
hindsight memory reflect my-bank "Tell me about Alice"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What's Happening
|
||||
|
||||
| Operation | What it does |
|
||||
|-----------|--------------|
|
||||
| **Retain** | Content is processed, facts are extracted, entities are identified and linked in a knowledge graph |
|
||||
| **Recall** | Four search strategies (semantic, keyword, graph, temporal) run in parallel to find relevant memories |
|
||||
| **Reflect** | Retrieved memories are used to generate a disposition-aware response |
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [**Retain**](./retain) — Advanced options for storing memories
|
||||
- [**Recall**](./recall) — Search and retrieval strategies
|
||||
- [**Reflect**](./reflect) — Disposition-aware reasoning
|
||||
- [**Memory Banks**](./memory-banks) — Configure disposition and mission
|
||||
- [**Server Deployment**](/developer/installation) — Docker Compose, Helm, and production setup
|
||||
@@ -0,0 +1,235 @@
|
||||
|
||||
# Recall Memories
|
||||
|
||||
Retrieve memories using multi-strategy recall.
|
||||
|
||||
{/* Import raw source files */}
|
||||
|
||||
:::info How Recall Works
|
||||
Learn about the four retrieval strategies (semantic, keyword, graph, temporal) and RRF fusion in the [Recall Architecture](/developer/retrieval) guide.
|
||||
> **💡 Prerequisites**
|
||||
>
|
||||
Make sure you've completed the [Quick Start](./quickstart) to install the client and start the server.
|
||||
## Basic Recall
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
response = client.recall(bank_id="my-bank", query="What does Alice do?")
|
||||
for r in response.results:
|
||||
print(f"- {r.text}")
|
||||
```
|
||||
|
||||
### Node.js
|
||||
|
||||
```javascript
|
||||
const response = await client.recall('my-bank', 'What does Alice do?');
|
||||
for (const r of response.results) {
|
||||
console.log(`${r.text} (score: ${r.weight})`);
|
||||
}
|
||||
```
|
||||
|
||||
### CLI
|
||||
|
||||
```bash
|
||||
hindsight memory recall my-bank "What does Alice do?"
|
||||
```
|
||||
|
||||
## Recall Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `query` | string | required | Natural language query |
|
||||
| `types` | list | all | Filter: `world`, `experience`, `observation` |
|
||||
| `budget` | string | "mid" | Budget level: `low`, `mid`, `high` |
|
||||
| `max_tokens` | int | 4096 | Token budget for results |
|
||||
| `trace` | bool | false | Enable trace output for debugging |
|
||||
| `include_chunks` | bool | false | Include raw text chunks that generated the memories |
|
||||
| `max_chunk_tokens` | int | 500 | Token budget for chunks |
|
||||
| `tags` | list | None | Filter memories by tags (see [Tag Filtering](#filter-by-tags)) |
|
||||
| `tags_match` | string | "any" | How to match tags: `any`, `all`, `any_strict`, `all_strict` |
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
response = client.recall(
|
||||
bank_id="my-bank",
|
||||
query="What does Alice do?",
|
||||
types=["world", "experience"],
|
||||
budget="high",
|
||||
max_tokens=8000,
|
||||
trace=True,
|
||||
)
|
||||
|
||||
# Access results
|
||||
for r in response.results:
|
||||
print(f"- {r.text}")
|
||||
```
|
||||
|
||||
### Node.js
|
||||
|
||||
```javascript
|
||||
const detailedResponse = await client.recall('my-bank', 'What does Alice do?', {
|
||||
types: ['world', 'experience'],
|
||||
budget: 'high',
|
||||
maxTokens: 8000,
|
||||
trace: true
|
||||
});
|
||||
|
||||
// Access results
|
||||
for (const r of detailedResponse.results) {
|
||||
console.log(`${r.text} (score: ${r.weight})`);
|
||||
}
|
||||
```
|
||||
|
||||
## Filter by Fact Type
|
||||
|
||||
Recall specific memory types:
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
# Only world facts (objective information)
|
||||
world_facts = client.recall(
|
||||
bank_id="my-bank",
|
||||
query="Where does Alice work?",
|
||||
types=["world"]
|
||||
)
|
||||
```
|
||||
```python
|
||||
# Only experience (conversations and events)
|
||||
experience = client.recall(
|
||||
bank_id="my-bank",
|
||||
query="What have I recommended?",
|
||||
types=["experience"]
|
||||
)
|
||||
```
|
||||
```python
|
||||
# Only observations (consolidated knowledge)
|
||||
observations = client.recall(
|
||||
bank_id="my-bank",
|
||||
query="What patterns have I learned?",
|
||||
types=["observation"]
|
||||
)
|
||||
```
|
||||
|
||||
### CLI
|
||||
|
||||
```bash
|
||||
hindsight memory recall my-bank "query" --fact-type world,observation
|
||||
```
|
||||
|
||||
> **💡 About Observations**
|
||||
>
|
||||
Observations are consolidated knowledge synthesized from multiple facts. They capture patterns, preferences, and learnings that the memory bank has built up over time. Observations are automatically created in the background after retain operations.
|
||||
## Token Budget Management
|
||||
|
||||
Hindsight is built for AI agents, not humans. Traditional retrieval systems return "top-k" results, but agents don't think in terms of result counts—they think in tokens. An agent's context window is measured in tokens, and that's exactly how Hindsight measures results.
|
||||
|
||||
The `max_tokens` parameter lets you control how much of your agent's context budget to spend on memories:
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
# Fill up to 4K tokens of context with relevant memories
|
||||
results = client.recall(bank_id="my-bank", query="What do I know about Alice?", max_tokens=4096)
|
||||
|
||||
# Smaller budget for quick lookups
|
||||
results = client.recall(bank_id="my-bank", query="Alice's email", max_tokens=500)
|
||||
```
|
||||
|
||||
This design means you never have to guess whether 10 results or 50 results will fit your context. Just specify the token budget and Hindsight returns as many relevant memories as will fit.
|
||||
|
||||
## Budget Levels
|
||||
|
||||
The `budget` parameter controls graph traversal depth:
|
||||
|
||||
- **"low"**: Fast, shallow retrieval — good for simple lookups
|
||||
- **"mid"**: Balanced — default for most queries
|
||||
- **"high"**: Deep exploration — finds indirect connections
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
# Quick lookup
|
||||
results = client.recall(bank_id="my-bank", query="Alice's email", budget="low")
|
||||
|
||||
# Deep exploration
|
||||
results = client.recall(bank_id="my-bank", query="How are Alice and Bob connected?", budget="high")
|
||||
```
|
||||
|
||||
### Node.js
|
||||
|
||||
```javascript
|
||||
// Quick lookup
|
||||
const quickResults = await client.recall('my-bank', "Alice's email", { budget: 'low' });
|
||||
|
||||
// Deep exploration
|
||||
const deepResults = await client.recall('my-bank', 'How are Alice and Bob connected?', { budget: 'high' });
|
||||
```
|
||||
|
||||
## Filter by Tags
|
||||
|
||||
Tags enable **visibility scoping**—filter memories based on tags assigned during [retain](./retain#tagging-memories). This is essential for multi-user agents where each user should only see their own memories.
|
||||
|
||||
### Basic Tag Filtering
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
# Filter recall to only memories tagged for a specific user
|
||||
response = client.recall(
|
||||
bank_id="my-bank",
|
||||
query="What feedback did the user give?",
|
||||
tags=["user:alice"],
|
||||
tags_match="any" # OR matching, includes untagged (default)
|
||||
)
|
||||
```
|
||||
|
||||
### Tag Match Modes
|
||||
|
||||
The `tags_match` parameter controls how tags are matched:
|
||||
|
||||
| Mode | Behavior | Untagged Memories |
|
||||
|------|----------|-------------------|
|
||||
| `any` | OR: memory has ANY of the specified tags | **Included** |
|
||||
| `all` | AND: memory has ALL of the specified tags | **Included** |
|
||||
| `any_strict` | OR: memory has ANY of the specified tags | **Excluded** |
|
||||
| `all_strict` | AND: memory has ALL of the specified tags | **Excluded** |
|
||||
|
||||
**Strict modes** are useful when you want to ensure only tagged memories are returned:
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
# Strict mode: only return memories that have matching tags (exclude untagged)
|
||||
response = client.recall(
|
||||
bank_id="my-bank",
|
||||
query="What did the user say?",
|
||||
tags=["user:alice"],
|
||||
tags_match="any_strict" # OR matching, excludes untagged memories
|
||||
)
|
||||
```
|
||||
|
||||
**AND matching** requires all specified tags to be present:
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
# AND matching: require ALL specified tags to be present
|
||||
response = client.recall(
|
||||
bank_id="my-bank",
|
||||
query="What bugs were reported?",
|
||||
tags=["user:alice", "bug-report"],
|
||||
tags_match="all_strict" # Memory must have BOTH tags
|
||||
)
|
||||
```
|
||||
|
||||
### Use Cases
|
||||
|
||||
| Scenario | Tags | Mode | Result |
|
||||
|----------|------|------|--------|
|
||||
| User A's memories only | `["user:alice"]` | `any_strict` | Only memories tagged `user:alice` |
|
||||
| Support + feedback | `["support", "feedback"]` | `any` | Memories with either tag + untagged |
|
||||
| Multi-user room | `["user:alice", "room:general"]` | `all_strict` | Only memories with both tags |
|
||||
| Global + user-specific | `["user:alice"]` | `any` | Alice's memories + shared (untagged) |
|
||||
@@ -0,0 +1,274 @@
|
||||
|
||||
# Reflect
|
||||
|
||||
Generate disposition-aware responses using an agentic reasoning loop.
|
||||
|
||||
When you call **reflect**, Hindsight runs an **agentic loop** that:
|
||||
1. **Autonomously searches** for relevant information using multiple tools
|
||||
2. **Applies** the bank's disposition traits to shape the reasoning style
|
||||
3. **Generates** a grounded answer with citations to the sources used
|
||||
|
||||
The agent has access to hierarchical retrieval tools (mental models → observations → raw facts) and decides what information it needs to answer your query.
|
||||
|
||||
{/* Import raw source files */}
|
||||
|
||||
:::info How Reflect Works
|
||||
Learn about disposition-driven reasoning in the [Reflect Architecture](/developer/reflect) guide.
|
||||
> **💡 Prerequisites**
|
||||
>
|
||||
Make sure you've completed the [Quick Start](./quickstart) to install the client and start the server.
|
||||
## Basic Usage
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
client.reflect(bank_id="my-bank", query="What should I know about Alice?")
|
||||
```
|
||||
|
||||
### Node.js
|
||||
|
||||
```javascript
|
||||
await client.reflect('my-bank', 'What should I know about Alice?');
|
||||
```
|
||||
|
||||
### CLI
|
||||
|
||||
```bash
|
||||
hindsight memory reflect my-bank "What do you know about Alice?"
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `query` | string | required | Question or prompt |
|
||||
| `budget` | string | "low" | Budget level: `low`, `mid`, `high` (see below) |
|
||||
| `max_tokens` | int | 4096 | Maximum tokens for the final response |
|
||||
| `response_schema` | object | None | JSON Schema for [structured output](#structured-output) |
|
||||
| `tags` | list | None | Filter memories by tags during reflection |
|
||||
| `tags_match` | string | "any" | How to match tags: `any`, `all`, `any_strict`, `all_strict` |
|
||||
| `trace` | bool | false | Include detailed agent trace in response |
|
||||
|
||||
### Budget
|
||||
|
||||
The `budget` parameter controls the research depth — how thoroughly the agent explores before answering:
|
||||
|
||||
| Budget | Research Depth | Use Case |
|
||||
|--------|----------------|----------|
|
||||
| `low` | Shallow | Quick answers, simple lookups. Prioritizes speed over completeness. |
|
||||
| `mid` | Moderate | Balanced exploration. Checks multiple sources when warranted. |
|
||||
| `high` | Deep | Comprehensive analysis. Explores all knowledge levels, uses multiple query variations. |
|
||||
|
||||
Use `high` for complex questions that require synthesizing information from multiple sources or verifying facts across different retrieval levels.
|
||||
|
||||
### Max Tokens
|
||||
|
||||
The `max_tokens` parameter limits the length of the final generated response. This does not affect how much the agent can retrieve during the agentic loop — only the final answer length.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
response = client.reflect(
|
||||
bank_id="my-bank",
|
||||
query="What do you think about remote work?",
|
||||
budget="mid",
|
||||
context="We're considering a hybrid work policy"
|
||||
)
|
||||
```
|
||||
|
||||
### Node.js
|
||||
|
||||
```javascript
|
||||
const response = await client.reflect('my-bank', 'What do you think about remote work?', {
|
||||
budget: 'mid',
|
||||
context: "We're considering a hybrid work policy"
|
||||
});
|
||||
```
|
||||
|
||||
## Disposition Influence
|
||||
|
||||
The bank's disposition affects reflect responses:
|
||||
|
||||
| Trait | Low (1) | High (5) |
|
||||
|-------|---------|----------|
|
||||
| **Skepticism** | Trusting, accepts claims | Questions and doubts claims |
|
||||
| **Literalism** | Flexible interpretation | Exact, literal interpretation |
|
||||
| **Empathy** | Detached, fact-focused | Considers emotional context |
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
# Create a bank with specific disposition
|
||||
client.create_bank(
|
||||
bank_id="cautious-advisor",
|
||||
name="Cautious Advisor",
|
||||
mission="I am a risk-aware financial advisor",
|
||||
disposition={
|
||||
"skepticism": 5, # Very skeptical of claims
|
||||
"literalism": 4, # Focuses on exact requirements
|
||||
"empathy": 2 # Prioritizes facts over feelings
|
||||
}
|
||||
)
|
||||
|
||||
# Reflect responses will reflect this disposition
|
||||
response = client.reflect(
|
||||
bank_id="cautious-advisor",
|
||||
query="Should I invest in crypto?"
|
||||
)
|
||||
# Response will likely emphasize risks and caution
|
||||
```
|
||||
|
||||
### Node.js
|
||||
|
||||
```javascript
|
||||
// Create a bank with specific disposition
|
||||
await client.createBank('cautious-advisor', {
|
||||
name: 'Cautious Advisor',
|
||||
background: 'I am a risk-aware financial advisor',
|
||||
disposition: {
|
||||
skepticism: 5,
|
||||
literalism: 4,
|
||||
empathy: 2
|
||||
}
|
||||
});
|
||||
|
||||
// Reflect responses will reflect this disposition
|
||||
const advisorResponse = await client.reflect('cautious-advisor', 'Should I invest in crypto?');
|
||||
```
|
||||
|
||||
## Citations
|
||||
|
||||
The response includes a `based_on` field that shows which sources were used:
|
||||
|
||||
- `based_on.memories` — Memory facts (world, experience) that were retrieved and cited
|
||||
- `based_on.mental_models` — User-curated mental models that were used
|
||||
- `based_on.directives` — Directives that were enforced
|
||||
|
||||
**Important:** Only IDs that were actually retrieved during the agent loop can be cited. The agent validates citations to prevent hallucinated references.
|
||||
|
||||
This enables:
|
||||
- **Transparency** — users see exactly which sources informed the answer
|
||||
- **Verification** — check if the response is grounded in actual memories
|
||||
- **Debugging** — use `trace=True` for detailed tool call logs
|
||||
|
||||
## Structured Output
|
||||
|
||||
For applications that need to process responses programmatically, you can request structured output by providing a JSON Schema via `response_schema`. When provided, the response includes a `structured_output` field with the LLM response parsed according to the schema. The `text` field will be empty since only a single LLM call is made for efficiency.
|
||||
|
||||
The easiest way to define a schema is using **Pydantic models**:
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
|
||||
# Define your response structure with Pydantic
|
||||
class HiringRecommendation(BaseModel):
|
||||
recommendation: str
|
||||
confidence: str # "low", "medium", "high"
|
||||
key_factors: list[str]
|
||||
risks: list[str] = []
|
||||
|
||||
response = client.reflect(
|
||||
bank_id="hiring-team",
|
||||
query="Should we hire Alice for the ML team lead position?",
|
||||
response_schema=HiringRecommendation.model_json_schema(),
|
||||
)
|
||||
|
||||
# Parse structured output into Pydantic model
|
||||
result = HiringRecommendation.model_validate(response.structured_output)
|
||||
print(f"Recommendation: {result.recommendation}")
|
||||
print(f"Confidence: {result.confidence}")
|
||||
print(f"Key factors: {result.key_factors}")
|
||||
```
|
||||
|
||||
### Node.js
|
||||
|
||||
```javascript
|
||||
// Define JSON schema directly
|
||||
const responseSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
recommendation: { type: 'string' },
|
||||
confidence: { type: 'string', enum: ['low', 'medium', 'high'] },
|
||||
key_factors: { type: 'array', items: { type: 'string' } },
|
||||
risks: { type: 'array', items: { type: 'string' } },
|
||||
},
|
||||
required: ['recommendation', 'confidence', 'key_factors'],
|
||||
};
|
||||
|
||||
const structuredResponse = await client.reflect('my-bank', 'What do you know about Alice and her career?', {
|
||||
responseSchema: responseSchema,
|
||||
});
|
||||
|
||||
// Structured output (if returned)
|
||||
if (structuredResponse.structuredOutput) {
|
||||
console.log('Recommendation:', structuredResponse.structuredOutput.recommendation || 'N/A');
|
||||
console.log('Key factors:', structuredResponse.structuredOutput.key_factors || []);
|
||||
}
|
||||
```
|
||||
|
||||
### CLI
|
||||
|
||||
```bash
|
||||
# First, create a JSON schema file schema.json:
|
||||
cat > schema.json << 'EOF'
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"recommendation": {"type": "string"},
|
||||
"confidence": {"type": "string", "enum": ["low", "medium", "high"]},
|
||||
"key_factors": {"type": "array", "items": {"type": "string"}}
|
||||
},
|
||||
"required": ["recommendation", "confidence", "key_factors"]
|
||||
}
|
||||
EOF
|
||||
|
||||
# Then use the --schema flag:
|
||||
hindsight memory reflect hiring-team \
|
||||
"Should we hire Alice for the ML team lead position?" \
|
||||
--schema schema.json
|
||||
|
||||
# Cleanup the temporary schema file
|
||||
rm -f schema.json
|
||||
```
|
||||
|
||||
| Use Case | Why Structured Output Helps |
|
||||
|----------|----------------------------|
|
||||
| **Decision pipelines** | Parse recommendations into workflow systems |
|
||||
| **Dashboards** | Extract confidence scores, risk factors for visualization |
|
||||
| **Multi-agent systems** | Pass structured data between agents |
|
||||
| **Auditing** | Log structured decisions with clear reasoning |
|
||||
|
||||
**Tips:**
|
||||
- Use Pydantic's `model_json_schema()` for type-safe schema generation
|
||||
- Use `model_validate()` to parse the response back into your Pydantic model
|
||||
- Keep schemas focused — extract only what you need
|
||||
- Use `Optional` fields for data that may not always be available
|
||||
|
||||
## Filter by Tags
|
||||
|
||||
Like [recall](./recall#filter-by-tags), reflect supports tag filtering to scope which memories are considered during reasoning. This is essential for multi-user scenarios where reflection should only consider memories relevant to a specific user.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
# Filter reflection to only consider memories for a specific user
|
||||
response = client.reflect(
|
||||
bank_id="my-bank",
|
||||
query="What does this user think about our product?",
|
||||
tags=["user:alice"],
|
||||
tags_match="any_strict" # Only use memories tagged for this user
|
||||
)
|
||||
```
|
||||
|
||||
The `tags_match` parameter works the same as in recall:
|
||||
|
||||
| Mode | Behavior |
|
||||
|------|----------|
|
||||
| `any` | OR matching, includes untagged memories |
|
||||
| `all` | AND matching, includes untagged memories |
|
||||
| `any_strict` | OR matching, excludes untagged memories |
|
||||
| `all_strict` | AND matching, excludes untagged memories |
|
||||
|
||||
See [Retain API](./retain#tagging-memories) for how to tag memories and [Recall API](./recall#filter-by-tags) for more details on tag matching modes.
|
||||
@@ -0,0 +1,254 @@
|
||||
|
||||
# Ingest Data
|
||||
|
||||
Store documents, conversations, and raw content into Hindsight to automatically extract and create memories.
|
||||
|
||||
When you **retain** content, Hindsight doesn't just store the raw text—it intelligently analyzes the content to extract meaningful facts, identify entities, and build a connected knowledge graph. This process transforms unstructured information into structured, queryable memories.
|
||||
|
||||
{/* Import raw source files */}
|
||||
|
||||
:::info How Retain Works
|
||||
Learn about fact extraction, entity resolution, and graph construction in the [Retain Architecture](/developer/retain) guide.
|
||||
> **💡 Prerequisites**
|
||||
>
|
||||
Make sure you've completed the [Quick Start](./quickstart) to install the client and start the server.
|
||||
## Store a Single Memory
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
client.retain(
|
||||
bank_id="my-bank",
|
||||
content="Alice works at Google as a software engineer"
|
||||
)
|
||||
```
|
||||
|
||||
### Node.js
|
||||
|
||||
```javascript
|
||||
await client.retain('my-bank', 'Alice works at Google as a software engineer');
|
||||
```
|
||||
|
||||
### CLI
|
||||
|
||||
```bash
|
||||
hindsight memory retain my-bank "Alice works at Google as a software engineer"
|
||||
```
|
||||
|
||||
## The Importance of Context
|
||||
|
||||
The `context` parameter is crucial for guiding how Hindsight extracts memories from your content. Think of it as providing a lens through which the system interprets the information.
|
||||
|
||||
**Why context matters:**
|
||||
- **Steers memory extraction**: Context tells the memory bank what type of information to focus on and how to interpret ambiguous content
|
||||
- **Improves relevance**: Memories extracted with proper context are more accurately categorized and easier to retrieve
|
||||
- **Disambiguates meaning**: The same sentence can have different implications depending on context (e.g., "the project was terminated" means different things in a career vs. product context)
|
||||
|
||||
## Store with Context and Date
|
||||
|
||||
Always provide context and event dates for optimal memory extraction:
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
client.retain(
|
||||
bank_id="my-bank",
|
||||
content="Alice got promoted to senior engineer",
|
||||
context="career update",
|
||||
timestamp="2024-03-15T10:00:00Z"
|
||||
)
|
||||
```
|
||||
|
||||
### Node.js
|
||||
|
||||
```javascript
|
||||
await client.retain('my-bank', 'Alice got promoted to senior engineer', {
|
||||
context: 'career update',
|
||||
timestamp: '2024-03-15T10:00:00Z'
|
||||
});
|
||||
```
|
||||
|
||||
### CLI
|
||||
|
||||
```bash
|
||||
hindsight memory retain my-bank "Alice got promoted" \
|
||||
--context "career update"
|
||||
```
|
||||
|
||||
The `timestamp` defaults to the current time if not specified. Providing explicit timestamps enables temporal queries like "What happened last spring?"
|
||||
|
||||
### Response Fields
|
||||
|
||||
The retain response includes:
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `success` | bool | Whether the operation succeeded |
|
||||
| `bank_id` | string | The memory bank ID |
|
||||
| `items_count` | int | Number of items processed |
|
||||
| `async` | bool | Whether processed asynchronously |
|
||||
| `usage` | TokenUsage | Token usage metrics for LLM calls (synchronous only) |
|
||||
|
||||
The `usage` field contains token metrics for cost tracking:
|
||||
- `input_tokens`: Tokens consumed by prompts
|
||||
- `output_tokens`: Tokens generated by the LLM
|
||||
- `total_tokens`: Sum of input and output tokens
|
||||
|
||||
Note: `usage` is only present for synchronous operations. Async operations (`async: true`) do not return usage metrics.
|
||||
|
||||
## Batch Ingestion
|
||||
|
||||
Store multiple items in a single request. **Batch ingestion is the recommended approach** as it significantly improves performance by reducing network overhead and allowing Hindsight to optimize the memory extraction process across related content.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
client.retain_batch(
|
||||
bank_id="my-bank",
|
||||
items=[
|
||||
{"content": "Alice works at Google", "context": "career"},
|
||||
{"content": "Bob is a data scientist at Meta", "context": "career"},
|
||||
{"content": "Alice and Bob are friends", "context": "relationship"}
|
||||
],
|
||||
document_id="conversation_001"
|
||||
)
|
||||
```
|
||||
|
||||
### Node.js
|
||||
|
||||
```javascript
|
||||
await client.retainBatch('my-bank', [
|
||||
{ content: 'Alice works at Google', context: 'career' },
|
||||
{ content: 'Bob is a data scientist at Meta', context: 'career' },
|
||||
{ content: 'Alice and Bob are friends', context: 'relationship' }
|
||||
], { documentId: 'conversation_001' });
|
||||
```
|
||||
|
||||
The `document_id` groups related memories for later management.
|
||||
|
||||
## Store from Files
|
||||
|
||||
### CLI
|
||||
|
||||
```bash
|
||||
# Single file
|
||||
hindsight memory retain-files my-bank document.txt
|
||||
|
||||
# Directory (recursive by default)
|
||||
hindsight memory retain-files my-bank ./documents/
|
||||
```
|
||||
|
||||
## Async Ingestion
|
||||
|
||||
For large batches, use async ingestion to avoid blocking:
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
# Start async ingestion (returns immediately)
|
||||
result = client.retain_batch(
|
||||
bank_id="my-bank",
|
||||
items=[
|
||||
{"content": "Large batch item 1"},
|
||||
{"content": "Large batch item 2"},
|
||||
],
|
||||
document_id="large-doc",
|
||||
retain_async=True
|
||||
)
|
||||
|
||||
# Check if it was processed asynchronously
|
||||
print(result.var_async) # True
|
||||
```
|
||||
|
||||
### Node.js
|
||||
|
||||
```javascript
|
||||
// Start async ingestion (returns immediately)
|
||||
await client.retainBatch('my-bank', [
|
||||
{ content: 'Large batch item 1' },
|
||||
{ content: 'Large batch item 2' },
|
||||
], {
|
||||
documentId: 'large-doc',
|
||||
async: true
|
||||
});
|
||||
```
|
||||
|
||||
## Tagging Memories
|
||||
|
||||
Tags enable **visibility scoping**—useful when one memory bank serves multiple users but each should only see relevant memories. For example, an agent that chats with multiple users can tag memories by user ID and filter during recall.
|
||||
|
||||
### Tag Individual Items
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
# Tag individual items for visibility scoping
|
||||
client.retain_batch(
|
||||
bank_id="my-bank",
|
||||
items=[
|
||||
{
|
||||
"content": "User Alice said she loves the new dashboard",
|
||||
"tags": ["user:alice", "feedback"]
|
||||
},
|
||||
{
|
||||
"content": "User Bob reported a bug in the search feature",
|
||||
"tags": ["user:bob", "bug-report"]
|
||||
}
|
||||
],
|
||||
document_id="user_feedback_001"
|
||||
)
|
||||
```
|
||||
|
||||
### Apply Tags to All Items in a Batch
|
||||
|
||||
Use `document_tags` to apply the same tags to all items in a request:
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
# Apply tags to all items in a batch
|
||||
client.retain_batch(
|
||||
bank_id="my-bank",
|
||||
items=[
|
||||
{"content": "Alice mentioned she prefers dark mode"},
|
||||
{"content": "Bob asked about keyboard shortcuts"}
|
||||
],
|
||||
document_id="support_session_123",
|
||||
document_tags=["session:123", "support"] # Applied to all items
|
||||
)
|
||||
```
|
||||
|
||||
When both `document_tags` and item-level `tags` are provided, they are merged together.
|
||||
|
||||
### Tag Naming Conventions
|
||||
|
||||
Use consistent naming patterns for tags:
|
||||
|
||||
| Pattern | Example | Use Case |
|
||||
|---------|---------|----------|
|
||||
| `user:<id>` | `user:alice` | Multi-user agent filtering |
|
||||
| `session:<id>` | `session:123` | Session-based scoping |
|
||||
| `room:<id>` | `room:general` | Chat room isolation |
|
||||
| `topic:<name>` | `topic:feedback` | Topic categorization |
|
||||
|
||||
### Listing Tags
|
||||
|
||||
Use the list tags API to discover existing tags, useful for UI autocomplete or wildcard expansion:
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
# List all tags in a bank
|
||||
response = requests.get(f"{HINDSIGHT_URL}/v1/default/banks/my-bank/tags")
|
||||
tags = response.json()
|
||||
for tag in tags["items"]:
|
||||
print(f"{tag['tag']}: {tag['count']} memories")
|
||||
|
||||
# Search with wildcards (* matches any characters)
|
||||
response = requests.get(f"{HINDSIGHT_URL}/v1/default/banks/my-bank/tags", params={"q": "user:*"})
|
||||
user_tags = response.json()
|
||||
response = requests.get(f"{HINDSIGHT_URL}/v1/default/banks/my-bank/tags", params={"q": "*-admin"})
|
||||
admin_tags = response.json()
|
||||
```
|
||||
|
||||
See [Recall API](./recall#filter-by-tags) for filtering memories by tags during retrieval.
|
||||
@@ -0,0 +1,595 @@
|
||||
# Configuration
|
||||
|
||||
Complete reference for configuring Hindsight services through environment variables.
|
||||
|
||||
Hindsight has two services, each with its own configuration prefix:
|
||||
|
||||
| Service | Prefix | Description |
|
||||
|---------|--------|-------------|
|
||||
| **API Service** | `HINDSIGHT_API_*` | Core memory engine |
|
||||
| **Control Plane** | `HINDSIGHT_CP_*` | Web UI |
|
||||
|
||||
---
|
||||
|
||||
## API Service
|
||||
|
||||
The API service handles all memory operations (retain, recall, reflect).
|
||||
|
||||
### Database
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `HINDSIGHT_API_DATABASE_URL` | PostgreSQL connection string | `pg0` (embedded) |
|
||||
| `HINDSIGHT_API_DATABASE_SCHEMA` | PostgreSQL schema name for tables | `public` |
|
||||
| `HINDSIGHT_API_RUN_MIGRATIONS_ON_STARTUP` | Run database migrations on API startup | `true` |
|
||||
|
||||
If not provided, the server uses embedded `pg0` — convenient for development but not recommended for production.
|
||||
|
||||
The `DATABASE_SCHEMA` setting allows you to use a custom PostgreSQL schema instead of the default `public` schema. This is useful for:
|
||||
- Multi-database setups where you want Hindsight tables in a dedicated schema
|
||||
- Hosting platforms (e.g., Supabase) where `public` schema is reserved or shared
|
||||
- Organizational preferences for schema naming conventions
|
||||
|
||||
```bash
|
||||
# Example: Using a custom schema
|
||||
export HINDSIGHT_API_DATABASE_URL=postgresql://user:pass@host:5432/dbname
|
||||
export HINDSIGHT_API_DATABASE_SCHEMA=hindsight
|
||||
```
|
||||
|
||||
Migrations will automatically create the schema if it doesn't exist and create all tables in the configured schema.
|
||||
|
||||
### Database Connection Pool
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `HINDSIGHT_API_DB_POOL_MIN_SIZE` | Minimum connections in the pool | `5` |
|
||||
| `HINDSIGHT_API_DB_POOL_MAX_SIZE` | Maximum connections in the pool | `100` |
|
||||
| `HINDSIGHT_API_DB_COMMAND_TIMEOUT` | PostgreSQL command timeout in seconds | `60` |
|
||||
| `HINDSIGHT_API_DB_ACQUIRE_TIMEOUT` | Connection acquisition timeout in seconds | `30` |
|
||||
|
||||
For high-concurrency workloads, increase `DB_POOL_MAX_SIZE`. Each concurrent recall/think operation can use 2-4 connections.
|
||||
|
||||
To run migrations manually (e.g., before starting the API), use the admin CLI:
|
||||
|
||||
```bash
|
||||
hindsight-admin run-db-migration
|
||||
# Or for a specific schema:
|
||||
hindsight-admin run-db-migration --schema tenant_acme
|
||||
```
|
||||
|
||||
### LLM Provider
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `HINDSIGHT_API_LLM_PROVIDER` | Provider: `openai`, `openai-codex`, `claude-code`, `anthropic`, `gemini`, `groq`, `ollama`, `lmstudio`, `vertexai` | `openai` |
|
||||
| `HINDSIGHT_API_LLM_API_KEY` | API key for LLM provider | - |
|
||||
| `HINDSIGHT_API_LLM_MODEL` | Model name | `gpt-5-mini` |
|
||||
| `HINDSIGHT_API_LLM_BASE_URL` | Custom LLM endpoint | Provider default |
|
||||
| `HINDSIGHT_API_LLM_MAX_CONCURRENT` | Max concurrent LLM requests | `32` |
|
||||
| `HINDSIGHT_API_LLM_MAX_RETRIES` | Max retry attempts for LLM API calls | `10` |
|
||||
| `HINDSIGHT_API_LLM_INITIAL_BACKOFF` | Initial retry backoff in seconds (exponential backoff) | `1.0` |
|
||||
| `HINDSIGHT_API_LLM_MAX_BACKOFF` | Max retry backoff cap in seconds | `60.0` |
|
||||
| `HINDSIGHT_API_LLM_TIMEOUT` | LLM request timeout in seconds | `120` |
|
||||
| `HINDSIGHT_API_LLM_GROQ_SERVICE_TIER` | Groq service tier: `on_demand`, `flex`, `auto` | `auto` |
|
||||
|
||||
**Provider Examples**
|
||||
|
||||
```bash
|
||||
# Groq (recommended for fast inference)
|
||||
export HINDSIGHT_API_LLM_PROVIDER=groq
|
||||
export HINDSIGHT_API_LLM_API_KEY=gsk_xxxxxxxxxxxx
|
||||
export HINDSIGHT_API_LLM_MODEL=openai/gpt-oss-20b
|
||||
# For free tier users: override to on_demand if you get service_tier errors
|
||||
# export HINDSIGHT_API_LLM_GROQ_SERVICE_TIER=on_demand
|
||||
|
||||
# OpenAI
|
||||
export HINDSIGHT_API_LLM_PROVIDER=openai
|
||||
export HINDSIGHT_API_LLM_API_KEY=sk-xxxxxxxxxxxx
|
||||
export HINDSIGHT_API_LLM_MODEL=gpt-4o
|
||||
|
||||
# Gemini
|
||||
export HINDSIGHT_API_LLM_PROVIDER=gemini
|
||||
export HINDSIGHT_API_LLM_API_KEY=xxxxxxxxxxxx
|
||||
export HINDSIGHT_API_LLM_MODEL=gemini-2.0-flash
|
||||
|
||||
# Anthropic
|
||||
export HINDSIGHT_API_LLM_PROVIDER=anthropic
|
||||
export HINDSIGHT_API_LLM_API_KEY=sk-ant-xxxxxxxxxxxx
|
||||
export HINDSIGHT_API_LLM_MODEL=claude-sonnet-4-20250514
|
||||
|
||||
# Vertex AI (Google Cloud - uses native genai SDK)
|
||||
export HINDSIGHT_API_LLM_PROVIDER=vertexai
|
||||
export HINDSIGHT_API_LLM_MODEL=gemini-2.0-flash-001
|
||||
export HINDSIGHT_API_LLM_VERTEXAI_PROJECT_ID=your-gcp-project-id
|
||||
export HINDSIGHT_API_LLM_VERTEXAI_REGION=us-central1
|
||||
# Optional: use ADC (gcloud auth application-default login) or provide service account key:
|
||||
# export HINDSIGHT_API_LLM_VERTEXAI_SERVICE_ACCOUNT_KEY=/path/to/service-account-key.json
|
||||
|
||||
# Ollama (local, no API key)
|
||||
export HINDSIGHT_API_LLM_PROVIDER=ollama
|
||||
export HINDSIGHT_API_LLM_BASE_URL=http://localhost:11434/v1
|
||||
export HINDSIGHT_API_LLM_MODEL=llama3
|
||||
|
||||
# LM Studio (local, no API key)
|
||||
export HINDSIGHT_API_LLM_PROVIDER=lmstudio
|
||||
export HINDSIGHT_API_LLM_BASE_URL=http://localhost:1234/v1
|
||||
export HINDSIGHT_API_LLM_MODEL=your-local-model
|
||||
|
||||
# OpenAI-compatible endpoint
|
||||
export HINDSIGHT_API_LLM_PROVIDER=openai
|
||||
export HINDSIGHT_API_LLM_BASE_URL=https://your-endpoint.com/v1
|
||||
export HINDSIGHT_API_LLM_API_KEY=your-api-key
|
||||
export HINDSIGHT_API_LLM_MODEL=your-model-name
|
||||
|
||||
# OpenAI Codex (ChatGPT Plus/Pro subscription - uses OAuth, no API key needed)
|
||||
export HINDSIGHT_API_LLM_PROVIDER=openai-codex
|
||||
export HINDSIGHT_API_LLM_MODEL=gpt-5.2-codex
|
||||
# No API key needed - uses OAuth tokens from ~/.codex/auth.json
|
||||
|
||||
# Claude Code (Claude Pro/Max subscription - uses OAuth, no API key needed)
|
||||
export HINDSIGHT_API_LLM_PROVIDER=claude-code
|
||||
export HINDSIGHT_API_LLM_MODEL=claude-sonnet-4-5-20250929
|
||||
# No API key needed - uses claude auth login credentials
|
||||
```
|
||||
|
||||
:::tip OpenAI Codex & Claude Code Setup
|
||||
For detailed setup instructions for **OpenAI Codex** (ChatGPT Plus/Pro) and **Claude Code** (Claude Pro/Max), see the [Models documentation](./models#openai-codex-setup-chatgpt-pluspro).
|
||||
:::
|
||||
|
||||
#### Vertex AI Setup
|
||||
|
||||
Google Cloud's Vertex AI provides access to Gemini models via the native Google GenAI SDK. Hindsight supports two authentication methods:
|
||||
|
||||
**Prerequisites:**
|
||||
- GCP project with Vertex AI API enabled
|
||||
- IAM role `roles/aiplatform.user` for your credentials
|
||||
|
||||
**Environment Variables:**
|
||||
|
||||
| Variable | Description | Required |
|
||||
|----------|-------------|----------|
|
||||
| `HINDSIGHT_API_LLM_VERTEXAI_PROJECT_ID` | Your GCP project ID | Yes |
|
||||
| `HINDSIGHT_API_LLM_VERTEXAI_REGION` | GCP region (e.g., `us-central1`) | No (default: `us-central1`) |
|
||||
| `HINDSIGHT_API_LLM_VERTEXAI_SERVICE_ACCOUNT_KEY` | Path to service account JSON key file | No (uses ADC if not set) |
|
||||
|
||||
**Authentication Methods:**
|
||||
|
||||
1. **Application Default Credentials (ADC)** - Recommended for development
|
||||
```bash
|
||||
# Setup ADC
|
||||
gcloud auth application-default login
|
||||
|
||||
# Configure Hindsight
|
||||
export HINDSIGHT_API_LLM_PROVIDER=vertexai
|
||||
export HINDSIGHT_API_LLM_MODEL=gemini-2.0-flash-001
|
||||
export HINDSIGHT_API_LLM_VERTEXAI_PROJECT_ID=your-project-id
|
||||
```
|
||||
|
||||
2. **Service Account Key** - Recommended for production
|
||||
```bash
|
||||
# Create service account and download key
|
||||
gcloud iam service-accounts create hindsight-api
|
||||
gcloud projects add-iam-policy-binding your-project-id \
|
||||
--member="serviceAccount:[email protected]" \
|
||||
--role="roles/aiplatform.user"
|
||||
gcloud iam service-accounts keys create key.json \
|
||||
--iam-account=hindsight-api@your-project-id.iam.gserviceaccount.com
|
||||
|
||||
# Configure Hindsight
|
||||
export HINDSIGHT_API_LLM_PROVIDER=vertexai
|
||||
export HINDSIGHT_API_LLM_MODEL=gemini-2.0-flash-001
|
||||
export HINDSIGHT_API_LLM_VERTEXAI_PROJECT_ID=your-project-id
|
||||
export HINDSIGHT_API_LLM_VERTEXAI_SERVICE_ACCOUNT_KEY=/path/to/key.json
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- Model names can optionally include the `google/` prefix (e.g., `google/gemini-2.0-flash-001`) - it will be stripped automatically
|
||||
- The native SDK handles token refresh automatically
|
||||
- Uses service account credentials if provided, otherwise falls back to ADC
|
||||
|
||||
### Per-Operation LLM Configuration
|
||||
|
||||
Different memory operations have different requirements. **Retain** (fact extraction) benefits from models with strong structured output capabilities, while **Reflect** (reasoning/response generation) can use lighter, faster models. Configure separate LLM models for each operation to optimize for cost and performance.
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `HINDSIGHT_API_RETAIN_LLM_PROVIDER` | LLM provider for retain operations | Falls back to `HINDSIGHT_API_LLM_PROVIDER` |
|
||||
| `HINDSIGHT_API_RETAIN_LLM_API_KEY` | API key for retain LLM | Falls back to `HINDSIGHT_API_LLM_API_KEY` |
|
||||
| `HINDSIGHT_API_RETAIN_LLM_MODEL` | Model for retain operations | Falls back to `HINDSIGHT_API_LLM_MODEL` |
|
||||
| `HINDSIGHT_API_RETAIN_LLM_BASE_URL` | Base URL for retain LLM | Falls back to `HINDSIGHT_API_LLM_BASE_URL` |
|
||||
| `HINDSIGHT_API_RETAIN_LLM_MAX_CONCURRENT` | Max concurrent requests for retain | Falls back to `HINDSIGHT_API_LLM_MAX_CONCURRENT` |
|
||||
| `HINDSIGHT_API_RETAIN_LLM_MAX_RETRIES` | Max retries for retain | Falls back to `HINDSIGHT_API_LLM_MAX_RETRIES` |
|
||||
| `HINDSIGHT_API_RETAIN_LLM_INITIAL_BACKOFF` | Initial backoff for retain retries (seconds) | Falls back to `HINDSIGHT_API_LLM_INITIAL_BACKOFF` |
|
||||
| `HINDSIGHT_API_RETAIN_LLM_MAX_BACKOFF` | Max backoff cap for retain retries (seconds) | Falls back to `HINDSIGHT_API_LLM_MAX_BACKOFF` |
|
||||
| `HINDSIGHT_API_RETAIN_LLM_TIMEOUT` | Timeout for retain requests (seconds) | Falls back to `HINDSIGHT_API_LLM_TIMEOUT` |
|
||||
| `HINDSIGHT_API_REFLECT_LLM_PROVIDER` | LLM provider for reflect operations | Falls back to `HINDSIGHT_API_LLM_PROVIDER` |
|
||||
| `HINDSIGHT_API_REFLECT_LLM_API_KEY` | API key for reflect LLM | Falls back to `HINDSIGHT_API_LLM_API_KEY` |
|
||||
| `HINDSIGHT_API_REFLECT_LLM_MODEL` | Model for reflect operations | Falls back to `HINDSIGHT_API_LLM_MODEL` |
|
||||
| `HINDSIGHT_API_REFLECT_LLM_BASE_URL` | Base URL for reflect LLM | Falls back to `HINDSIGHT_API_LLM_BASE_URL` |
|
||||
| `HINDSIGHT_API_REFLECT_LLM_MAX_CONCURRENT` | Max concurrent requests for reflect | Falls back to `HINDSIGHT_API_LLM_MAX_CONCURRENT` |
|
||||
| `HINDSIGHT_API_REFLECT_LLM_MAX_RETRIES` | Max retries for reflect | Falls back to `HINDSIGHT_API_LLM_MAX_RETRIES` |
|
||||
| `HINDSIGHT_API_REFLECT_LLM_INITIAL_BACKOFF` | Initial backoff for reflect retries (seconds) | Falls back to `HINDSIGHT_API_LLM_INITIAL_BACKOFF` |
|
||||
| `HINDSIGHT_API_REFLECT_LLM_MAX_BACKOFF` | Max backoff cap for reflect retries (seconds) | Falls back to `HINDSIGHT_API_LLM_MAX_BACKOFF` |
|
||||
| `HINDSIGHT_API_REFLECT_LLM_TIMEOUT` | Timeout for reflect requests (seconds) | Falls back to `HINDSIGHT_API_LLM_TIMEOUT` |
|
||||
| `HINDSIGHT_API_CONSOLIDATION_LLM_PROVIDER` | LLM provider for observation consolidation | Falls back to `HINDSIGHT_API_LLM_PROVIDER` |
|
||||
| `HINDSIGHT_API_CONSOLIDATION_LLM_API_KEY` | API key for consolidation LLM | Falls back to `HINDSIGHT_API_LLM_API_KEY` |
|
||||
| `HINDSIGHT_API_CONSOLIDATION_LLM_MODEL` | Model for consolidation operations | Falls back to `HINDSIGHT_API_LLM_MODEL` |
|
||||
| `HINDSIGHT_API_CONSOLIDATION_LLM_BASE_URL` | Base URL for consolidation LLM | Falls back to `HINDSIGHT_API_LLM_BASE_URL` |
|
||||
| `HINDSIGHT_API_CONSOLIDATION_LLM_MAX_CONCURRENT` | Max concurrent requests for consolidation | Falls back to `HINDSIGHT_API_LLM_MAX_CONCURRENT` |
|
||||
| `HINDSIGHT_API_CONSOLIDATION_LLM_MAX_RETRIES` | Max retries for consolidation | Falls back to `HINDSIGHT_API_LLM_MAX_RETRIES` |
|
||||
| `HINDSIGHT_API_CONSOLIDATION_LLM_INITIAL_BACKOFF` | Initial backoff for consolidation retries (seconds) | Falls back to `HINDSIGHT_API_LLM_INITIAL_BACKOFF` |
|
||||
| `HINDSIGHT_API_CONSOLIDATION_LLM_MAX_BACKOFF` | Max backoff cap for consolidation retries (seconds) | Falls back to `HINDSIGHT_API_LLM_MAX_BACKOFF` |
|
||||
| `HINDSIGHT_API_CONSOLIDATION_LLM_TIMEOUT` | Timeout for consolidation requests (seconds) | Falls back to `HINDSIGHT_API_LLM_TIMEOUT` |
|
||||
|
||||
:::tip When to Use Per-Operation Config
|
||||
- **Retain**: Use models with strong structured output (e.g., GPT-4o, Claude) for accurate fact extraction
|
||||
- **Reflect**: Use faster/cheaper models (e.g., GPT-4o-mini, Groq) for reasoning and response generation
|
||||
- **Recall**: Does not use LLM (pure retrieval), so no configuration needed
|
||||
:::
|
||||
|
||||
**Example: Separate Models for Retain and Reflect**
|
||||
|
||||
```bash
|
||||
# Default LLM (used as fallback)
|
||||
export HINDSIGHT_API_LLM_PROVIDER=openai
|
||||
export HINDSIGHT_API_LLM_API_KEY=sk-xxxxxxxxxxxx
|
||||
export HINDSIGHT_API_LLM_MODEL=gpt-4o
|
||||
|
||||
# Use GPT-4o for retain (strong structured output)
|
||||
export HINDSIGHT_API_RETAIN_LLM_MODEL=gpt-4o
|
||||
|
||||
# Use faster/cheaper model for reflect
|
||||
export HINDSIGHT_API_REFLECT_LLM_PROVIDER=groq
|
||||
export HINDSIGHT_API_REFLECT_LLM_API_KEY=gsk_xxxxxxxxxxxx
|
||||
export HINDSIGHT_API_REFLECT_LLM_MODEL=llama-3.3-70b-versatile
|
||||
```
|
||||
|
||||
**Example: Tuning Retry Behavior for Rate-Limited APIs**
|
||||
|
||||
```bash
|
||||
# For Anthropic with tight rate limits (10k output tokens/minute)
|
||||
export HINDSIGHT_API_LLM_PROVIDER=anthropic
|
||||
export HINDSIGHT_API_LLM_API_KEY=sk-ant-xxxxxxxxxxxx
|
||||
export HINDSIGHT_API_LLM_MODEL=claude-sonnet-4-20250514
|
||||
|
||||
# Reduce concurrent requests for retain to avoid rate limits
|
||||
export HINDSIGHT_API_RETAIN_LLM_MAX_CONCURRENT=3
|
||||
|
||||
# Fail faster with fewer retries
|
||||
export HINDSIGHT_API_RETAIN_LLM_MAX_RETRIES=3
|
||||
|
||||
# Or increase backoff times to wait out rate limit windows
|
||||
export HINDSIGHT_API_RETAIN_LLM_INITIAL_BACKOFF=2.0 # Start at 2s instead of 1s
|
||||
export HINDSIGHT_API_RETAIN_LLM_MAX_BACKOFF=120.0 # Cap at 2min instead of 1min
|
||||
```
|
||||
|
||||
### Embeddings
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `HINDSIGHT_API_EMBEDDINGS_PROVIDER` | Provider: `local`, `tei`, `openai`, `cohere`, or `litellm` | `local` |
|
||||
| `HINDSIGHT_API_EMBEDDINGS_LOCAL_MODEL` | Model for local provider | `BAAI/bge-small-en-v1.5` |
|
||||
| `HINDSIGHT_API_EMBEDDINGS_TEI_URL` | TEI server URL | - |
|
||||
| `HINDSIGHT_API_EMBEDDINGS_OPENAI_API_KEY` | OpenAI API key (falls back to `HINDSIGHT_API_LLM_API_KEY`) | - |
|
||||
| `HINDSIGHT_API_EMBEDDINGS_OPENAI_MODEL` | OpenAI embedding model | `text-embedding-3-small` |
|
||||
| `HINDSIGHT_API_EMBEDDINGS_OPENAI_BASE_URL` | Custom base URL for OpenAI-compatible API (e.g., Azure OpenAI) | - |
|
||||
| `HINDSIGHT_API_COHERE_API_KEY` | Cohere API key (shared for embeddings and reranker) | - |
|
||||
| `HINDSIGHT_API_EMBEDDINGS_COHERE_MODEL` | Cohere embedding model | `embed-english-v3.0` |
|
||||
| `HINDSIGHT_API_EMBEDDINGS_COHERE_BASE_URL` | Custom base URL for Cohere-compatible API (e.g., Azure-hosted) | - |
|
||||
| `HINDSIGHT_API_LITELLM_API_BASE` | LiteLLM proxy base URL (shared for embeddings and reranker) | `http://localhost:4000` |
|
||||
| `HINDSIGHT_API_LITELLM_API_KEY` | LiteLLM proxy API key (optional, depends on proxy config) | - |
|
||||
| `HINDSIGHT_API_EMBEDDINGS_LITELLM_MODEL` | LiteLLM embedding model (use provider prefix, e.g., `cohere/embed-english-v3.0`) | `text-embedding-3-small` |
|
||||
|
||||
```bash
|
||||
# Local (default) - uses SentenceTransformers
|
||||
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=local
|
||||
export HINDSIGHT_API_EMBEDDINGS_LOCAL_MODEL=BAAI/bge-small-en-v1.5
|
||||
|
||||
# OpenAI - cloud-based embeddings
|
||||
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=openai
|
||||
export HINDSIGHT_API_EMBEDDINGS_OPENAI_API_KEY=sk-xxxxxxxxxxxx # or reuses HINDSIGHT_API_LLM_API_KEY
|
||||
export HINDSIGHT_API_EMBEDDINGS_OPENAI_MODEL=text-embedding-3-small # 1536 dimensions
|
||||
|
||||
# Azure OpenAI - embeddings via Azure endpoint
|
||||
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=openai
|
||||
export HINDSIGHT_API_EMBEDDINGS_OPENAI_API_KEY=your-azure-api-key
|
||||
export HINDSIGHT_API_EMBEDDINGS_OPENAI_MODEL=text-embedding-3-small
|
||||
export HINDSIGHT_API_EMBEDDINGS_OPENAI_BASE_URL=https://your-resource.openai.azure.com/openai/deployments/your-deployment
|
||||
|
||||
# TEI - HuggingFace Text Embeddings Inference (recommended for production)
|
||||
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=tei
|
||||
export HINDSIGHT_API_EMBEDDINGS_TEI_URL=http://localhost:8080
|
||||
|
||||
# Cohere - cloud-based embeddings
|
||||
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=cohere
|
||||
export HINDSIGHT_API_COHERE_API_KEY=your-api-key
|
||||
export HINDSIGHT_API_EMBEDDINGS_COHERE_MODEL=embed-english-v3.0 # 1024 dimensions
|
||||
|
||||
# Azure-hosted Cohere - embeddings via custom endpoint
|
||||
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=cohere
|
||||
export HINDSIGHT_API_COHERE_API_KEY=your-azure-api-key
|
||||
export HINDSIGHT_API_EMBEDDINGS_COHERE_MODEL=embed-english-v3.0
|
||||
export HINDSIGHT_API_EMBEDDINGS_COHERE_BASE_URL=https://your-azure-cohere-endpoint.com
|
||||
|
||||
# LiteLLM proxy - unified gateway for multiple providers
|
||||
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=litellm
|
||||
export HINDSIGHT_API_LITELLM_API_BASE=http://localhost:4000
|
||||
export HINDSIGHT_API_LITELLM_API_KEY=your-litellm-key # optional
|
||||
export HINDSIGHT_API_EMBEDDINGS_LITELLM_MODEL=text-embedding-3-small # or cohere/embed-english-v3.0
|
||||
```
|
||||
|
||||
#### Embedding Dimensions
|
||||
|
||||
Hindsight automatically detects the embedding dimension from the model at startup and adjusts the database schema accordingly. The default model (`BAAI/bge-small-en-v1.5`) produces 384-dimensional vectors, while OpenAI models produce 1536 or 3072 dimensions.
|
||||
|
||||
:::warning Dimension Changes
|
||||
Once memories are stored, you cannot change the embedding dimension without losing data. If you need to switch to a model with different dimensions:
|
||||
|
||||
1. **Empty database**: The schema is adjusted automatically on startup
|
||||
2. **Existing data**: Either delete all memories first, or use a model with matching dimensions
|
||||
|
||||
Supported OpenAI embedding dimensions:
|
||||
- `text-embedding-3-small`: 1536 dimensions
|
||||
- `text-embedding-3-large`: 3072 dimensions
|
||||
- `text-embedding-ada-002`: 1536 dimensions (legacy)
|
||||
:::
|
||||
|
||||
### Reranker
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `HINDSIGHT_API_RERANKER_PROVIDER` | Provider: `local`, `tei`, `cohere`, `flashrank`, `litellm`, or `rrf` | `local` |
|
||||
| `HINDSIGHT_API_RERANKER_LOCAL_MODEL` | Model for local provider | `cross-encoder/ms-marco-MiniLM-L-6-v2` |
|
||||
| `HINDSIGHT_API_RERANKER_LOCAL_MAX_CONCURRENT` | Max concurrent local reranking (prevents CPU thrashing under load) | `4` |
|
||||
| `HINDSIGHT_API_RERANKER_TEI_URL` | TEI server URL | - |
|
||||
| `HINDSIGHT_API_RERANKER_TEI_BATCH_SIZE` | Batch size for TEI reranking | `128` |
|
||||
| `HINDSIGHT_API_RERANKER_TEI_MAX_CONCURRENT` | Max concurrent TEI reranking requests | `8` |
|
||||
| `HINDSIGHT_API_RERANKER_COHERE_MODEL` | Cohere rerank model | `rerank-english-v3.0` |
|
||||
| `HINDSIGHT_API_RERANKER_COHERE_BASE_URL` | Custom base URL for Cohere-compatible API (e.g., Azure-hosted) | - |
|
||||
| `HINDSIGHT_API_RERANKER_LITELLM_MODEL` | LiteLLM rerank model (use provider prefix, e.g., `cohere/rerank-english-v3.0`) | `cohere/rerank-english-v3.0` |
|
||||
| `HINDSIGHT_API_RERANKER_FLASHRANK_MODEL` | FlashRank model for fast CPU-based reranking | `ms-marco-MiniLM-L-12-v2` |
|
||||
| `HINDSIGHT_API_RERANKER_FLASHRANK_CACHE_DIR` | Cache directory for FlashRank models | System default |
|
||||
|
||||
```bash
|
||||
# Local (default) - uses SentenceTransformers CrossEncoder
|
||||
export HINDSIGHT_API_RERANKER_PROVIDER=local
|
||||
export HINDSIGHT_API_RERANKER_LOCAL_MODEL=cross-encoder/ms-marco-MiniLM-L-6-v2
|
||||
|
||||
# TEI - for high-performance inference
|
||||
export HINDSIGHT_API_RERANKER_PROVIDER=tei
|
||||
export HINDSIGHT_API_RERANKER_TEI_URL=http://localhost:8081
|
||||
|
||||
# Cohere - cloud-based reranking
|
||||
export HINDSIGHT_API_RERANKER_PROVIDER=cohere
|
||||
export HINDSIGHT_API_COHERE_API_KEY=your-api-key # shared with embeddings
|
||||
export HINDSIGHT_API_RERANKER_COHERE_MODEL=rerank-english-v3.0
|
||||
|
||||
# Azure-hosted Cohere - reranking via custom endpoint
|
||||
export HINDSIGHT_API_RERANKER_PROVIDER=cohere
|
||||
export HINDSIGHT_API_COHERE_API_KEY=your-azure-api-key
|
||||
export HINDSIGHT_API_RERANKER_COHERE_MODEL=rerank-english-v3.0
|
||||
export HINDSIGHT_API_RERANKER_COHERE_BASE_URL=https://your-azure-cohere-endpoint.com
|
||||
|
||||
# LiteLLM proxy - unified gateway for multiple reranking providers
|
||||
export HINDSIGHT_API_RERANKER_PROVIDER=litellm
|
||||
export HINDSIGHT_API_LITELLM_API_BASE=http://localhost:4000
|
||||
export HINDSIGHT_API_LITELLM_API_KEY=your-litellm-key # optional
|
||||
export HINDSIGHT_API_RERANKER_LITELLM_MODEL=cohere/rerank-english-v3.0 # or voyage/rerank-2, together_ai/...
|
||||
```
|
||||
|
||||
LiteLLM supports multiple reranking providers via the `/rerank` endpoint:
|
||||
- Cohere (`cohere/rerank-english-v3.0`, `cohere/rerank-multilingual-v3.0`)
|
||||
- Together AI (`together_ai/...`)
|
||||
- Voyage AI (`voyage/rerank-2`)
|
||||
- Jina AI (`jina_ai/...`)
|
||||
- AWS Bedrock (`bedrock/...`)
|
||||
|
||||
### Authentication
|
||||
|
||||
By default, Hindsight runs without authentication. For production deployments, enable API key authentication using the built-in tenant extension:
|
||||
|
||||
```bash
|
||||
# Enable the built-in API key authentication
|
||||
export HINDSIGHT_API_TENANT_EXTENSION=hindsight_api.extensions.builtin.tenant:ApiKeyTenantExtension
|
||||
export HINDSIGHT_API_TENANT_API_KEY=your-secret-api-key
|
||||
```
|
||||
|
||||
When enabled, all requests must include the API key in the `Authorization` header:
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer your-secret-api-key" \
|
||||
http://localhost:8888/v1/default/banks
|
||||
```
|
||||
|
||||
Requests without a valid API key receive a `401 Unauthorized` response.
|
||||
|
||||
:::tip Custom Authentication
|
||||
For advanced authentication (JWT, OAuth, multi-tenant schemas), implement a custom `TenantExtension`. See the [Extensions documentation](./extensions.md) for details.
|
||||
:::
|
||||
|
||||
### Server
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `HINDSIGHT_API_HOST` | Bind address | `0.0.0.0` |
|
||||
| `HINDSIGHT_API_PORT` | Server port | `8888` |
|
||||
| `HINDSIGHT_API_WORKERS` | Number of uvicorn worker processes | `1` |
|
||||
| `HINDSIGHT_API_LOG_LEVEL` | Log level: `debug`, `info`, `warning`, `error` | `info` |
|
||||
| `HINDSIGHT_API_LOG_FORMAT` | Log format: `text` or `json` (structured logging for cloud platforms) | `text` |
|
||||
| `HINDSIGHT_API_MCP_ENABLED` | Enable MCP server at `/mcp/{bank_id}/` | `true` |
|
||||
|
||||
### Retrieval
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `HINDSIGHT_API_GRAPH_RETRIEVER` | Graph retrieval algorithm: `link_expansion`, `mpfp`, or `bfs` | `link_expansion` |
|
||||
| `HINDSIGHT_API_RECALL_MAX_CONCURRENT` | Max concurrent recall operations per worker (backpressure) | `32` |
|
||||
| `HINDSIGHT_API_RECALL_CONNECTION_BUDGET` | Max concurrent DB connections per recall operation | `4` |
|
||||
| `HINDSIGHT_API_RERANKER_MAX_CANDIDATES` | Max candidates to rerank per recall (RRF pre-filters the rest) | `300` |
|
||||
| `HINDSIGHT_API_MPFP_TOP_K_NEIGHBORS` | Fan-out limit per node in MPFP graph traversal | `20` |
|
||||
| `HINDSIGHT_API_MENTAL_MODEL_REFRESH_CONCURRENCY` | Max concurrent mental model refreshes | `8` |
|
||||
|
||||
#### Graph Retrieval Algorithms
|
||||
|
||||
- **`link_expansion`** (default): Fast, simple graph expansion from semantic seeds via entity co-occurrence and causal links. Target latency under 100ms. Recommended for most use cases.
|
||||
- **`mpfp`**: Multi-Path Fact Propagation - iterative graph traversal with activation spreading. More thorough but slower.
|
||||
- **`bfs`**: Breadth-first search from seed facts. Simple but less effective for large graphs.
|
||||
|
||||
### Retain
|
||||
|
||||
Controls the retain (memory ingestion) pipeline.
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `HINDSIGHT_API_RETAIN_MAX_COMPLETION_TOKENS` | Max completion tokens for fact extraction LLM calls | `64000` |
|
||||
| `HINDSIGHT_API_RETAIN_CHUNK_SIZE` | Max characters per chunk for fact extraction. Larger chunks extract fewer LLM calls but may lose context. | `3000` |
|
||||
| `HINDSIGHT_API_RETAIN_EXTRACTION_MODE` | Fact extraction mode: `concise`, `verbose`, or `custom` | `concise` |
|
||||
| `HINDSIGHT_API_RETAIN_CUSTOM_INSTRUCTIONS` | Custom extraction guidelines (only used when mode is `custom`) | - |
|
||||
| `HINDSIGHT_API_RETAIN_EXTRACT_CAUSAL_LINKS` | Extract causal relationships between facts | `true` |
|
||||
|
||||
#### Extraction Modes
|
||||
|
||||
The extraction mode controls how aggressively facts are extracted from content:
|
||||
|
||||
- **`concise`** (default): Selective extraction that focuses on significant, long-term valuable facts. Filters out greetings, filler, and trivial information. Produces fewer but higher-quality facts with better performance.
|
||||
|
||||
- **`verbose`**: Detailed extraction that captures every piece of information with maximum verbosity. Produces more facts with extensive detail but slower performance and higher token usage.
|
||||
|
||||
- **`custom`**: Inject your own extraction guidelines while keeping the structural parts of the prompt (output format, coreference resolution, temporal handling, etc.) intact. Useful for A/B testing different extraction strategies or domain-specific customization.
|
||||
|
||||
**Example: Custom Extraction Mode**
|
||||
|
||||
```bash
|
||||
# Set mode to custom
|
||||
export HINDSIGHT_API_RETAIN_EXTRACTION_MODE=custom
|
||||
|
||||
# Define custom guidelines (multi-line is fine)
|
||||
export HINDSIGHT_API_RETAIN_CUSTOM_INSTRUCTIONS="ONLY extract facts that are:
|
||||
✅ Technical decisions and their rationale
|
||||
✅ Architecture patterns and design choices
|
||||
✅ Performance metrics and benchmarks
|
||||
✅ Code reviews and feedback
|
||||
|
||||
DO NOT extract:
|
||||
❌ Generic greetings or pleasantries
|
||||
❌ Process chatter (\"let me check\", \"one moment\")
|
||||
❌ Repeated information already captured
|
||||
|
||||
CONSOLIDATE related technical discussions into ONE fact when possible.
|
||||
|
||||
Ask yourself: 'Would this technical context be useful in 6 months?' If no, skip it."
|
||||
```
|
||||
|
||||
### Observations (Experimental)
|
||||
|
||||
Observations are consolidated knowledge synthesized from facts.
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `HINDSIGHT_API_ENABLE_OBSERVATIONS` | Enable observation consolidation | `true` |
|
||||
| `HINDSIGHT_API_CONSOLIDATION_BATCH_SIZE` | Memories to load per batch (internal optimization) | `50` |
|
||||
| `HINDSIGHT_API_CONSOLIDATION_MAX_TOKENS` | Max tokens for recall when finding related observations during consolidation | `1024` |
|
||||
|
||||
### Reflect
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `HINDSIGHT_API_REFLECT_MAX_ITERATIONS` | Max tool call iterations before forcing a response | `10` |
|
||||
|
||||
### MCP Server
|
||||
|
||||
Configuration for MCP server endpoints.
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `HINDSIGHT_API_MCP_ENABLED` | Enable MCP server at `/mcp/{bank_id}/` | `true` |
|
||||
| `HINDSIGHT_API_MCP_AUTH_TOKEN` | Bearer token for MCP authentication (optional) | - |
|
||||
| `HINDSIGHT_API_MCP_LOCAL_BANK_ID` | Memory bank ID for local MCP | `mcp` |
|
||||
| `HINDSIGHT_API_MCP_INSTRUCTIONS` | Additional instructions appended to retain/recall tool descriptions | - |
|
||||
|
||||
**MCP Authentication:**
|
||||
|
||||
By default, the MCP endpoint is open. For production deployments, set `HINDSIGHT_API_MCP_AUTH_TOKEN` to require Bearer token authentication:
|
||||
|
||||
```bash
|
||||
export HINDSIGHT_API_MCP_AUTH_TOKEN=your-secret-token
|
||||
```
|
||||
|
||||
Clients must then include the token in the `Authorization` header. See [MCP Server documentation](./mcp-server.md#authentication) for details.
|
||||
|
||||
**Local MCP instructions:**
|
||||
|
||||
```bash
|
||||
# Example: instruct MCP to also store assistant actions
|
||||
export HINDSIGHT_API_MCP_INSTRUCTIONS="Also store every action you take, including tool calls and decisions made."
|
||||
```
|
||||
|
||||
### Distributed Workers
|
||||
|
||||
Configuration for background task processing. By default, the API processes tasks internally. For high-throughput deployments, run dedicated workers. See [Services - Worker Service](./services#worker-service) for details.
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `HINDSIGHT_API_WORKER_ENABLED` | Enable internal worker in API process | `true` |
|
||||
| `HINDSIGHT_API_WORKER_ID` | Unique worker identifier | hostname |
|
||||
| `HINDSIGHT_API_WORKER_POLL_INTERVAL_MS` | Database polling interval in milliseconds | `500` |
|
||||
| `HINDSIGHT_API_WORKER_MAX_RETRIES` | Max retries before marking task failed | `3` |
|
||||
| `HINDSIGHT_API_WORKER_HTTP_PORT` | HTTP port for worker metrics/health (worker CLI only) | `8889` |
|
||||
| `HINDSIGHT_API_WORKER_MAX_SLOTS` | Maximum concurrent tasks per worker | `10` |
|
||||
| `HINDSIGHT_API_WORKER_CONSOLIDATION_MAX_SLOTS` | Maximum concurrent consolidation tasks per worker | `2` |
|
||||
|
||||
### Performance Optimization
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `HINDSIGHT_API_SKIP_LLM_VERIFICATION` | Skip LLM connection check on startup | `false` |
|
||||
| `HINDSIGHT_API_LAZY_RERANKER` | Lazy-load reranker model (faster startup) | `false` |
|
||||
|
||||
### Programmatic Configuration
|
||||
|
||||
You can also configure the API programmatically using `MemoryEngine.from_env()`:
|
||||
|
||||
```python
|
||||
from hindsight_api import MemoryEngine
|
||||
|
||||
memory = MemoryEngine.from_env()
|
||||
await memory.initialize()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Control Plane
|
||||
|
||||
The Control Plane is the web UI for managing memory banks.
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `HINDSIGHT_CP_DATAPLANE_API_URL` | URL of the API service | `http://localhost:8888` |
|
||||
|
||||
```bash
|
||||
# Point Control Plane to a remote API service
|
||||
export HINDSIGHT_CP_DATAPLANE_API_URL=http://api.example.com:8888
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example .env File
|
||||
|
||||
```bash
|
||||
# API Service
|
||||
HINDSIGHT_API_DATABASE_URL=postgresql://hindsight:hindsight_dev@localhost:5432/hindsight
|
||||
# HINDSIGHT_API_DATABASE_SCHEMA=public # optional, defaults to 'public'
|
||||
HINDSIGHT_API_LLM_PROVIDER=groq
|
||||
HINDSIGHT_API_LLM_API_KEY=gsk_xxxxxxxxxxxx
|
||||
|
||||
# Authentication (optional, recommended for production)
|
||||
# HINDSIGHT_API_TENANT_EXTENSION=hindsight_api.extensions.builtin.tenant:ApiKeyTenantExtension
|
||||
# HINDSIGHT_API_TENANT_API_KEY=your-secret-api-key
|
||||
|
||||
# Control Plane
|
||||
HINDSIGHT_CP_DATAPLANE_API_URL=http://localhost:8888
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
For configuration issues not covered here, please [open an issue](https://github.com/vectorize-io/hindsight/issues) on GitHub.
|
||||
@@ -0,0 +1,149 @@
|
||||
---
|
||||
sidebar_position: 7
|
||||
---
|
||||
|
||||
# Development Guide
|
||||
|
||||
Guide to setting up a local development environment for contributing to Hindsight.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python 3.11+
|
||||
- [uv](https://docs.astral.sh/uv/) - Fast Python package manager
|
||||
- Docker and Docker Compose
|
||||
- An LLM API key (OpenAI, Groq, or Ollama)
|
||||
|
||||
## Local Development Setup
|
||||
|
||||
### 1. Clone the Repository
|
||||
|
||||
```bash
|
||||
git clone https://github.com/vectorize-io/hindsight.git
|
||||
cd hindsight
|
||||
```
|
||||
|
||||
### 2. Install Dependencies
|
||||
|
||||
```bash
|
||||
uv sync
|
||||
```
|
||||
|
||||
### 3. Start PostgreSQL
|
||||
|
||||
Start only the database via Docker:
|
||||
|
||||
```bash
|
||||
cd docker && docker-compose up -d postgres
|
||||
```
|
||||
|
||||
### 4. Configure Environment
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Edit `.env` with your LLM API key:
|
||||
|
||||
```bash
|
||||
# Database (connects to Docker postgres)
|
||||
HINDSIGHT_API_DATABASE_URL=postgresql://hindsight:hindsight_dev@localhost:5432/hindsight
|
||||
|
||||
# LLM Provider (choose one)
|
||||
HINDSIGHT_API_LLM_PROVIDER=groq
|
||||
HINDSIGHT_API_LLM_API_KEY=gsk_xxxxxxxxxxxx
|
||||
HINDSIGHT_API_LLM_MODEL=llama-3.1-70b-versatile
|
||||
```
|
||||
|
||||
### 5. Start the API Server
|
||||
|
||||
```bash
|
||||
./scripts/start-server.sh --env local
|
||||
```
|
||||
|
||||
The server will be available at http://localhost:8888.
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
uv run pytest
|
||||
|
||||
# Run specific test file
|
||||
uv run pytest tests/test_retrieval.py
|
||||
|
||||
# Run with verbose output
|
||||
uv run pytest -v
|
||||
```
|
||||
|
||||
## Code Generation
|
||||
|
||||
### Regenerate API Clients
|
||||
|
||||
When you modify the OpenAPI spec, regenerate the clients:
|
||||
|
||||
```bash
|
||||
./scripts/generate-clients.sh
|
||||
```
|
||||
|
||||
This generates:
|
||||
- Python client in `hindsight-clients/python/`
|
||||
- TypeScript client in `hindsight-clients/typescript/`
|
||||
|
||||
### Export OpenAPI Schema
|
||||
|
||||
```bash
|
||||
./scripts/export-openapi.sh
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
hindsight/
|
||||
├── hindsight-api/ # Main API server
|
||||
│ ├── hindsight_api/
|
||||
│ │ ├── api/ # HTTP endpoints
|
||||
│ │ ├── engine/ # Memory engine, retrieval, reasoning
|
||||
│ │ └── web/ # Server entry point
|
||||
│ └── tests/
|
||||
├── hindsight-clients/ # Generated SDK clients
|
||||
│ ├── python/
|
||||
│ └── typescript/
|
||||
├── hindsight-control-plane/ # Admin UI (Next.js)
|
||||
├── docker/ # Docker Compose setup
|
||||
└── scripts/ # Development scripts
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Create a feature branch from `main`
|
||||
2. Make your changes
|
||||
3. Run tests: `uv run pytest`
|
||||
4. Submit a pull request
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Database Connection Issues
|
||||
|
||||
Ensure PostgreSQL is running:
|
||||
|
||||
```bash
|
||||
docker-compose ps
|
||||
```
|
||||
|
||||
Check database connectivity:
|
||||
|
||||
```bash
|
||||
psql postgresql://hindsight:hindsight_dev@localhost:5432/hindsight
|
||||
```
|
||||
|
||||
### ML Model Download
|
||||
|
||||
On first run, Hindsight downloads embedding and reranking models. This may take a few minutes. Models are cached in `~/.cache/huggingface/`.
|
||||
|
||||
### Port Conflicts
|
||||
|
||||
If port 8888 is in use:
|
||||
|
||||
```bash
|
||||
HINDSIGHT_API_PORT=8889 ./scripts/start-server.sh --env local
|
||||
```
|
||||
@@ -0,0 +1,269 @@
|
||||
# Extensions
|
||||
|
||||
Extensions allow you to customize and extend Hindsight behavior without modifying core code. They enable multi-tenancy, custom authentication, additional HTTP endpoints, and operation hooks.
|
||||
|
||||
---
|
||||
|
||||
## Available Extensions
|
||||
|
||||
### TenantExtension
|
||||
|
||||
Handles multi-tenancy and API key authentication. Validates incoming requests and determines which PostgreSQL schema to use for database operations, enabling tenant isolation at the database level.
|
||||
|
||||
**Built-in: ApiKeyTenantExtension**
|
||||
|
||||
A simple implementation that validates API keys against an environment variable and uses the `public` schema for all authenticated requests.
|
||||
|
||||
```bash
|
||||
HINDSIGHT_API_TENANT_EXTENSION=hindsight_api.extensions.builtin.tenant:ApiKeyTenantExtension
|
||||
HINDSIGHT_API_TENANT_API_KEY=your-secret-key
|
||||
```
|
||||
|
||||
**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`.
|
||||
|
||||
---
|
||||
|
||||
### HttpExtension
|
||||
|
||||
Adds custom HTTP endpoints under the `/ext/` path prefix. Useful for adding domain-specific APIs that integrate with Hindsight's memory engine.
|
||||
|
||||
**No built-in implementation** - implement your own to add custom endpoints.
|
||||
|
||||
```bash
|
||||
HINDSIGHT_API_HTTP_EXTENSION=mypackage.ext:MyHttpExtension
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### OperationValidatorExtension
|
||||
|
||||
Hooks into retain/recall/reflect operations for validation and monitoring. Use cases include:
|
||||
- Rate limiting and quota enforcement
|
||||
- Permission checks and content filtering
|
||||
- Audit logging and usage tracking
|
||||
- Custom metrics collection
|
||||
|
||||
**No built-in implementation** - implement your own based on your requirements.
|
||||
|
||||
```bash
|
||||
HINDSIGHT_API_OPERATION_VALIDATOR_EXTENSION=mypackage.validators:MyValidator
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### MCPExtension
|
||||
|
||||
Registers additional MCP (Model Context Protocol) tools on the Hindsight MCP server. Enables external packages to add custom tools without modifying core code.
|
||||
|
||||
**No built-in implementation** - implement your own to add custom MCP tools.
|
||||
|
||||
```bash
|
||||
HINDSIGHT_API_MCP_EXTENSION=mypackage.mcp:MyMCPExtension
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Writing Custom Extensions
|
||||
|
||||
### Extension Basics
|
||||
|
||||
Extensions are Python classes loaded via environment variables:
|
||||
|
||||
```bash
|
||||
HINDSIGHT_API_<TYPE>_EXTENSION=mypackage.module:MyExtensionClass
|
||||
```
|
||||
|
||||
Configuration is passed via prefixed environment variables:
|
||||
|
||||
```bash
|
||||
HINDSIGHT_API_<TYPE>_SOME_CONFIG=value
|
||||
# Extension receives: {"some_config": "value"}
|
||||
```
|
||||
|
||||
All extensions support lifecycle hooks:
|
||||
- `on_startup()` - Called when the application starts
|
||||
- `on_shutdown()` - Called when the application shuts down
|
||||
|
||||
Extensions have access to an `ExtensionContext` that provides:
|
||||
- `run_migration(schema)` - Run database migrations for a schema
|
||||
- `get_memory_engine()` - Get the MemoryEngine interface
|
||||
|
||||
### Example: Custom TenantExtension with JWT
|
||||
|
||||
```python
|
||||
import jwt
|
||||
from hindsight_api.extensions import TenantExtension, TenantContext, AuthenticationError
|
||||
|
||||
class JwtTenantExtension(TenantExtension):
|
||||
def __init__(self, config: dict[str, str]):
|
||||
super().__init__(config)
|
||||
self.jwt_secret = config.get("jwt_secret")
|
||||
if not self.jwt_secret:
|
||||
raise ValueError("HINDSIGHT_API_TENANT_JWT_SECRET is required")
|
||||
|
||||
async def authenticate(self, context: RequestContext) -> TenantContext:
|
||||
token = context.api_key
|
||||
if not token:
|
||||
raise AuthenticationError("Bearer token required")
|
||||
|
||||
try:
|
||||
payload = jwt.decode(token, self.jwt_secret, algorithms=["HS256"])
|
||||
tenant_id = payload.get("tenant_id")
|
||||
if not tenant_id:
|
||||
raise AuthenticationError("Missing tenant_id in token")
|
||||
return TenantContext(schema_name=f"tenant_{tenant_id}")
|
||||
except jwt.InvalidTokenError as e:
|
||||
raise AuthenticationError(str(e))
|
||||
```
|
||||
|
||||
### Example: Custom HttpExtension
|
||||
|
||||
```python
|
||||
from fastapi import APIRouter
|
||||
from hindsight_api.extensions import HttpExtension
|
||||
|
||||
class MyHttpExtension(HttpExtension):
|
||||
def get_router(self, memory: MemoryEngine) -> APIRouter:
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/hello")
|
||||
async def hello():
|
||||
return {"message": "Hello from extension!"}
|
||||
|
||||
@router.post("/custom/{bank_id}/action")
|
||||
async def custom_action(bank_id: str):
|
||||
# Access memory engine for database operations
|
||||
pool = await memory._get_pool()
|
||||
# ... custom logic
|
||||
return {"status": "ok"}
|
||||
|
||||
return router
|
||||
```
|
||||
|
||||
Routes are available at `/ext/hello`, `/ext/custom/{bank_id}/action`, etc.
|
||||
|
||||
### Example: Custom OperationValidatorExtension
|
||||
|
||||
```python
|
||||
from hindsight_api.extensions import (
|
||||
OperationValidatorExtension,
|
||||
ValidationResult,
|
||||
RetainContext,
|
||||
RecallContext,
|
||||
ReflectContext,
|
||||
RetainResult,
|
||||
)
|
||||
|
||||
class MyValidator(OperationValidatorExtension):
|
||||
# Pre-operation validation (required)
|
||||
async def validate_retain(self, ctx: RetainContext) -> ValidationResult:
|
||||
# Implement your validation logic
|
||||
return ValidationResult.accept()
|
||||
# Or reject: return ValidationResult.reject("Reason")
|
||||
|
||||
async def validate_recall(self, ctx: RecallContext) -> ValidationResult:
|
||||
return ValidationResult.accept()
|
||||
|
||||
async def validate_reflect(self, ctx: ReflectContext) -> ValidationResult:
|
||||
return ValidationResult.accept()
|
||||
|
||||
# Post-operation hooks (optional)
|
||||
async def on_retain_complete(self, result: RetainResult) -> None:
|
||||
# Log usage, update metrics, send notifications, etc.
|
||||
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
|
||||
|
||||
### With Docker
|
||||
|
||||
Mount your extension package as a volume and set the environment variable:
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
services:
|
||||
hindsight-api:
|
||||
image: vectorize/hindsight-api:latest
|
||||
volumes:
|
||||
- ./my_extensions:/app/my_extensions
|
||||
environment:
|
||||
- HINDSIGHT_API_TENANT_EXTENSION=my_extensions.auth:JwtTenantExtension
|
||||
- HINDSIGHT_API_TENANT_JWT_SECRET=${JWT_SECRET}
|
||||
- PYTHONPATH=/app
|
||||
```
|
||||
|
||||
Or build a custom image with your extensions:
|
||||
|
||||
```dockerfile
|
||||
FROM vectorize/hindsight-api:latest
|
||||
COPY my_extensions /app/my_extensions
|
||||
ENV PYTHONPATH=/app
|
||||
```
|
||||
|
||||
### Bare Metal
|
||||
|
||||
Install your extension package in the same Python environment as Hindsight:
|
||||
|
||||
```bash
|
||||
# Install Hindsight
|
||||
pip install hindsight-api
|
||||
|
||||
# Install your extension package
|
||||
pip install ./my-extensions
|
||||
# or
|
||||
pip install my-extensions-package
|
||||
|
||||
# Configure
|
||||
export HINDSIGHT_API_TENANT_EXTENSION=my_extensions.auth:JwtTenantExtension
|
||||
export HINDSIGHT_API_TENANT_JWT_SECRET=your-secret
|
||||
|
||||
# Run
|
||||
hindsight-api
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Contributing Extensions
|
||||
|
||||
Custom extensions that solve common use cases are welcome contributions to the Hindsight project. If you've built an extension for:
|
||||
|
||||
- Authentication providers (OAuth, SAML, API gateways)
|
||||
- Rate limiting or quota management
|
||||
- Audit logging integrations
|
||||
- Metrics exporters (Datadog, New Relic, etc.)
|
||||
- Custom HTTP endpoints for specific platforms
|
||||
|
||||
Consider contributing it to the `hindsight_api.extensions.builtin` package. Open an issue or pull request on [GitHub](https://github.com/vectorize-io/hindsight) to discuss your extension.
|
||||
@@ -0,0 +1,138 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
slug: /
|
||||
---
|
||||
|
||||
# Overview
|
||||
|
||||
## Why Hindsight?
|
||||
|
||||
AI agents forget everything between sessions. Every conversation starts from zero—no context about who you are, what you've discussed, or what the assistant has learned. This isn't just an implementation detail; it fundamentally limits what AI Agents can do.
|
||||
|
||||
**The problem is harder than it looks:**
|
||||
|
||||
- **Simple vector search isn't enough** — "What did Alice do last spring?" requires temporal reasoning, not just semantic similarity
|
||||
- **Facts get disconnected** — Knowing "Alice works at Google" and "Google is in Mountain View" should let you answer "Where does Alice work?" even if you never stored that directly
|
||||
- **AI Agents need to consolidate knowledge** — A coding assistant that remembers "the user prefers functional programming" should consolidate this into an observation and weigh it when making recommendations
|
||||
- **Context matters** — The same information means different things to different memory banks with different personalities
|
||||
|
||||
Hindsight solves these problems with a memory system designed specifically for AI agents.
|
||||
|
||||
## What Hindsight Does
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
subgraph app["<b>Your Application</b>"]
|
||||
Agent[AI Agent]
|
||||
end
|
||||
|
||||
subgraph hindsight["<b>Hindsight</b>"]
|
||||
API[API Server]
|
||||
|
||||
subgraph bank["<b>Memory Bank</b>"]
|
||||
direction TB
|
||||
MentalModels[Mental Models]
|
||||
Observations[Observations]
|
||||
MemEnt[Memories & Entities]
|
||||
Chunks[Chunks]
|
||||
Documents[Documents]
|
||||
|
||||
MentalModels --> Observations --> MemEnt --> Chunks --> Documents
|
||||
end
|
||||
end
|
||||
|
||||
Agent -->|retain| API
|
||||
Agent -->|recall| API
|
||||
Agent -->|reflect| API
|
||||
|
||||
API --> bank
|
||||
```
|
||||
|
||||
**Your AI agent** stores information via `retain()`, searches with `recall()`, and reasons with `reflect()` — all interactions with its dedicated **memory bank**
|
||||
|
||||
## Key Components
|
||||
|
||||
### Memory Types
|
||||
|
||||
Hindsight organizes knowledge into a hierarchy of facts and consolidated knowledge:
|
||||
|
||||
| Type | What it stores | Example |
|
||||
|------|----------------|---------|
|
||||
| **Mental Model** | User-curated summaries for common queries | "Team communication best practices" |
|
||||
| **Observation** | Automatically consolidated knowledge from facts | "User was a React enthusiast but has now switched to Vue" (captures history) |
|
||||
| **World Fact** | Objective facts received | "Alice works at Google" |
|
||||
| **Experience Fact** | Bank's own actions and interactions | "I recommended Python to Bob" |
|
||||
|
||||
During reflect, the agent checks sources in priority order: **Mental Models → Observations → Raw Facts**.
|
||||
|
||||
### Multi-Strategy Retrieval (TEMPR)
|
||||
|
||||
Four search strategies run in parallel:
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
Q[Query] --> S[Semantic]
|
||||
Q --> K[Keyword]
|
||||
Q --> G[Graph]
|
||||
Q --> T[Temporal]
|
||||
|
||||
S --> RRF[RRF Fusion]
|
||||
K --> RRF
|
||||
G --> RRF
|
||||
T --> RRF
|
||||
|
||||
RRF --> CE[Cross-Encoder]
|
||||
CE --> R[Results]
|
||||
```
|
||||
|
||||
| Strategy | Best for |
|
||||
|----------|----------|
|
||||
| **Semantic** | Conceptual similarity, paraphrasing |
|
||||
| **Keyword (BM25)** | Names, technical terms, exact matches |
|
||||
| **Graph** | Related entities, indirect connections |
|
||||
| **Temporal** | "last spring", "in June", time ranges |
|
||||
|
||||
### Observation Consolidation
|
||||
|
||||
After memories are retained, Hindsight automatically consolidates related facts into **observations** — synthesized knowledge representations that capture patterns and learnings:
|
||||
|
||||
- **Automatic synthesis**: New facts are analyzed and consolidated into existing or new observations
|
||||
- **Evidence tracking**: Each observation tracks which facts support it
|
||||
- **Continuous refinement**: Observations evolve as new evidence arrives
|
||||
|
||||
### Mission, Directives & Disposition
|
||||
|
||||
Memory banks can be configured to shape how the agent reasons during `reflect`:
|
||||
|
||||
| Configuration | Purpose | Example |
|
||||
|---------------|---------|---------|
|
||||
| **Mission** | Natural language identity for the bank | "I am a research assistant specializing in ML. I prefer simplicity over cutting-edge." |
|
||||
| **Directives** | Hard rules the agent must follow | "Never recommend specific stocks", "Always cite sources" |
|
||||
| **Disposition** | Soft traits that influence reasoning style | Skepticism, literalism, empathy (1-5 scale) |
|
||||
|
||||
The **mission** tells Hindsight what knowledge to prioritize and provides context for reasoning. **Directives** are guardrails and compliance rules that must never be violated. **Disposition traits** subtly influence interpretation style.
|
||||
|
||||
These settings only affect the `reflect` operation, not `recall`.
|
||||
|
||||
## Next Steps
|
||||
|
||||
### Getting Started
|
||||
- [**Quick Start**](/developer/api/quickstart) — Install and get up and running in 60 seconds
|
||||
- [**RAG vs Hindsight**](/developer/rag-vs-hindsight) — See how Hindsight differs from traditional RAG with real examples
|
||||
|
||||
### Core Concepts
|
||||
- [**Retain**](/developer/retain) — How memories are stored with multi-dimensional facts
|
||||
- [**Recall**](/developer/retrieval) — How TEMPR's 4-way search retrieves memories
|
||||
- [**Reflect**](/developer/reflect) — How mission, directives, and disposition shape reasoning
|
||||
|
||||
### API Methods
|
||||
- [**Retain**](/developer/api/retain) — Store information in memory banks
|
||||
- [**Recall**](/developer/api/recall) — Search and retrieve memories
|
||||
- [**Reflect**](/developer/api/reflect) — Agentic reasoning with memory
|
||||
- [**Mental Models**](/developer/api/mental-models) — User-curated summaries for common queries
|
||||
- [**Memory Banks**](/developer/api/memory-banks) — Configure mission, directives, and disposition
|
||||
- [**Documents**](/developer/api/documents) — Manage document sources
|
||||
- [**Operations**](/developer/api/operations) — Monitor async tasks
|
||||
|
||||
### Deployment
|
||||
- [**Server Setup**](/developer/installation) — Deploy with Docker Compose, Helm, or pip
|
||||
@@ -0,0 +1,259 @@
|
||||
# Installation
|
||||
|
||||
Hindsight can be deployed in several ways depending on your infrastructure and requirements.
|
||||
|
||||
:::tip Don't want to manage infrastructure?
|
||||
**[Hindsight Cloud](https://vectorize.io/hindsight/cloud)** is a fully managed service that handles all infrastructure, scaling, and maintenance. We're onboarding design partners now — [request early access](https://vectorize.io/hindsight/cloud).
|
||||
:::
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### PostgreSQL with pgvector
|
||||
|
||||
Hindsight requires PostgreSQL with the **pgvector** extension for vector similarity search.
|
||||
|
||||
**By default**, Hindsight uses **pg0** — an embedded PostgreSQL that runs locally on your machine. This is convenient for development but **not recommended for production**.
|
||||
|
||||
**For production**, use an external PostgreSQL with pgvector:
|
||||
- **Supabase** — Managed PostgreSQL with pgvector built-in
|
||||
- **Neon** — Serverless PostgreSQL with pgvector
|
||||
- **AWS RDS** / **Cloud SQL** / **Azure** — With pgvector extension enabled
|
||||
- **Self-hosted** — PostgreSQL 14+ with pgvector installed
|
||||
|
||||
### LLM Provider
|
||||
|
||||
You need an LLM API key for fact extraction, entity resolution, and answer generation:
|
||||
|
||||
- **Groq** (recommended): Fast inference with `gpt-oss-20b`
|
||||
- **OpenAI**: GPT-4o, GPT-4o-mini
|
||||
- **Ollama**: Run models locally
|
||||
|
||||
See [Models](./models) for detailed comparison and configuration.
|
||||
|
||||
---
|
||||
|
||||
## Docker
|
||||
|
||||
**Best for**: Quick start, development, small deployments
|
||||
|
||||
Run everything in one container with embedded PostgreSQL:
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY=sk-xxx
|
||||
|
||||
docker run --rm -it --pull always -p 8888:8888 -p 9999:9999 \
|
||||
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
|
||||
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
|
||||
ghcr.io/vectorize-io/hindsight:latest
|
||||
```
|
||||
|
||||
- **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
|
||||
|
||||
**Best for**: Production deployments, auto-scaling, cloud environments
|
||||
|
||||
```bash
|
||||
# Install with built-in PostgreSQL
|
||||
helm install hindsight oci://ghcr.io/vectorize-io/charts/hindsight \
|
||||
--set api.llm.provider=groq \
|
||||
--set api.llm.apiKey=gsk_xxxxxxxxxxxx \
|
||||
--set postgresql.enabled=true
|
||||
|
||||
# Or use external PostgreSQL
|
||||
helm install hindsight oci://ghcr.io/vectorize-io/charts/hindsight \
|
||||
--set api.llm.provider=groq \
|
||||
--set api.llm.apiKey=gsk_xxxxxxxxxxxx \
|
||||
--set postgresql.enabled=false \
|
||||
--set api.database.url=postgresql://user:[email protected]:5432/hindsight
|
||||
|
||||
# Install a specific version
|
||||
helm install hindsight oci://ghcr.io/vectorize-io/charts/hindsight --version 0.1.3
|
||||
|
||||
# Upgrade to latest
|
||||
helm upgrade hindsight oci://ghcr.io/vectorize-io/charts/hindsight
|
||||
```
|
||||
|
||||
**Requirements**:
|
||||
- Kubernetes cluster (GKE, EKS, AKS, or self-hosted)
|
||||
- Helm 3.8+
|
||||
|
||||
### Distributed Workers
|
||||
|
||||
For high-throughput deployments, enable dedicated worker pods to scale task processing independently:
|
||||
|
||||
```bash
|
||||
helm install hindsight oci://ghcr.io/vectorize-io/charts/hindsight \
|
||||
--set worker.enabled=true \
|
||||
--set worker.replicaCount=3
|
||||
```
|
||||
|
||||
See [Services - Worker Service](./services#worker-service) for configuration details and architecture.
|
||||
|
||||
See the [Helm chart values.yaml](https://github.com/vectorize-io/hindsight/tree/main/helm/hindsight/values.yaml) for all chart options.
|
||||
|
||||
---
|
||||
|
||||
## Bare Metal (pip)
|
||||
|
||||
**Best for**: Custom deployments, integration into existing Python applications
|
||||
|
||||
### Install
|
||||
|
||||
```bash
|
||||
pip install hindsight-all
|
||||
```
|
||||
|
||||
### Run with Embedded Database
|
||||
|
||||
For development and testing, Hindsight can run with an embedded PostgreSQL (pg0):
|
||||
|
||||
```bash
|
||||
export HINDSIGHT_API_LLM_PROVIDER=groq
|
||||
export HINDSIGHT_API_LLM_API_KEY=gsk_xxxxxxxxxxxx
|
||||
|
||||
hindsight-api
|
||||
```
|
||||
|
||||
This creates a database in `~/.hindsight/data/` and starts the API on http://localhost:8888.
|
||||
|
||||
### Run with External PostgreSQL
|
||||
|
||||
For production, connect to your own PostgreSQL instance:
|
||||
|
||||
```bash
|
||||
export HINDSIGHT_API_DATABASE_URL=postgresql://user:pass@localhost:5432/hindsight
|
||||
export HINDSIGHT_API_LLM_PROVIDER=groq
|
||||
export HINDSIGHT_API_LLM_API_KEY=gsk_xxxxxxxxxxxx
|
||||
|
||||
hindsight-api
|
||||
```
|
||||
|
||||
**Note**: The database must exist and have pgvector enabled (`CREATE EXTENSION vector;`).
|
||||
|
||||
### CLI Options
|
||||
|
||||
```bash
|
||||
hindsight-api --port 9000 # Custom port (default: 8888)
|
||||
hindsight-api --host 127.0.0.1 # Bind to localhost only
|
||||
hindsight-api --workers 4 # Multiple worker processes
|
||||
hindsight-api --log-level debug # Verbose logging
|
||||
```
|
||||
|
||||
### Control Plane
|
||||
|
||||
The Control Plane (Web UI) can be run standalone using npx:
|
||||
|
||||
```bash
|
||||
npx @vectorize-io/hindsight-control-plane --api-url http://localhost:8888
|
||||
```
|
||||
|
||||
This connects to your running API server and provides a visual interface for managing memory banks, exploring entities, and testing queries.
|
||||
|
||||
#### Options
|
||||
|
||||
| Option | Environment Variable | Default | Description |
|
||||
|--------|---------------------|---------|-------------|
|
||||
| `-p, --port` | `PORT` | 9999 | Port to listen on |
|
||||
| `-H, --hostname` | `HOSTNAME` | 0.0.0.0 | Hostname to bind to |
|
||||
| `-a, --api-url` | `HINDSIGHT_CP_DATAPLANE_API_URL` | http://localhost:8888 | Hindsight API URL |
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
# Run on custom port
|
||||
npx @vectorize-io/hindsight-control-plane --port 9999 --api-url http://localhost:8888
|
||||
|
||||
# Using environment variables
|
||||
export HINDSIGHT_CP_DATAPLANE_API_URL=http://api.example.com
|
||||
npx @vectorize-io/hindsight-control-plane
|
||||
|
||||
# Production deployment
|
||||
PORT=80 HINDSIGHT_CP_DATAPLANE_API_URL=https://api.hindsight.io npx @vectorize-io/hindsight-control-plane
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Configuration](./configuration.md) — Environment variables and settings
|
||||
- [Models](./models.md) — ML models and providers
|
||||
- [Monitoring](./monitoring.md) — Metrics and observability
|
||||
@@ -0,0 +1,204 @@
|
||||
---
|
||||
sidebar_position: 5
|
||||
---
|
||||
|
||||
# MCP Server
|
||||
|
||||
Hindsight includes a built-in [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that allows AI assistants to store and retrieve memories directly.
|
||||
|
||||
## Access
|
||||
|
||||
The MCP server is **enabled by default** and mounted at `/mcp` on the API server. Each memory bank has its own MCP endpoint:
|
||||
|
||||
```
|
||||
http://localhost:8888/mcp/{bank_id}/
|
||||
```
|
||||
|
||||
For example, to connect to the memory bank `alice`:
|
||||
```
|
||||
http://localhost:8888/mcp/alice/
|
||||
```
|
||||
|
||||
To disable the MCP server, set the environment variable:
|
||||
|
||||
```bash
|
||||
export HINDSIGHT_API_MCP_ENABLED=false
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
By default, the MCP endpoint is **open** (no authentication required).
|
||||
|
||||
To enable authentication, configure the API key tenant extension:
|
||||
|
||||
```bash
|
||||
export HINDSIGHT_API_TENANT_EXTENSION=hindsight_api.extensions.builtin.tenant:ApiKeyTenantExtension
|
||||
export HINDSIGHT_API_TENANT_API_KEY=your-secret-key
|
||||
```
|
||||
|
||||
When authentication is enabled, include your API key in the `Authorization` header:
|
||||
|
||||
### Claude Code
|
||||
|
||||
```bash
|
||||
claude mcp add --transport http hindsight http://localhost:8888/mcp \
|
||||
--header "Authorization: Bearer your-secret-key" \
|
||||
--header "X-Bank-Id: my-bank"
|
||||
```
|
||||
|
||||
### Claude Desktop
|
||||
|
||||
Add to `~/.claude_desktop_config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"hindsight": {
|
||||
"url": "http://localhost:8888/mcp",
|
||||
"headers": {
|
||||
"Authorization": "Bearer your-secret-key",
|
||||
"X-Bank-Id": "my-bank"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Direct HTTP Request
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8888/mcp \
|
||||
-H "Authorization: Bearer your-secret-key" \
|
||||
-H "X-Bank-Id: my-bank" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"jsonrpc": "2.0", "method": "tools/list", "id": 1}'
|
||||
```
|
||||
|
||||
If the key is missing or invalid, requests will receive a `401 Unauthorized` response.
|
||||
|
||||
## Bank Selection
|
||||
|
||||
Specify the memory bank via:
|
||||
|
||||
1. **X-Bank-Id header** (recommended): `--header "X-Bank-Id: my-bank"`
|
||||
2. **URL path**: `http://localhost:8888/mcp/my-bank/`
|
||||
3. **Default**: Uses `HINDSIGHT_MCP_BANK_ID` env var (default: "default")
|
||||
|
||||
## Per-Bank Endpoints
|
||||
|
||||
Unlike traditional MCP servers where tools require explicit identifiers, Hindsight uses **per-bank endpoints**. The `bank_id` is part of the URL path, so tools don't need to specify which bank to use—it's implicit from the connection.
|
||||
|
||||
This design:
|
||||
- **Simplifies tool usage** — no need to pass `bank_id` with every call
|
||||
- **Enforces isolation** — each MCP connection is scoped to a single bank
|
||||
- **Enables multi-tenant setups** — connect different users to different endpoints
|
||||
|
||||
---
|
||||
|
||||
## Available Tools
|
||||
|
||||
### retain
|
||||
|
||||
Store information to long-term memory.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `content` | string | Yes | The fact or memory to store |
|
||||
| `context` | string | No | Category for the memory (default: `general`) |
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"name": "retain",
|
||||
"arguments": {
|
||||
"content": "User prefers Python over JavaScript for backend development",
|
||||
"context": "programming_preferences"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**When to use:**
|
||||
- User shares personal facts, preferences, or interests
|
||||
- Important events or milestones are mentioned
|
||||
- Decisions, opinions, or goals are stated
|
||||
- Work context or project details are discussed
|
||||
|
||||
---
|
||||
|
||||
### recall
|
||||
|
||||
Search memories to provide personalized responses.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `query` | string | Yes | Natural language search query |
|
||||
| `max_results` | integer | No | Maximum results to return (default: 10) |
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"name": "recall",
|
||||
"arguments": {
|
||||
"query": "What are the user's programming language preferences?"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"id": "fact_abc123",
|
||||
"text": "User prefers Python over JavaScript for backend development",
|
||||
"type": "world",
|
||||
"context": "programming_preferences",
|
||||
"event_date": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**When to use:**
|
||||
- Start of conversation to recall relevant context
|
||||
- Before making recommendations
|
||||
- When user asks about something they may have mentioned before
|
||||
- To provide continuity across conversations
|
||||
|
||||
---
|
||||
|
||||
### reflect
|
||||
|
||||
Generate thoughtful analysis by synthesizing stored memories with the bank's personality.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `query` | string | Yes | The question or topic to reflect on |
|
||||
| `context` | string | No | Optional context about why this reflection is needed |
|
||||
| `budget` | string | No | Search budget: `low`, `mid`, or `high` (default: `low`) |
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"name": "reflect",
|
||||
"arguments": {
|
||||
"query": "Based on my past decisions, what architectural style do I prefer?",
|
||||
"budget": "mid"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**When to use:**
|
||||
- When reasoned analysis is needed, not just fact retrieval
|
||||
- Questions like "What should I do?" rather than "What did I say?"
|
||||
- Synthesizing patterns across multiple memories
|
||||
|
||||
---
|
||||
|
||||
## Integration with AI Assistants
|
||||
|
||||
The MCP server can be used with any MCP-compatible AI assistant. See the [Authentication](#authentication) section above for Claude Code and Claude Desktop configuration examples.
|
||||
|
||||
Each user can have their own configuration pointing to their personal memory bank using either:
|
||||
- The `X-Bank-Id` header (recommended)
|
||||
- A bank-specific URL path like `/mcp/alice/`
|
||||
@@ -0,0 +1,418 @@
|
||||
# Models
|
||||
|
||||
Hindsight uses several machine learning models for different tasks.
|
||||
|
||||
## Overview
|
||||
|
||||
| Model Type | Purpose | Default | Configurable |
|
||||
|------------|---------|---------|--------------|
|
||||
| **LLM** | Fact extraction, reasoning, generation | Provider-specific | Yes |
|
||||
| **Embedding** | Vector representations for semantic search | `BAAI/bge-small-en-v1.5` | Yes |
|
||||
| **Cross-Encoder** | Reranking search results | `cross-encoder/ms-marco-MiniLM-L-6-v2` | Yes |
|
||||
|
||||
All local models (embedding, cross-encoder) are automatically downloaded from HuggingFace on first run.
|
||||
|
||||
---
|
||||
|
||||
## LLM
|
||||
|
||||
Used for fact extraction, entity resolution, mental model consolidation, and answer synthesis.
|
||||
|
||||
**Supported providers:** OpenAI, Anthropic, Gemini, Groq, Ollama, LM Studio, and **any OpenAI-compatible API**
|
||||
|
||||
:::tip OpenAI-Compatible Providers
|
||||
Hindsight works with any provider that exposes an OpenAI-compatible API (e.g., Azure OpenAI). Simply set `HINDSIGHT_API_LLM_PROVIDER=openai` and configure `HINDSIGHT_API_LLM_BASE_URL` to point to your provider's endpoint.
|
||||
|
||||
See [Configuration](./configuration#llm-provider) for setup examples.
|
||||
:::
|
||||
|
||||
### Tested Models
|
||||
|
||||
The following models have been tested and verified to work correctly with Hindsight:
|
||||
|
||||
| Provider | Model |
|
||||
|----------|-------|
|
||||
| **OpenAI** | `gpt-5.2` |
|
||||
| **OpenAI** | `gpt-5` |
|
||||
| **OpenAI** | `gpt-5-mini` |
|
||||
| **OpenAI** | `gpt-5-nano` |
|
||||
| **OpenAI** | `gpt-4.1-mini` |
|
||||
| **OpenAI** | `gpt-4.1-nano` |
|
||||
| **OpenAI** | `gpt-4o-mini` |
|
||||
| **Anthropic** | `claude-sonnet-4-20250514` |
|
||||
| **Anthropic** | `claude-3-5-sonnet-20241022` |
|
||||
| **Gemini** | `gemini-3-pro-preview` |
|
||||
| **Gemini** | `gemini-2.5-flash` |
|
||||
| **Gemini** | `gemini-2.5-flash-lite` |
|
||||
| **Groq** | `openai/gpt-oss-120b` |
|
||||
| **Groq** | `openai/gpt-oss-20b` |
|
||||
|
||||
### Provider Default Models
|
||||
|
||||
Each provider has a recommended default model that's used when `HINDSIGHT_API_LLM_MODEL` is not explicitly set. This makes configuration simpler - just specify the provider and get a sensible default:
|
||||
|
||||
| Provider | Default Model |
|
||||
|----------|--------------|
|
||||
| `openai` | `o3-mini` |
|
||||
| `anthropic` | `claude-haiku-4-5-20251001` |
|
||||
| `gemini` | `gemini-2.5-flash` |
|
||||
| `groq` | `openai/gpt-oss-120b` |
|
||||
| `ollama` | `gemma3:12b` |
|
||||
| `lmstudio` | `local-model` |
|
||||
| `vertexai` | `gemini-2.0-flash-001` |
|
||||
| `openai-codex` | `gpt-5.2-codex` |
|
||||
| `claude-code` | `claude-sonnet-4-5-20250929` |
|
||||
|
||||
**Example:** Setting just the provider uses its default model:
|
||||
```bash
|
||||
# Uses claude-haiku-4-5-20251001 automatically
|
||||
export HINDSIGHT_API_LLM_PROVIDER=anthropic
|
||||
export HINDSIGHT_API_LLM_API_KEY=sk-ant-xxxxxxxxxxxx
|
||||
```
|
||||
|
||||
You can override the default by explicitly setting `HINDSIGHT_API_LLM_MODEL`:
|
||||
```bash
|
||||
# Override to use Sonnet instead
|
||||
export HINDSIGHT_API_LLM_PROVIDER=anthropic
|
||||
export HINDSIGHT_API_LLM_API_KEY=sk-ant-xxxxxxxxxxxx
|
||||
export HINDSIGHT_API_LLM_MODEL=claude-sonnet-4-5-20250929
|
||||
```
|
||||
|
||||
This also applies to per-operation overrides:
|
||||
```bash
|
||||
# Global: OpenAI o3-mini (default)
|
||||
export HINDSIGHT_API_LLM_PROVIDER=openai
|
||||
|
||||
# Retain: Anthropic claude-haiku-4-5-20251001 (default)
|
||||
export HINDSIGHT_API_RETAIN_LLM_PROVIDER=anthropic
|
||||
```
|
||||
|
||||
### Using Other Models
|
||||
|
||||
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
|
||||
# Groq (recommended)
|
||||
export HINDSIGHT_API_LLM_PROVIDER=groq
|
||||
export HINDSIGHT_API_LLM_API_KEY=gsk_xxxxxxxxxxxx
|
||||
export HINDSIGHT_API_LLM_MODEL=openai/gpt-oss-20b
|
||||
|
||||
# OpenAI
|
||||
export HINDSIGHT_API_LLM_PROVIDER=openai
|
||||
export HINDSIGHT_API_LLM_API_KEY=sk-xxxxxxxxxxxx
|
||||
export HINDSIGHT_API_LLM_MODEL=gpt-4o
|
||||
|
||||
# Gemini
|
||||
export HINDSIGHT_API_LLM_PROVIDER=gemini
|
||||
export HINDSIGHT_API_LLM_API_KEY=xxxxxxxxxxxx
|
||||
export HINDSIGHT_API_LLM_MODEL=gemini-2.0-flash
|
||||
|
||||
# Anthropic
|
||||
export HINDSIGHT_API_LLM_PROVIDER=anthropic
|
||||
export HINDSIGHT_API_LLM_API_KEY=sk-ant-xxxxxxxxxxxx
|
||||
export HINDSIGHT_API_LLM_MODEL=claude-sonnet-4-20250514
|
||||
|
||||
# Ollama (local)
|
||||
export HINDSIGHT_API_LLM_PROVIDER=ollama
|
||||
export HINDSIGHT_API_LLM_BASE_URL=http://localhost:11434/v1
|
||||
export HINDSIGHT_API_LLM_MODEL=llama3
|
||||
|
||||
# LM Studio (local)
|
||||
export HINDSIGHT_API_LLM_PROVIDER=lmstudio
|
||||
export HINDSIGHT_API_LLM_BASE_URL=http://localhost:1234/v1
|
||||
export HINDSIGHT_API_LLM_MODEL=your-local-model
|
||||
```
|
||||
|
||||
**Note:** The LLM is the primary bottleneck for retain operations. See [Performance](./performance) for optimization strategies.
|
||||
|
||||
---
|
||||
|
||||
### OpenAI Codex Setup (ChatGPT Plus/Pro)
|
||||
|
||||
Use your ChatGPT Plus or Pro subscription for Hindsight without separate OpenAI Platform API costs.
|
||||
|
||||
**Prerequisites:**
|
||||
- Active ChatGPT Plus or Pro subscription
|
||||
- Node.js/npm installed (for Codex CLI)
|
||||
|
||||
**Setup Steps:**
|
||||
|
||||
1. **Install Codex CLI:**
|
||||
```bash
|
||||
npm install -g @openai/codex
|
||||
```
|
||||
|
||||
2. **Login with ChatGPT credentials:**
|
||||
```bash
|
||||
codex auth login
|
||||
```
|
||||
This opens a browser window to authenticate with your ChatGPT account and saves OAuth tokens to `~/.codex/auth.json`.
|
||||
|
||||
3. **Verify authentication:**
|
||||
```bash
|
||||
ls ~/.codex/auth.json # Should show the auth file exists
|
||||
```
|
||||
|
||||
4. **Configure Hindsight:**
|
||||
```bash
|
||||
export HINDSIGHT_API_LLM_PROVIDER=openai-codex
|
||||
# export HINDSIGHT_API_LLM_MODEL=gpt-5.1-codex # defaults to gpt-5.2-codex
|
||||
# No API key needed - reads from ~/.codex/auth.json automatically
|
||||
```
|
||||
|
||||
5. **Start Hindsight:**
|
||||
```bash
|
||||
hindsight-api
|
||||
```
|
||||
|
||||
You can use any model supported by OpenAI Codex CLI
|
||||
|
||||
**Important Notes:**
|
||||
- OAuth tokens are stored in `~/.codex/auth.json`
|
||||
- Tokens refresh automatically when needed
|
||||
- Usage is billed to your ChatGPT subscription (not separate API costs)
|
||||
- For personal development use only (see ChatGPT Terms of Service)
|
||||
|
||||
---
|
||||
|
||||
### 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
|
||||
|
||||
**Setup Steps:**
|
||||
|
||||
1. **Install Claude Code CLI:**
|
||||
```bash
|
||||
npm install -g @anthropics/claude-code
|
||||
# Or via Homebrew
|
||||
brew install anthropics/claude-code/claude-code
|
||||
```
|
||||
|
||||
2. **Login with Claude credentials:**
|
||||
```bash
|
||||
claude auth login
|
||||
```
|
||||
This opens a browser window to authenticate with your Claude account. Authentication is automatically managed by the Claude Agent SDK.
|
||||
|
||||
3. **Verify authentication:**
|
||||
```bash
|
||||
claude --version
|
||||
# Should show version without errors
|
||||
```
|
||||
|
||||
4. **Configure Hindsight:**
|
||||
```bash
|
||||
export HINDSIGHT_API_LLM_PROVIDER=claude-code
|
||||
# No API key needed - uses claude auth login credentials
|
||||
```
|
||||
|
||||
5. **Start Hindsight:**
|
||||
```bash
|
||||
hindsight-api
|
||||
```
|
||||
|
||||
You can use any model supported by Claude Code CLI.
|
||||
|
||||
**Important Notes:**
|
||||
- Authentication handled by Claude Agent SDK (uses bundled CLI)
|
||||
- Credentials managed securely by Claude Code
|
||||
- Usage billed to your Claude subscription (not separate API costs)
|
||||
- For personal development use only (see Claude Terms of Service)
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Embedding Model
|
||||
|
||||
Converts text into dense vector representations for semantic similarity search.
|
||||
|
||||
**Default:** `BAAI/bge-small-en-v1.5` (384 dimensions, ~130MB)
|
||||
|
||||
### Supported Providers
|
||||
|
||||
| Provider | Description | Best For |
|
||||
|----------|-------------|----------|
|
||||
| `local` | SentenceTransformers (default) | Development, low latency |
|
||||
| `openai` | OpenAI embeddings API | Production, high quality |
|
||||
| `cohere` | Cohere embeddings API | Production, multilingual |
|
||||
| `tei` | HuggingFace Text Embeddings Inference | Production, self-hosted |
|
||||
| `litellm` | LiteLLM proxy (unified gateway) | Multi-provider setups |
|
||||
|
||||
### Local Models
|
||||
|
||||
| Model | Dimensions | Use Case |
|
||||
|-------|------------|----------|
|
||||
| `BAAI/bge-small-en-v1.5` | 384 | Default, fast, good quality |
|
||||
| `sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2` | 384 | Multilingual (50+ languages) |
|
||||
|
||||
### OpenAI Models
|
||||
|
||||
| Model | Dimensions | Use Case |
|
||||
|-------|------------|----------|
|
||||
| `text-embedding-3-small` | 1536 | Default OpenAI, cost-effective |
|
||||
| `text-embedding-3-large` | 3072 | Higher quality, more expensive |
|
||||
| `text-embedding-ada-002` | 1536 | Legacy model |
|
||||
|
||||
### Cohere Models
|
||||
|
||||
| Model | Dimensions | Use Case |
|
||||
|-------|------------|----------|
|
||||
| `embed-english-v3.0` | 1024 | English text |
|
||||
| `embed-multilingual-v3.0` | 1024 | 100+ languages |
|
||||
|
||||
:::warning Embedding Dimensions
|
||||
Hindsight automatically detects the embedding dimension at startup and adjusts the database schema. Once memories are stored, you cannot change dimensions without losing data.
|
||||
:::
|
||||
|
||||
**Configuration Examples:**
|
||||
|
||||
```bash
|
||||
# Local provider (default)
|
||||
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=local
|
||||
export HINDSIGHT_API_EMBEDDINGS_LOCAL_MODEL=BAAI/bge-small-en-v1.5
|
||||
|
||||
# OpenAI
|
||||
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=openai
|
||||
export HINDSIGHT_API_EMBEDDINGS_OPENAI_API_KEY=sk-xxxxxxxxxxxx
|
||||
export HINDSIGHT_API_EMBEDDINGS_OPENAI_MODEL=text-embedding-3-small
|
||||
|
||||
# Cohere
|
||||
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=cohere
|
||||
export HINDSIGHT_API_COHERE_API_KEY=your-api-key
|
||||
export HINDSIGHT_API_EMBEDDINGS_COHERE_MODEL=embed-english-v3.0
|
||||
|
||||
# TEI (self-hosted)
|
||||
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=tei
|
||||
export HINDSIGHT_API_EMBEDDINGS_TEI_URL=http://localhost:8080
|
||||
|
||||
# LiteLLM proxy
|
||||
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=litellm
|
||||
export HINDSIGHT_API_LITELLM_API_BASE=http://localhost:4000
|
||||
export HINDSIGHT_API_EMBEDDINGS_LITELLM_MODEL=text-embedding-3-small
|
||||
```
|
||||
|
||||
See [Configuration](./configuration#embeddings) for all options including Azure OpenAI and custom endpoints.
|
||||
|
||||
---
|
||||
|
||||
## Cross-Encoder (Reranker)
|
||||
|
||||
Reranks initial search results to improve precision.
|
||||
|
||||
**Default:** `cross-encoder/ms-marco-MiniLM-L-6-v2` (~85MB)
|
||||
|
||||
### Supported Providers
|
||||
|
||||
| Provider | Description | Best For |
|
||||
|----------|-------------|----------|
|
||||
| `local` | SentenceTransformers CrossEncoder (default) | Development, low latency |
|
||||
| `cohere` | Cohere rerank API | Production, high quality |
|
||||
| `tei` | HuggingFace Text Embeddings Inference | Production, self-hosted |
|
||||
| `flashrank` | FlashRank (lightweight, fast) | Resource-constrained environments |
|
||||
| `litellm` | LiteLLM proxy (unified gateway) | Multi-provider setups |
|
||||
| `rrf` | RRF-only (no neural reranking) | Testing, minimal resources |
|
||||
|
||||
### Local Models
|
||||
|
||||
| Model | Use Case |
|
||||
|-------|----------|
|
||||
| `cross-encoder/ms-marco-MiniLM-L-6-v2` | Default, fast |
|
||||
| `cross-encoder/ms-marco-MiniLM-L-12-v2` | Higher accuracy |
|
||||
| `cross-encoder/mmarco-mMiniLMv2-L12-H384-v1` | Multilingual |
|
||||
|
||||
### Cohere Models
|
||||
|
||||
| Model | Use Case |
|
||||
|-------|----------|
|
||||
| `rerank-english-v3.0` | English text |
|
||||
| `rerank-multilingual-v3.0` | 100+ languages |
|
||||
|
||||
### LiteLLM Supported Providers
|
||||
|
||||
LiteLLM supports multiple reranking providers via the `/rerank` endpoint:
|
||||
|
||||
| Provider | Model Example |
|
||||
|----------|---------------|
|
||||
| Cohere | `cohere/rerank-english-v3.0` |
|
||||
| Together AI | `together_ai/...` |
|
||||
| Voyage AI | `voyage/rerank-2` |
|
||||
| Jina AI | `jina_ai/...` |
|
||||
| AWS Bedrock | `bedrock/...` |
|
||||
|
||||
**Configuration Examples:**
|
||||
|
||||
```bash
|
||||
# Local provider (default)
|
||||
export HINDSIGHT_API_RERANKER_PROVIDER=local
|
||||
export HINDSIGHT_API_RERANKER_LOCAL_MODEL=cross-encoder/ms-marco-MiniLM-L-6-v2
|
||||
|
||||
# Cohere
|
||||
export HINDSIGHT_API_RERANKER_PROVIDER=cohere
|
||||
export HINDSIGHT_API_COHERE_API_KEY=your-api-key
|
||||
export HINDSIGHT_API_RERANKER_COHERE_MODEL=rerank-english-v3.0
|
||||
|
||||
# TEI (self-hosted)
|
||||
export HINDSIGHT_API_RERANKER_PROVIDER=tei
|
||||
export HINDSIGHT_API_RERANKER_TEI_URL=http://localhost:8081
|
||||
|
||||
# FlashRank (lightweight)
|
||||
export HINDSIGHT_API_RERANKER_PROVIDER=flashrank
|
||||
|
||||
# LiteLLM proxy
|
||||
export HINDSIGHT_API_RERANKER_PROVIDER=litellm
|
||||
export HINDSIGHT_API_LITELLM_API_BASE=http://localhost:4000
|
||||
export HINDSIGHT_API_RERANKER_LITELLM_MODEL=cohere/rerank-english-v3.0
|
||||
|
||||
# RRF-only (no neural reranking)
|
||||
export HINDSIGHT_API_RERANKER_PROVIDER=rrf
|
||||
```
|
||||
|
||||
See [Configuration](./configuration#reranker) for all options including Azure-hosted endpoints and batch settings.
|
||||
@@ -0,0 +1,199 @@
|
||||
# Monitoring
|
||||
|
||||
Hindsight provides comprehensive monitoring through Prometheus metrics and pre-built Grafana dashboards.
|
||||
|
||||
## Local Development
|
||||
|
||||
For local metrics visualization, a convenience script downloads and runs Prometheus and Grafana:
|
||||
|
||||
```bash
|
||||
./scripts/dev/start-monitoring.sh
|
||||
```
|
||||
|
||||
This will start:
|
||||
- **Grafana**: http://localhost:8890 (anonymous access enabled)
|
||||
- **Prometheus**: http://localhost:8889
|
||||
- **API Metrics**: http://localhost:8888/metrics
|
||||
|
||||
:::note Production Deployment
|
||||
The local monitoring script is for development only. In production, you need to install and configure Prometheus and Grafana separately, then point Prometheus to scrape your Hindsight API's `/metrics` endpoint.
|
||||
:::
|
||||
|
||||
## Grafana Dashboards
|
||||
|
||||
Pre-built dashboards are available in [`monitoring/grafana/dashboards/`](https://github.com/anthropics/hindsight/tree/main/monitoring/grafana/dashboards). Import these JSON files into your Grafana instance:
|
||||
|
||||
| Dashboard | Description |
|
||||
|-----------|-------------|
|
||||
| **Hindsight Operations** | Operation rates, latency percentiles, per-bank metrics |
|
||||
| **Hindsight LLM Metrics** | LLM calls, token usage, latency by scope/provider |
|
||||
| **Hindsight API Service** | HTTP requests, error rates, DB pool, process metrics |
|
||||
|
||||
The dashboards are automatically provisioned when using the monitoring stack script.
|
||||
|
||||
## Metrics Endpoint
|
||||
|
||||
Hindsight exposes Prometheus metrics at `/metrics`:
|
||||
|
||||
```bash
|
||||
curl http://localhost:8888/metrics
|
||||
```
|
||||
|
||||
## Available Metrics
|
||||
|
||||
### Operation Metrics
|
||||
|
||||
| Metric | Type | Labels | Description |
|
||||
|--------|------|--------|-------------|
|
||||
| `hindsight.operation.duration` | Histogram | operation, bank_id, source, budget, max_tokens, success | Duration of operations in seconds |
|
||||
| `hindsight.operation.total` | Counter | operation, bank_id, source, budget, max_tokens, success | Total number of operations executed |
|
||||
|
||||
**Labels:**
|
||||
- `operation`: Operation type (`retain`, `recall`, `reflect`)
|
||||
- `bank_id`: Memory bank identifier
|
||||
- `source`: Where the operation was triggered from (`api`, `reflect`, `internal`)
|
||||
- `budget`: Budget level if specified (`low`, `mid`, `high`)
|
||||
- `max_tokens`: Max tokens if specified
|
||||
- `success`: Whether the operation succeeded (`true`, `false`)
|
||||
|
||||
The `source` label allows distinguishing between:
|
||||
- `api`: Direct API calls from clients
|
||||
- `reflect`: Internal recall calls made during reflect operations
|
||||
- `internal`: Other internal operations
|
||||
|
||||
### LLM Metrics
|
||||
|
||||
| Metric | Type | Labels | Description |
|
||||
|--------|------|--------|-------------|
|
||||
| `hindsight.llm.duration` | Histogram | provider, model, scope, success | Duration of LLM API calls in seconds |
|
||||
| `hindsight.llm.calls.total` | Counter | provider, model, scope, success | Total number of LLM API calls |
|
||||
| `hindsight.llm.tokens.input` | Counter | provider, model, scope, success, token_bucket | Input tokens for LLM calls |
|
||||
| `hindsight.llm.tokens.output` | Counter | provider, model, scope, success, token_bucket | Output tokens from LLM calls |
|
||||
|
||||
**Labels:**
|
||||
- `provider`: LLM provider (`openai`, `anthropic`, `gemini`, `groq`, `ollama`, `lmstudio`)
|
||||
- `model`: Model name (e.g., `gpt-4`, `claude-3-sonnet`)
|
||||
- `scope`: What the LLM call is for (`memory`, `reflect`, `consolidation`, `answer`)
|
||||
- `success`: Whether the call succeeded (`true`, `false`)
|
||||
- `token_bucket`: Token count bucket for cardinality control (`0-100`, `100-500`, `500-1k`, `1k-5k`, `5k-10k`, `10k-50k`, `50k+`)
|
||||
|
||||
### HTTP Request Metrics
|
||||
|
||||
| Metric | Type | Labels | Description |
|
||||
|--------|------|--------|-------------|
|
||||
| `hindsight.http.duration` | Histogram | method, endpoint, status_code, status_class | Duration of HTTP requests in seconds |
|
||||
| `hindsight.http.requests.total` | Counter | method, endpoint, status_code, status_class | Total number of HTTP requests |
|
||||
| `hindsight.http.requests.in_progress` | UpDownCounter | method, endpoint | Number of HTTP requests currently being processed |
|
||||
|
||||
**Labels:**
|
||||
- `method`: HTTP method (`GET`, `POST`, `PUT`, `DELETE`)
|
||||
- `endpoint`: Request path (normalized to reduce cardinality - UUIDs replaced with `{id}`)
|
||||
- `status_code`: HTTP status code (`200`, `400`, `500`, etc.)
|
||||
- `status_class`: Status code class (`2xx`, `4xx`, `5xx`)
|
||||
|
||||
### Database Pool Metrics
|
||||
|
||||
| Metric | Type | Labels | Description |
|
||||
|--------|------|--------|-------------|
|
||||
| `hindsight.db.pool.size` | Gauge | - | Current number of connections in the pool |
|
||||
| `hindsight.db.pool.idle` | Gauge | - | Number of idle connections in the pool |
|
||||
| `hindsight.db.pool.min` | Gauge | - | Minimum pool size |
|
||||
| `hindsight.db.pool.max` | Gauge | - | Maximum pool size |
|
||||
|
||||
### Process Metrics
|
||||
|
||||
| Metric | Type | Labels | Description |
|
||||
|--------|------|--------|-------------|
|
||||
| `hindsight.process.cpu.seconds` | Gauge | type | Process CPU time in seconds |
|
||||
| `hindsight.process.memory.bytes` | Gauge | type | Process memory usage in bytes |
|
||||
| `hindsight.process.open_fds` | Gauge | - | Number of open file descriptors |
|
||||
| `hindsight.process.threads` | Gauge | - | Number of active threads |
|
||||
|
||||
**Labels:**
|
||||
- `type` (CPU): `user` or `system`
|
||||
- `type` (Memory): `rss_max` (maximum resident set size)
|
||||
|
||||
### Histogram Buckets
|
||||
|
||||
Custom bucket boundaries are configured for better percentile accuracy:
|
||||
|
||||
**Operation Duration Buckets (seconds):**
|
||||
```
|
||||
0.1, 0.25, 0.5, 0.75, 1.0, 2.0, 3.0, 5.0, 7.5, 10.0, 15.0, 20.0, 30.0, 60.0, 120.0
|
||||
```
|
||||
|
||||
**LLM Duration Buckets (seconds):**
|
||||
```
|
||||
0.1, 0.25, 0.5, 1.0, 2.0, 3.0, 5.0, 10.0, 15.0, 30.0, 60.0, 120.0
|
||||
```
|
||||
|
||||
**HTTP Duration Buckets (seconds):**
|
||||
```
|
||||
0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0
|
||||
```
|
||||
|
||||
## Prometheus Configuration
|
||||
|
||||
```yaml
|
||||
scrape_configs:
|
||||
- job_name: 'hindsight'
|
||||
static_configs:
|
||||
- targets: ['localhost:8888']
|
||||
```
|
||||
|
||||
## Example Queries
|
||||
|
||||
### Average operation latency by type
|
||||
```promql
|
||||
rate(hindsight_operation_duration_sum[5m]) / rate(hindsight_operation_duration_count[5m])
|
||||
```
|
||||
|
||||
### LLM calls per minute by provider
|
||||
```promql
|
||||
rate(hindsight_llm_calls_total[1m]) * 60
|
||||
```
|
||||
|
||||
### P95 LLM latency
|
||||
```promql
|
||||
histogram_quantile(0.95, rate(hindsight_llm_duration_bucket[5m]))
|
||||
```
|
||||
|
||||
### Total tokens consumed by model
|
||||
```promql
|
||||
sum by (model) (hindsight_llm_tokens_input_total + hindsight_llm_tokens_output_total)
|
||||
```
|
||||
|
||||
### Internal vs API recall operations
|
||||
```promql
|
||||
sum by (source) (rate(hindsight_operation_total{operation="recall"}[5m]))
|
||||
```
|
||||
|
||||
### HTTP requests per second by endpoint
|
||||
```promql
|
||||
sum by (endpoint) (rate(hindsight_http_requests_total[1m]))
|
||||
```
|
||||
|
||||
### HTTP error rate (5xx)
|
||||
```promql
|
||||
sum(rate(hindsight_http_requests_total{status_class="5xx"}[5m])) / sum(rate(hindsight_http_requests_total[5m]))
|
||||
```
|
||||
|
||||
### P95 HTTP latency
|
||||
```promql
|
||||
histogram_quantile(0.95, sum by (le) (rate(hindsight_http_duration_seconds_bucket[5m])))
|
||||
```
|
||||
|
||||
### Database pool utilization
|
||||
```promql
|
||||
hindsight_db_pool_size / hindsight_db_pool_max
|
||||
```
|
||||
|
||||
### Active database connections
|
||||
```promql
|
||||
hindsight_db_pool_size - hindsight_db_pool_idle
|
||||
```
|
||||
|
||||
### CPU usage rate
|
||||
```promql
|
||||
rate(hindsight_process_cpu_seconds{type="user"}[1m])
|
||||
```
|
||||
@@ -0,0 +1,217 @@
|
||||
---
|
||||
sidebar_position: 5
|
||||
---
|
||||
|
||||
# Multilingual Support
|
||||
|
||||
Hindsight automatically detects the language of your input and responds in the same language. This means facts, entities, and reflect responses are preserved in their original language without translation to English.
|
||||
|
||||
## How It Works
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[Chinese Input] --> B[Language Detection]
|
||||
B --> C[Extract Facts in Chinese]
|
||||
C --> D[Chinese Entities]
|
||||
D --> E[Chinese Response]
|
||||
```
|
||||
|
||||
When you retain content or reflect on a query, Hindsight:
|
||||
|
||||
1. **Detects the input language** automatically from the content
|
||||
2. **Extracts facts in the original language** - preserving nuance and meaning
|
||||
3. **Stores entities in their native script** - 张伟 stays 张伟, not "Zhang Wei"
|
||||
4. **Responds in the same language** - queries in Chinese get Chinese answers
|
||||
|
||||
---
|
||||
|
||||
## Retain with Non-English Content
|
||||
|
||||
When you retain content in any language, Hindsight extracts and stores facts in that same language.
|
||||
|
||||
### Example: Chinese Content
|
||||
|
||||
```python
|
||||
from hindsight import Hindsight
|
||||
|
||||
hindsight = Hindsight()
|
||||
|
||||
# Retain Chinese content
|
||||
hindsight.retain(
|
||||
bank_id="user-123",
|
||||
content="""
|
||||
张伟是一位资深软件工程师,在腾讯工作了五年。
|
||||
他专门研究分布式系统,并领导了公司微服务架构的开发。
|
||||
""",
|
||||
context="团队概述"
|
||||
)
|
||||
|
||||
# Query in Chinese - get Chinese results
|
||||
results = hindsight.recall(
|
||||
bank_id="user-123",
|
||||
query="告诉我关于张伟的信息"
|
||||
)
|
||||
|
||||
# Facts are returned in Chinese:
|
||||
# - 张伟是一位资深软件工程师,在腾讯工作了五年
|
||||
# - 张伟专门研究分布式系统,并领导了公司微服务架构的开发
|
||||
```
|
||||
|
||||
### Example: Japanese Content
|
||||
|
||||
```python
|
||||
hindsight.retain(
|
||||
bank_id="user-123",
|
||||
content="""
|
||||
田中さんはソフトウェアエンジニアで、東京のスタートアップで働いています。
|
||||
彼女はPythonとTypeScriptが得意で、毎日コードレビューをしています。
|
||||
""",
|
||||
context="チームプロフィール"
|
||||
)
|
||||
|
||||
# Query in Japanese
|
||||
results = hindsight.recall(
|
||||
bank_id="user-123",
|
||||
query="田中さんについて教えてください"
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reflect with Non-English Queries
|
||||
|
||||
The `reflect` operation also respects the input language, generating thoughtful responses in the same language as the query.
|
||||
|
||||
### Example: Chinese Reflection
|
||||
|
||||
```python
|
||||
# Store facts about team members (in Chinese)
|
||||
hindsight.retain(
|
||||
bank_id="team-eval",
|
||||
content="张伟是一位优秀的软件工程师,完成了五个重大项目。他总是按时交付,代码整洁有良好的文档。",
|
||||
context="绩效评估"
|
||||
)
|
||||
|
||||
hindsight.retain(
|
||||
bank_id="team-eval",
|
||||
content="李明最近加入团队。他错过了第一个截止日期,代码有很多bug。",
|
||||
context="绩效评估"
|
||||
)
|
||||
|
||||
# Reflect in Chinese
|
||||
result = hindsight.reflect(
|
||||
bank_id="team-eval",
|
||||
query="谁是更可靠的工程师?"
|
||||
)
|
||||
|
||||
# Response is in Chinese:
|
||||
# "我认为张伟更可靠。张伟完成了五个重大项目,按时交付,代码质量高..."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Mixed Language Content
|
||||
|
||||
Hindsight handles mixed-language content gracefully, preserving both languages where appropriate.
|
||||
|
||||
### Example: Chinese Text with English Company Names
|
||||
|
||||
```python
|
||||
hindsight.retain(
|
||||
bank_id="user-123",
|
||||
content="""
|
||||
王芳在Google北京办公室工作,她是一名高级产品经理。
|
||||
之前她在Microsoft和Amazon工作过。
|
||||
她负责管理YouTube在中国市场的推广策略。
|
||||
""",
|
||||
context="员工资料"
|
||||
)
|
||||
|
||||
# Facts preserve both languages:
|
||||
# - 王芳在Google北京办公室工作,担任高级产品经理
|
||||
# - 王芳曾在Microsoft和Amazon工作过
|
||||
# - 王芳负责管理YouTube在中国市场的推广策略
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Supported Languages
|
||||
|
||||
**Hindsight's multilingual support depends entirely on your LLM's language capabilities.** Hindsight instructs the LLM to detect the input language and respond in that same language. If your LLM supports a language, Hindsight will work with it.
|
||||
|
||||
Most modern LLMs (GPT-4, Claude, Gemini, Llama 3, etc.) support dozens of languages including:
|
||||
|
||||
- **East Asian**: Chinese (Simplified/Traditional), Japanese, Korean
|
||||
- **European**: Spanish, French, German, Italian, Portuguese, Dutch, Polish, Russian
|
||||
- **Middle Eastern**: Arabic, Hebrew, Turkish
|
||||
- **South Asian**: Hindi, Bengali, Tamil
|
||||
- **Southeast Asian**: Thai, Vietnamese, Indonesian
|
||||
|
||||
**To verify support for your target language**, test your LLM directly with content in that language. If the LLM can understand and generate text in the language, Hindsight will preserve it correctly.
|
||||
|
||||
---
|
||||
|
||||
## Configuring for Multilingual Use
|
||||
|
||||
For optimal multilingual performance, you should configure all three components of the pipeline:
|
||||
|
||||
### 1. LLM (Required)
|
||||
Your LLM must support the target languages. Most modern LLMs do, but verify with your specific model.
|
||||
|
||||
### 2. Embedding Model (Recommended)
|
||||
The default embedding model (`BAAI/bge-small-en-v1.5`) is **English-only**. For multilingual content, use a multilingual embedding model:
|
||||
|
||||
```bash
|
||||
# In your .env file
|
||||
HINDSIGHT_API_EMBEDDINGS_LOCAL_MODEL=BAAI/bge-m3
|
||||
```
|
||||
|
||||
**Recommended multilingual embedding models:**
|
||||
| Model | Languages | Notes |
|
||||
|-------|-----------|-------|
|
||||
| `BAAI/bge-m3` | 100+ | Best overall multilingual performance |
|
||||
| `intfloat/multilingual-e5-large` | 100+ | Good alternative |
|
||||
| `sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2` | 50+ | Lighter weight |
|
||||
|
||||
### 3. Reranker Model (Recommended)
|
||||
The default reranker (`cross-encoder/ms-marco-MiniLM-L-6-v2`) is **English-only**. For multilingual content, use a multilingual reranker:
|
||||
|
||||
```bash
|
||||
# In your .env file
|
||||
HINDSIGHT_API_RERANKER_LOCAL_MODEL=BAAI/bge-reranker-v2-m3
|
||||
```
|
||||
|
||||
**Recommended multilingual reranker models:**
|
||||
| Model | Languages | Notes |
|
||||
|-------|-----------|-------|
|
||||
| `BAAI/bge-reranker-v2-m3` | 100+ | Best multilingual reranking |
|
||||
| `cross-encoder/mmarco-mMiniLMv2-L12-H384-v1` | 14 | Lighter alternative |
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Use Multilingual Models for Non-English Content
|
||||
If you primarily work with non-English content, configure multilingual embedding and reranker models. English-only models will still store your content correctly, but semantic search quality will be degraded.
|
||||
|
||||
### 2. Keep Content in One Language Per Retain Call
|
||||
While mixed content works, keeping each `retain` call in a single language produces more consistent results.
|
||||
|
||||
### 3. Query in the Same Language as Your Content
|
||||
For best results, query using the same language as your stored content. Cross-language queries (e.g., English query for Chinese content) may work but results can vary depending on your embedding model.
|
||||
|
||||
---
|
||||
|
||||
## Technical Details
|
||||
|
||||
Multilingual support is implemented through LLM prompt instructions rather than external language detection libraries. This approach:
|
||||
|
||||
- **Requires no additional dependencies**
|
||||
- **Works with any LLM** that supports multiple languages
|
||||
- **Handles edge cases** like mixed-language content naturally
|
||||
- **Preserves semantic meaning** better than rule-based translation
|
||||
|
||||
The LLM is instructed to:
|
||||
1. Detect the input language
|
||||
2. Extract all facts, entities, and descriptions in that same language
|
||||
3. Never translate to English unless the input is in English
|
||||
@@ -0,0 +1,182 @@
|
||||
|
||||
|
||||
# Observations: Knowledge Consolidation
|
||||
|
||||
After memories are retained, Hindsight automatically consolidates related facts into **observations** — synthesized knowledge representations that capture patterns and learnings.
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[New Facts] --> B[Consolidation Engine]
|
||||
B --> C{Existing Observation?}
|
||||
C -->|Yes| D[Refine Observation]
|
||||
C -->|No| E[Create Observation]
|
||||
D --> F[Observations]
|
||||
E --> F
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What Are Observations?
|
||||
|
||||
Observations are **consolidated knowledge** synthesized from multiple facts. Unlike raw facts which are individual pieces of information, observations represent patterns, preferences, and learnings that emerge from accumulated evidence.
|
||||
|
||||
| Raw Facts | Observation |
|
||||
|-----------|--------------|
|
||||
| "Alice prefers Python" | "Alice is a Python-focused developer who values readability and simplicity" |
|
||||
| "Alice dislikes verbose code" | |
|
||||
| "Alice recommends type hints" | |
|
||||
|
||||
Observations provide:
|
||||
- **Synthesis**: Patterns that emerge from multiple facts
|
||||
- **Context**: Richer understanding than individual facts
|
||||
- **Efficiency**: Condensed knowledge for faster retrieval
|
||||
|
||||
---
|
||||
|
||||
## How Consolidation Works
|
||||
|
||||
### Automatic Background Processing
|
||||
|
||||
After `retain()` completes, the consolidation engine runs automatically:
|
||||
|
||||
1. **New facts analyzed** — Each new fact is compared against existing observations
|
||||
2. **Pattern detection** — Related facts are grouped and synthesized
|
||||
3. **Observation creation/update** — New observations are created or existing ones refined
|
||||
4. **Evidence tracking** — Each observation maintains references to supporting facts
|
||||
|
||||
### Evidence-Based Evolution
|
||||
|
||||
Observations evolve as new evidence arrives:
|
||||
|
||||
| Event | What the bank learns | Observation state |
|
||||
|-------|---------------------|----------------|
|
||||
| **Day 1** | "Redis is open source under BSD license" | "Redis is excellent for caching — fast, reliable, and OSS-friendly" (2 supporting facts) |
|
||||
| **Day 2** | "Redis has great community support" | Observation reinforced (3 supporting facts) |
|
||||
| **Day 30** | "Redis changed license to SSPL" | Observation refined: "Redis is technically strong, but has license concerns for cloud" |
|
||||
| **Day 45** | "Valkey forked Redis under BSD" | New observation: "Consider Valkey for new projects requiring true OSS" |
|
||||
|
||||
### Handling Contradictory Evidence
|
||||
|
||||
What happens when a new fact contradicts an existing observation?
|
||||
|
||||
The consolidation engine doesn't blindly overwrite — it **reconciles** the contradiction by capturing the evolution:
|
||||
|
||||
**Example: User preference changes**
|
||||
|
||||
| Time | Fact | Observation |
|
||||
|------|------|--------------|
|
||||
| Week 1 | "User says they love React" | "User prefers React for frontend development" |
|
||||
| Week 2 | "User praises React's component model" | "User is enthusiastic about React, particularly its component model" |
|
||||
| Week 3 | "User says they've switched to Vue and won't use React anymore" | "User was previously a React enthusiast who appreciated its component model, but has now switched to Vue and no longer uses React" |
|
||||
|
||||
Notice how the final observation captures the **full journey** — not just "User prefers Vue" but the complete evolution of their preference. This nuanced understanding means:
|
||||
|
||||
- Your agent won't recommend React tutorials to someone who explicitly moved away from it
|
||||
- Your agent understands *why* this matters (they were enthusiastic before, so this is a deliberate choice)
|
||||
- Your agent can reference this history when relevant ("I know you used to work with React...")
|
||||
|
||||
The system:
|
||||
1. **Detects the conflict** — New fact contradicts existing observation
|
||||
2. **Preserves history** — Incorporates the previous understanding into the new observation
|
||||
3. **Creates nuanced observation** — Synthesizes a richer understanding that captures the change
|
||||
4. **Updates freshness** — Marks the observation as recently updated
|
||||
|
||||
**Example: Correcting misinformation**
|
||||
|
||||
| Time | Fact | Observation |
|
||||
|------|------|--------------|
|
||||
| Day 1 | "Alice works at Google" | "Alice is a Google employee" |
|
||||
| Day 10 | "Alice actually works at Meta, not Google" | "Alice works at Meta (previously thought to work at Google)" |
|
||||
|
||||
When a fact explicitly corrects previous information, the observation is updated to reflect the correction while noting the previous understanding. The raw facts are always preserved, so you can trace back to see what was originally stated and when it was corrected.
|
||||
|
||||
---
|
||||
|
||||
## Observations in Retrieval
|
||||
|
||||
Observations are automatically included in both `recall()` and `reflect()` operations:
|
||||
|
||||
### In Recall
|
||||
|
||||
Observations are returned alongside raw facts, filtered by the `types` parameter:
|
||||
|
||||
```python
|
||||
# Include observations in recall
|
||||
results = client.recall(
|
||||
bank_id="my-bank",
|
||||
query="What programming languages does Alice prefer?",
|
||||
types=["world", "experience", "observation"]
|
||||
)
|
||||
|
||||
# Observations only
|
||||
observations = client.recall(
|
||||
bank_id="my-bank",
|
||||
query="What patterns have I learned?",
|
||||
types=["observation"]
|
||||
)
|
||||
```
|
||||
|
||||
### In Reflect
|
||||
|
||||
The reflect agent uses **hierarchical retrieval**:
|
||||
|
||||
1. **[Mental Models](/developer/api/mental-models)** — User-curated summaries (highest priority)
|
||||
2. **Observations** — Consolidated knowledge with freshness awareness
|
||||
3. **Raw Facts** — Ground truth for verification
|
||||
|
||||
The agent automatically queries observations and uses them to inform its reasoning.
|
||||
|
||||
---
|
||||
|
||||
## Freshness Awareness
|
||||
|
||||
Observations track when they were last updated. During reflect, the agent considers freshness:
|
||||
|
||||
- **Fresh observations**: Used directly for reasoning
|
||||
- **Stale observations**: Agent verifies against current facts before relying on them
|
||||
|
||||
This ensures responses stay accurate even as the underlying data changes.
|
||||
|
||||
---
|
||||
|
||||
## Mission-Oriented Consolidation
|
||||
|
||||
The bank's **mission** directly influences what knowledge gets consolidated into observations. When you set a mission on your memory bank, the consolidation engine focuses on extracting knowledge that serves that mission.
|
||||
|
||||
**Example:**
|
||||
|
||||
```python
|
||||
client.create_bank(
|
||||
bank_id="support-agent",
|
||||
mission="You're a customer support agent - keep track of "
|
||||
"customer preferences, past issues, and communication styles."
|
||||
)
|
||||
```
|
||||
|
||||
With this mission, the consolidation engine will:
|
||||
- **Prioritize** customer preferences, issue patterns, and communication styles
|
||||
- **Skip** ephemeral details that don't serve support goals
|
||||
- **Synthesize** observations focused on helping customers
|
||||
|
||||
Without a mission, the engine performs general-purpose consolidation. With a mission, it becomes focused and efficient — extracting only knowledge that matters for your use case.
|
||||
|
||||
| Mission | Observations Focus |
|
||||
|---------|-------------------|
|
||||
| *Customer support agent* | Customer preferences, issue patterns, resolution history |
|
||||
| *Code review assistant* | Coding patterns, team conventions, common mistakes |
|
||||
| *Research assistant* | Topic expertise, source reliability, methodology preferences |
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
Observation consolidation runs automatically. You can monitor consolidation via the [Operations API](./api/operations).
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [**Retain**](./retain) — How facts are stored and trigger consolidation
|
||||
- [**Recall**](./retrieval) — How observations are retrieved
|
||||
- [**Reflect**](./reflect) — How the agentic loop uses observations
|
||||
- [**Mental Models**](./api/mental-models) — User-curated summaries for common queries
|
||||
@@ -0,0 +1,133 @@
|
||||
# Performance
|
||||
|
||||
Hindsight is designed for high-performance semantic memory operations at scale. This page covers performance characteristics, optimization strategies, and best practices.
|
||||
|
||||
## Overview
|
||||
|
||||
Hindsight's performance is optimized across three key operations:
|
||||
|
||||
- **Retain (Ingestion)**: Batch processing with async operations for large-scale memory storage
|
||||
- **Recall (Search)**: Sub-second semantic search with configurable thinking budgets
|
||||
- **Reflect (Reasoning)**: Disposition-aware answer generation with controllable compute
|
||||
|
||||
## Design Philosophy: Optimized for Fast Reads
|
||||
|
||||
Hindsight is **architected from the ground up to prioritize read performance over write performance**. This design decision reflects the typical usage pattern of memory systems: memories are written once but read many times.
|
||||
|
||||
The system makes deliberate trade-offs to ensure **sub-second recall operations**:
|
||||
|
||||
- **Pre-computed embeddings**: All memory embeddings are generated and indexed during retention
|
||||
- **Optimized vector search**: HNSW indexes enable fast approximate nearest neighbor search
|
||||
- **Fact extraction at write time**: Complex LLM-based fact extraction happens during retention, not retrieval
|
||||
- **Structured memory graphs**: Relationships and temporal information are resolved upfront
|
||||
|
||||
This means **Recall (search) operations are blazingly fast** because all the heavy lifting has already been done.
|
||||
|
||||
### Performance Comparison
|
||||
|
||||
| Operation | Typical Latency | Primary Bottleneck | Optimization Strategy |
|
||||
|-----------|----------------|-------------------|----------------------------------|
|
||||
| **Recall** | 100-600ms | Re-ranker (on CPU) | Use GPU for re-ranking, or reduce budget |
|
||||
| **Reflect** | 800-3000ms | LLM generation | Use faster LLM |
|
||||
| **Retain** | 500ms-2000ms per batch | **LLM fact extraction** | Use high-throughput LLM provider |
|
||||
|
||||
Hindsight is designed to ensure your **application's read path (recall/reflect) is always fast**, even if it means spending more time upfront during writes. This is the right trade-off for memory systems where:
|
||||
|
||||
- Memories are retained in background processes or during low-traffic periods
|
||||
- Memories are queried frequently in user-facing, latency-sensitive contexts
|
||||
- The ratio of reads to writes is high (typically 10:1 or higher)
|
||||
|
||||
---
|
||||
|
||||
## Retain Performance
|
||||
|
||||
**Retain (write) operations are inherently slower** because they involve LLM-based fact extraction, entity recognition, temporal reasoning, relationship mapping, and embedding generation. **The LLM is the primary bottleneck for write latency.**
|
||||
|
||||
### Hindsight Doesn't Need a Smart Model
|
||||
|
||||
The fact extraction process is structured and well-defined, so smaller, faster models work extremely well. Our recommended model is `gpt-oss-20b` (available via Groq and other providers).
|
||||
|
||||
To maximize retention throughput:
|
||||
|
||||
1. **Use high-throughput LLM providers**: Choose providers with high requests-per-minute (RPM) limits and low latency
|
||||
- **Fast**: [Groq](https://groq.com) with `gpt-oss-20b` or other openai-oss models, self-hosted models on GPU clusters (vLLM, TGI)
|
||||
- **Slow**: Standard cloud LLM providers with rate limits
|
||||
|
||||
2. **Batch your operations**: Group related content into batch requests. The only limit is the HTTP payload size — Hindsight automatically splits large batches into smaller, optimized chunks under the hood, so you don't have to worry about it.
|
||||
|
||||
3. **Use async mode for large datasets**: Queue operations in the background
|
||||
|
||||
4. **Parallel processing**: For very large datasets, use multiple concurrent retention requests with different `document_id` values
|
||||
|
||||
### Throughput
|
||||
|
||||
Factors affecting throughput:
|
||||
- Document size and complexity
|
||||
- LLM provider rate limits (for fact extraction)
|
||||
- Database write performance
|
||||
- Available CPU/memory resources
|
||||
|
||||
---
|
||||
|
||||
## Recall Performance
|
||||
|
||||
### Budget
|
||||
|
||||
The `budget` parameter controls the search depth and quality. Choose based on query complexity — comprehensive questions that need thorough analysis benefit from higher budgets:
|
||||
|
||||
| Budget | Use Case |
|
||||
|--------|----------|
|
||||
| `low` | Quick lookups, real-time chat |
|
||||
| `mid` | Standard queries, balanced performance |
|
||||
| `high` | Comprehensive questions, thorough analysis |
|
||||
|
||||
### Optimization
|
||||
|
||||
1. **Appropriate budgets**: Use lower budgets for simple queries, higher for comprehensive reasoning
|
||||
2. **Limit result tokens**: Set `max_tokens` to control response size (default: 4096)
|
||||
3. **Include chunks**: Use `include_chunks` to retrieve the raw text that generated memories when you need additional context
|
||||
|
||||
### Database Performance
|
||||
|
||||
Hindsight uses PostgreSQL with pgvector for efficient vector search:
|
||||
|
||||
- **Index type**: HNSW for approximate nearest neighbor search
|
||||
- **Typical query time**: 10-50ms for vector search on 100K+ facts
|
||||
- **Scalability**: Tested with millions of facts per bank
|
||||
|
||||
## Reflect Performance
|
||||
|
||||
### Performance Characteristics
|
||||
|
||||
| Component | Latency | Description |
|
||||
|-----------|----------------|-------------|
|
||||
| Memory search | 100-600ms | Based on budget (low/mid/high) |
|
||||
| LLM generation | 500-2000ms | Depends on provider and response length |
|
||||
| **Total** | **600-2600ms** | Typical end-to-end latency |
|
||||
|
||||
### Optimization Strategies
|
||||
|
||||
1. **Budget selection**: Use lower budgets when context is sufficient
|
||||
2. **Context provision**: Provide relevant `context` to reduce recall requirements and steer towards more focused answers
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Operations
|
||||
- **Use appropriate budgets**: Don't over-provision for simple queries; use higher budgets for comprehensive reasoning
|
||||
- **Batch retain operations**: Group related content together for better efficiency
|
||||
- **Cache frequent queries**: Cache at the application level for repeated queries
|
||||
- **Profile with trace**: Use the `trace` parameter to identify slow operations
|
||||
|
||||
### Scaling
|
||||
- **Horizontal scaling**: Deploy multiple API instances behind a load balancer with shared PostgreSQL
|
||||
- **Concurrency**: 100+ simultaneous requests supported; memory search scales with CPU cores
|
||||
- **LLM rate limits**: Distribute load across multiple API keys/providers (typically 60-500 RPM per key)
|
||||
|
||||
### Cost Optimization
|
||||
- **Use efficient models**: `gpt-oss-20b` via Groq for retain — Hindsight doesn't need frontier models
|
||||
- **Control token budgets**: Limit `max_tokens` for recall, use lower budgets when possible
|
||||
- **Optimize chunks**: Larger chunks (1000-2000 tokens) are more efficient than many small ones
|
||||
|
||||
### Monitoring
|
||||
- **Prometheus metrics**: Available at `/metrics` — track latency percentiles, throughput, and error rates
|
||||
- **Key metrics**: `hindsight_recall_duration_seconds`, `hindsight_reflect_duration_seconds`, `hindsight_retain_items_total`
|
||||
@@ -0,0 +1,110 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
# RAG vs Memory
|
||||
|
||||
Traditional RAG (Retrieval-Augmented Generation) retrieves documents similar to a query. Hindsight provides structured memory with temporal reasoning, entity understanding, and belief formation.
|
||||
|
||||
## Capability Comparison
|
||||
|
||||
| Capability | RAG | Hindsight |
|
||||
|------------|-----|-----------|
|
||||
| **Search strategy** | Semantic similarity only | Semantic + keyword + graph + temporal |
|
||||
| **Multi-hop reasoning** | Limited to retrieved chunks | Graph traversal across entity relationships |
|
||||
| **Temporal queries** | Keyword matching ("spring") | Date parsing and range filtering |
|
||||
| **Entity understanding** | None | Entity resolution, co-occurrence tracking |
|
||||
| **Knowledge consolidation** | Stateless | Mental models that synthesize and evolve |
|
||||
| **Disposition** | None | 3 traits (skepticism, literalism, empathy) influence interpretation |
|
||||
|
||||
## Architecture Comparison
|
||||
|
||||
### RAG
|
||||
|
||||
| Step | Operation |
|
||||
|------|-----------|
|
||||
| 1 | Embed query |
|
||||
| 2 | Vector similarity search |
|
||||
| 3 | Return top-k chunks |
|
||||
| 4 | Generate response |
|
||||
|
||||
Single retrieval strategy. No state between queries.
|
||||
|
||||
### Hindsight
|
||||
|
||||
| Step | Operation |
|
||||
|------|-----------|
|
||||
| 1 | Parse query (extract temporal expressions, entities) |
|
||||
| 2 | Execute 4 parallel retrievals: semantic, BM25, graph, temporal |
|
||||
| 3 | Fuse results with RRF |
|
||||
| 4 | Rerank with cross-encoder |
|
||||
| 5 | Apply disposition traits |
|
||||
| 6 | Generate response |
|
||||
|
||||
Multiple retrieval strategies. Persistent state across sessions.
|
||||
|
||||
## Example Scenarios
|
||||
|
||||
### Multi-Hop Reasoning
|
||||
|
||||
**Stored facts:**
|
||||
- "Alice is the tech lead on Project Atlas"
|
||||
- "Project Atlas uses Kubernetes"
|
||||
- "Kubernetes cluster had an outage Tuesday"
|
||||
|
||||
**Query:** "Was Alice affected by recent issues?"
|
||||
|
||||
| System | Result |
|
||||
|--------|--------|
|
||||
| RAG | Retrieves facts about Alice only (no semantic similarity to "issues") |
|
||||
| Hindsight | Traverses Alice → Project Atlas → Kubernetes → outage via entity links |
|
||||
|
||||
### Temporal Queries
|
||||
|
||||
**Stored facts with timestamps:**
|
||||
- March: "Alice started microservices migration"
|
||||
- April: "Alice completed auth service"
|
||||
- October: "Alice focusing on performance"
|
||||
|
||||
**Query:** "What did Alice do last spring?"
|
||||
|
||||
| System | Result |
|
||||
|--------|--------|
|
||||
| RAG | Returns all Alice facts regardless of date |
|
||||
| Hindsight | Parses "last spring" → March-May, filters to that range |
|
||||
|
||||
### Entity Understanding
|
||||
|
||||
**Stored facts about a user across sessions:**
|
||||
- "Pro subscription"
|
||||
- "Mobile app crashes in settings"
|
||||
- "Switched to annual billing"
|
||||
- "Desktop app working fine"
|
||||
|
||||
**Query:** "What do you know about my account?"
|
||||
|
||||
| System | Result |
|
||||
|--------|--------|
|
||||
| RAG | Lists disconnected facts |
|
||||
| Hindsight | Returns connected facts via entity graph: subscription status, billing, known issues |
|
||||
|
||||
### Knowledge Evolution
|
||||
|
||||
**Week 1:** User struggles with async Python, succeeds with threads
|
||||
**Week 3:** User asks about asyncio, implements async database calls
|
||||
|
||||
| System | Behavior |
|
||||
|--------|----------|
|
||||
| RAG | No memory of progression |
|
||||
| Hindsight | Consolidates mental model "user prefers sync" → refines to "user growing comfortable with async" |
|
||||
|
||||
## When to Use Each
|
||||
|
||||
| Use Case | Recommended |
|
||||
|----------|-------------|
|
||||
| Document Q&A over static corpus | RAG |
|
||||
| Search with no temporal requirements | RAG |
|
||||
| AI assistants with persistent memory | Hindsight |
|
||||
| Applications requiring entity tracking | Hindsight |
|
||||
| Systems needing consistent disposition | Hindsight |
|
||||
| Temporal queries ("last month", "in 2023") | Hindsight |
|
||||
@@ -0,0 +1,244 @@
|
||||
|
||||
|
||||
# Reflect: Agentic Reasoning with Disposition
|
||||
|
||||
When you call `reflect()`, Hindsight runs an **agentic loop** that autonomously gathers evidence and reasons through the lens of the bank's disposition to generate contextual responses.
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph agent["Reflect Agent Loop"]
|
||||
A[Query] --> B{Need more info?}
|
||||
B -->|Yes| C[Call Tools]
|
||||
C --> D[search_mental_models]
|
||||
C --> E[search_observations]
|
||||
C --> F[recall]
|
||||
C --> G[expand]
|
||||
D --> B
|
||||
E --> B
|
||||
F --> B
|
||||
G --> B
|
||||
B -->|No| H[Generate Response]
|
||||
end
|
||||
H --> I[Response + Citations]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||
Unlike simple retrieval, reflect is an **agentic system** that:
|
||||
|
||||
1. **Autonomously gathers evidence** — The agent decides what information it needs and calls appropriate tools
|
||||
2. **Uses hierarchical retrieval** — Checks mental models first, then observations, then raw facts
|
||||
3. **Applies disposition** — Shapes reasoning based on the bank's personality traits
|
||||
4. **Enforces directives** — Hard rules that must be followed in all responses
|
||||
5. **Cites sources** — Returns which memories and observations were used
|
||||
|
||||
### The Agentic Loop
|
||||
|
||||
The reflect agent runs in a loop with access to these tools:
|
||||
|
||||
| Tool | Purpose | Priority |
|
||||
|------|---------|----------|
|
||||
| `search_mental_models` | User-curated summaries | Highest (check first) |
|
||||
| `search_observations` | Consolidated knowledge | High |
|
||||
| `recall` | Raw facts (ground truth) | Fallback |
|
||||
| `expand` | Get more context for a memory | As needed |
|
||||
| `done` | Complete with final answer | When ready |
|
||||
|
||||
The agent:
|
||||
- **Must gather evidence** before answering (guardrail prevents empty responses)
|
||||
- **Runs up to 10 iterations** to find relevant information
|
||||
- **Validates citations** — only IDs that were actually retrieved can be cited
|
||||
|
||||
### Hierarchical Retrieval Strategy
|
||||
|
||||
The agent uses a smart retrieval hierarchy:
|
||||
|
||||
1. **[Mental Models](/developer/api/mental-models)** — User-curated summaries you've pre-computed for common queries
|
||||
2. **[Observations](/developer/observations)** — Consolidated knowledge with freshness awareness
|
||||
3. **Raw Facts** — Ground truth for verification when observations are stale
|
||||
|
||||
**Mental models** are saved reflect responses that you create for frequently asked questions. They're checked first because they represent explicitly curated knowledge. See the [Mental Models API](/developer/api/mental-models) for how to create and manage them.
|
||||
|
||||
If an observation is marked as **stale**, the agent automatically verifies it against current facts.
|
||||
|
||||
---
|
||||
|
||||
## Why Reflect?
|
||||
|
||||
Most AI systems can retrieve facts, but they can't **reason** about them in a consistent way.
|
||||
|
||||
### The Problem
|
||||
|
||||
Without reflect:
|
||||
- **No consistent character**: Same question gets different answers each time
|
||||
- **No knowledge synthesis**: System never connects related facts
|
||||
- **No reasoning context**: Responses don't reflect accumulated knowledge
|
||||
- **Generic responses**: Every AI sounds the same
|
||||
|
||||
### The Value
|
||||
|
||||
With reflect:
|
||||
- **Consistent character**: A "detail-oriented, cautious" bank emphasizes risks and thorough planning
|
||||
- **Evolving knowledge**: Observations strengthen and adapt as evidence accumulates
|
||||
- **Contextual reasoning**: "Based on what I know about your team's remote work success..."
|
||||
- **Differentiated behavior**: Support bots sound diplomatic, code reviewers sound direct
|
||||
|
||||
### When to Use Reflect
|
||||
|
||||
| Use `recall()` when... | Use `reflect()` when... |
|
||||
|------------------------|-------------------------|
|
||||
| You need raw facts | You need reasoned interpretation |
|
||||
| You're building your own reasoning | You want disposition-consistent responses |
|
||||
| You need maximum control | You want the bank to "think" for itself |
|
||||
| Simple fact lookup | Forming recommendations |
|
||||
|
||||
**Example:**
|
||||
- `recall("Alice")` → Returns all Alice facts and relevant mental models
|
||||
- `reflect("Should we hire Alice?")` → Agent gathers evidence about Alice, reasons about fit, returns answer with citations
|
||||
|
||||
---
|
||||
|
||||
## Disposition Traits
|
||||
|
||||
When you create a memory bank, you can configure its disposition using three traits. These traits influence how the bank interprets information and reasons during `reflect()`:
|
||||
|
||||
| Trait | Scale | Low (1) | High (5) |
|
||||
|-------|-------|---------|----------|
|
||||
| **Skepticism** | 1-5 | Trusting, accepts information at face value | Skeptical, questions and doubts claims |
|
||||
| **Literalism** | 1-5 | Flexible interpretation, reads between the lines | Literal interpretation, takes things at face value |
|
||||
| **Empathy** | 1-5 | Detached, focuses on facts | Empathetic, considers emotional context |
|
||||
|
||||
### Mission: Natural Language Identity
|
||||
|
||||
Beyond numeric traits, you can provide a natural language **mission** that describes the bank's identity:
|
||||
|
||||
```python
|
||||
client.create_bank(
|
||||
bank_id="architect-bank",
|
||||
mission="You're a senior software architect - keep track of system designs, "
|
||||
"technology decisions, and architectural patterns. Prefer simplicity over cutting-edge.",
|
||||
disposition={
|
||||
"skepticism": 4, # Questions new technologies
|
||||
"literalism": 4, # Focuses on concrete specs
|
||||
"empathy": 2 # Prioritizes technical facts
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
The mission tells Hindsight what knowledge to prioritize and shapes how disposition traits are applied:
|
||||
- "keep track of system designs" → focuses consolidation on architectural decisions
|
||||
- "prefer simplicity over cutting-edge" + high skepticism → questions complex solutions
|
||||
- Explicit guidance → consistent memory focus across conversations
|
||||
|
||||
---
|
||||
|
||||
## Disposition Shapes Reasoning
|
||||
|
||||
Two banks with different dispositions, given identical facts about remote work:
|
||||
|
||||
**Bank A** (low skepticism, high empathy):
|
||||
> "Remote work enables flexibility and work-life balance. The team seems happier and more productive when they can choose their environment."
|
||||
|
||||
**Bank B** (high skepticism, low empathy):
|
||||
> "Remote work claims need verification. What are the actual productivity metrics? The anecdotal benefits may not translate to measurable outcomes."
|
||||
|
||||
**Same facts → Different conclusions** because disposition shapes interpretation.
|
||||
|
||||
---
|
||||
|
||||
## Disposition Presets by Use Case
|
||||
|
||||
Different use cases benefit from different disposition configurations:
|
||||
|
||||
| Use Case | Recommended Traits | Why |
|
||||
|----------|-------------------|-----|
|
||||
| **Customer Support** | skepticism: 2, literalism: 2, empathy: 5 | Trusting, flexible, understanding |
|
||||
| **Code Review** | skepticism: 4, literalism: 5, empathy: 2 | Questions assumptions, precise, direct |
|
||||
| **Legal Analysis** | skepticism: 5, literalism: 5, empathy: 2 | Highly skeptical, exact interpretation |
|
||||
| **Therapist/Coach** | skepticism: 2, literalism: 2, empathy: 5 | Supportive, reads between lines |
|
||||
| **Research Assistant** | skepticism: 4, literalism: 3, empathy: 3 | Questions claims, balanced interpretation |
|
||||
|
||||
---
|
||||
|
||||
## Directives: Hard Rules
|
||||
|
||||
While disposition traits *influence* reasoning style, **directives** are hard rules that the agent *must* follow. Directives are injected into the prompt and enforced in every response.
|
||||
|
||||
### When to Use Directives
|
||||
|
||||
Use directives for constraints that must never be violated:
|
||||
|
||||
- **Compliance rules**: "Never recommend specific stocks or financial products"
|
||||
- **Privacy constraints**: "Never share personal data with third parties"
|
||||
- **Style requirements**: "Always respond in formal English"
|
||||
- **Domain guardrails**: "Always cite sources when making factual claims"
|
||||
|
||||
### Directives vs Disposition
|
||||
|
||||
| Aspect | Disposition | Directives |
|
||||
|--------|-------------|------------|
|
||||
| **Nature** | Soft influence | Hard rules |
|
||||
| **Effect** | Shapes interpretation and tone | Must be followed exactly |
|
||||
| **Violation** | Acceptable (it's a tendency) | Not acceptable |
|
||||
| **Example** | High skepticism → questions claims | "Never make medical diagnoses" |
|
||||
|
||||
:::tip
|
||||
Use disposition for personality and character. Use directives for compliance and guardrails.
|
||||
See [Memory Banks: Directives](/developer/api/memory-banks#directives) for how to create and manage directives.
|
||||
|
||||
---
|
||||
|
||||
## What You Get from Reflect
|
||||
|
||||
When you call `reflect()`:
|
||||
|
||||
**Returns:**
|
||||
- **Response text** — Disposition-influenced answer from the agent
|
||||
- **based_on** — Evidence used: memories, mental models, and directives that grounded the response
|
||||
- **trace** — Tool calls, LLM calls, and observations accessed (when `include.tool_calls=True`)
|
||||
- **structured_output** — Parsed response if `response_schema` was provided
|
||||
- **usage** — Token usage metrics
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"text": "Based on Alice's ML expertise and her work at Google, she'd be an excellent fit for the research team lead position...",
|
||||
"based_on": {
|
||||
"memories": [
|
||||
{"id": "mem-123", "text": "Alice has 5 years of ML experience", "type": "world"},
|
||||
{"id": "mem-456", "text": "Alice worked at Google on search ranking", "type": "experience"}
|
||||
],
|
||||
"mental_models": [],
|
||||
"directives": [
|
||||
{"id": "dir-001", "name": "Formal Language", "rules": ["Always respond in formal English"]}
|
||||
]
|
||||
},
|
||||
"usage": {"input_tokens": 1500, "output_tokens": 500, "total_tokens": 2000}
|
||||
}
|
||||
```
|
||||
|
||||
The agent automatically gathers evidence, validates citations, and generates a grounded response.
|
||||
|
||||
---
|
||||
|
||||
## Why Disposition Matters
|
||||
|
||||
Without disposition, all AI assistants sound the same. With disposition:
|
||||
|
||||
- **Customer support bots** can be diplomatic and empathetic
|
||||
- **Code review assistants** can be direct and thorough
|
||||
- **Creative assistants** can be open to unconventional ideas
|
||||
- **Risk analysts** can be appropriately cautious
|
||||
|
||||
Disposition creates **consistent character** across conversations while observations **evolve with evidence**.
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [**Observations**](./observations) — How knowledge is consolidated
|
||||
- [**Retain**](./retain) — How rich facts are stored
|
||||
- [**Recall**](./retrieval) — How multi-strategy search works
|
||||
- [**Reflect API**](./api/reflect) — Code examples and parameters
|
||||
@@ -0,0 +1,199 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
# Retain: How Hindsight Stores Memories
|
||||
|
||||
When you call `retain()`, Hindsight transforms conversations and documents into structured, searchable memories that preserve meaning and context.
|
||||
|
||||
## What Retain Does
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[Your Content] --> B[Extract Facts]
|
||||
B --> C[Identify Entities]
|
||||
C --> D[Build Connections]
|
||||
D --> E[Memory Bank]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rich Fact Extraction
|
||||
|
||||
Hindsight doesn't just store what was said — it captures **why**, **how**, and **what it means**.
|
||||
|
||||
### What Gets Captured
|
||||
|
||||
When you retain "Alice joined Google last spring and was thrilled about the research opportunities", Hindsight extracts:
|
||||
|
||||
**The core facts:**
|
||||
- Alice joined Google
|
||||
- This happened last spring
|
||||
|
||||
**The emotions and meaning:**
|
||||
- She was thrilled
|
||||
- It represented an important opportunity
|
||||
|
||||
**The reasoning:**
|
||||
- She chose it for the research opportunities
|
||||
|
||||
This rich extraction means you can later ask "Why did Alice join Google?" and get a meaningful answer, not just "she joined Google."
|
||||
|
||||
### Preserving Context
|
||||
|
||||
Traditional systems fragment information:
|
||||
- "Bob suggested Summer Vibes"
|
||||
- "Alice wanted something unique"
|
||||
- "They chose Beach Beats"
|
||||
|
||||
Hindsight preserves the full narrative:
|
||||
- "Alice and Bob discussed naming their summer party playlist. Bob suggested 'Summer Vibes' because it's catchy, but Alice wanted something unique. They ultimately decided on 'Beach Beats' for its playful tone."
|
||||
|
||||
This means search results include the full context, not disconnected fragments.
|
||||
|
||||
---
|
||||
|
||||
## Two Types of Facts
|
||||
|
||||
Hindsight distinguishes between **world** facts (about others) and **experience** (conversations and events):
|
||||
|
||||
| Type | Description | Example |
|
||||
|-----------------|-----------------------------------|---------|
|
||||
| **world** | Facts about people, places, things | "Alice works at Google" |
|
||||
| **experience** | Conversations and events | "I recommended Python to Alice" |
|
||||
|
||||
|
||||
**Note:** Observations are consolidated automatically in the background after `retain()` operations complete. This consolidation process synthesizes patterns from new facts into the bank's knowledge base.
|
||||
|
||||
---
|
||||
|
||||
## Entity Recognition
|
||||
|
||||
Hindsight automatically identifies and tracks **entities** — the people, organizations, and concepts that matter.
|
||||
|
||||
### What Gets Recognized
|
||||
|
||||
- **People:** "Alice", "Dr. Smith", "Bob Chen"
|
||||
- **Organizations:** "Google", "MIT", "OpenAI"
|
||||
- **Places:** "Paris", "Central Park", "California"
|
||||
- **Products & Concepts:** "Python", "TensorFlow", "machine learning"
|
||||
|
||||
### Entity Resolution
|
||||
|
||||
The same entity mentioned different ways gets unified:
|
||||
- "Alice" + "Alice Chen" + "Alice C." → one person
|
||||
- "Bob" + "Robert Chen" → one person (nickname resolution)
|
||||
|
||||
**Why it matters:** You can ask "What do I know about Alice?" and get everything, even if she was mentioned as "Alice Chen" in some conversations.
|
||||
|
||||
### Context-Aware Disambiguation
|
||||
|
||||
If "Alice" appears with "Google" and "Stanford" multiple times, a new "Alice" mentioning those is likely the same person. Hindsight uses co-occurrence patterns to disambiguate common names.
|
||||
|
||||
---
|
||||
|
||||
## Building Connections
|
||||
|
||||
Memories aren't isolated — Hindsight creates a **knowledge graph** with four types of connections:
|
||||
|
||||
### Entity Connections
|
||||
|
||||
All facts mentioning the same entity are linked together.
|
||||
|
||||
**Enables:** "Tell me everything about Alice" → retrieves all Alice-related facts
|
||||
|
||||
### Time-Based Connections
|
||||
|
||||
Facts close in time are connected, with stronger links for closer dates.
|
||||
|
||||
**Enables:** "What else happened around then?" → finds contextually related events
|
||||
|
||||
### Meaning-Based Connections
|
||||
|
||||
Semantically similar facts are linked, even if they use different words.
|
||||
|
||||
**Enables:** "Tell me about similar topics" → finds thematically related information
|
||||
|
||||
### Causal Connections
|
||||
|
||||
Cause-effect relationships are explicitly tracked.
|
||||
|
||||
**Enables:** "Why did this happen?" → trace reasoning chains
|
||||
**Example:** "Alice felt burned out" ← caused by ← "She worked 80-hour weeks"
|
||||
|
||||
---
|
||||
|
||||
## Understanding Time
|
||||
|
||||
Hindsight tracks **two temporal dimensions**:
|
||||
|
||||
### When It Happened
|
||||
|
||||
For events (meetings, trips, milestones), Hindsight records when they occurred.
|
||||
- "Alice got married in June 2024" → occurred in June 2024
|
||||
|
||||
For general facts (preferences, characteristics), there's no specific occurrence time.
|
||||
- "Alice prefers Python" → ongoing preference
|
||||
|
||||
### When You Learned It
|
||||
|
||||
Hindsight also tracks when you told it each fact.
|
||||
|
||||
**Why both?**
|
||||
|
||||
Imagine in January 2025, someone tells you "Alice got married in June 2024":
|
||||
- **Historical queries** work: "What did Alice do in 2024?" → finds the marriage
|
||||
- **Recency ranking** works: Recent mentions get priority in search
|
||||
- **Temporal reasoning** works: "What happened before her marriage?" → finds earlier events
|
||||
|
||||
Without this distinction, old information would either be unsearchable by date or treated as irrelevant.
|
||||
|
||||
---
|
||||
|
||||
## Tagging Memories
|
||||
|
||||
Tags enable visibility scoping—useful when one memory bank serves multiple users but each should only see relevant memories.
|
||||
|
||||
- **Item tags**: Tag individual memories with specific scopes
|
||||
- **Document tags**: Apply tags to all items in a batch
|
||||
- **Tag filtering**: Filter during recall/reflect by tags
|
||||
|
||||
See [Retain API](./api/retain) for code examples and [Recall API](./api/recall) for filtering options.
|
||||
|
||||
---
|
||||
|
||||
## What You Get
|
||||
|
||||
After `retain()` completes:
|
||||
|
||||
- **Structured facts** that preserve meaning, emotions, and reasoning
|
||||
- **Unified entities** that resolve different name variations
|
||||
- **Knowledge graph** with entity, temporal, semantic, and causal links
|
||||
- **Temporal grounding** for both historical and recency-based queries
|
||||
- **Optional tags** for filtering during recall
|
||||
|
||||
All stored in your isolated **memory bank**, ready for `recall()` and `reflect()`.
|
||||
|
||||
---
|
||||
|
||||
## Observation Consolidation
|
||||
|
||||
After `retain()` completes, Hindsight automatically triggers **observation consolidation** in the background. This process:
|
||||
|
||||
1. Analyzes new facts against existing observations
|
||||
2. Creates new observations when patterns emerge
|
||||
3. Refines existing observations with new evidence
|
||||
4. Tracks which facts support each observation
|
||||
|
||||
This happens asynchronously — your `retain()` call returns immediately while consolidation runs in the background.
|
||||
|
||||
See [Observations](./observations) for details on how consolidation works.
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [**Observations**](./observations) — How knowledge is consolidated after retain
|
||||
- [**Recall**](./retrieval) — How multi-strategy search retrieves relevant memories
|
||||
- [**Reflect**](./reflect) — How the agentic loop uses observations
|
||||
- [**Retain API**](./api/retain) — Code examples and parameters
|
||||
@@ -0,0 +1,226 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
# Recall: How Hindsight Retrieves Memories
|
||||
|
||||
When you call `recall()`, Hindsight uses multiple search strategies in parallel to find the most relevant memories, regardless of how you phrase your query.
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
Q[Query] --> S[Semantic]
|
||||
Q --> K[Keyword]
|
||||
Q --> G[Graph]
|
||||
Q --> T[Temporal]
|
||||
|
||||
S --> RRF[RRF Fusion]
|
||||
K --> RRF
|
||||
G --> RRF
|
||||
T --> RRF
|
||||
|
||||
RRF --> CE[Cross-Encoder]
|
||||
CE --> R[Results]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## The Challenge of Memory Recall
|
||||
|
||||
Different queries need different search approaches:
|
||||
|
||||
- **"Alice works at Google"** → needs exact name matching
|
||||
- **"Where does Alice work?"** → needs semantic understanding
|
||||
- **"What did Alice do last spring?"** → needs temporal reasoning
|
||||
- **"Why did Alice leave?"** → needs causal relationship tracing
|
||||
|
||||
No single search method handles all these well. Hindsight solves this with **TEMPR** — four complementary strategies that run in parallel.
|
||||
|
||||
---
|
||||
|
||||
## Four Search Strategies
|
||||
|
||||
### Semantic Search
|
||||
|
||||
**What it does:** Understands the *meaning* behind words, not just the words themselves.
|
||||
|
||||
**Best for:**
|
||||
- Conceptual matches: "Alice's job" → "Alice works as a software engineer"
|
||||
- Paraphrasing: "Bob's expertise" → "Bob specializes in machine learning"
|
||||
- Synonyms: "meeting" matches "conference", "discussion", "gathering"
|
||||
|
||||
**Why it matters:** You can ask questions naturally without matching exact keywords.
|
||||
|
||||
---
|
||||
|
||||
### Keyword Search
|
||||
|
||||
**What it does:** Finds exact terms and names, even when they're spelled uniquely.
|
||||
|
||||
**Best for:**
|
||||
- Proper nouns: "Google", "Alice Chen", "MIT"
|
||||
- Technical terms: "PostgreSQL", "HNSW", "TensorFlow"
|
||||
- Unique identifiers: URLs, product names, specific phrases
|
||||
|
||||
**Why it matters:** Ensures you never miss results that mention specific names or terms, even if they're semantically distant from your query.
|
||||
|
||||
---
|
||||
|
||||
### Graph Traversal
|
||||
|
||||
**What it does:** Follows connections between entities to find indirectly related information.
|
||||
|
||||
**Best for:**
|
||||
- Indirect relationships: "What does Alice do?" → Alice → Google → Google's products
|
||||
- Entity exploration: "Bob's colleagues" → Bob → co-workers → shared projects
|
||||
- Multi-hop reasoning: "Alice's team's achievements"
|
||||
|
||||
**Why it matters:** Retrieves facts that aren't semantically or lexically similar but are **structurally connected** through the knowledge graph.
|
||||
|
||||
**Example:** Even if Alice and her manager are never mentioned together, graph traversal can find the manager through shared projects or team relationships.
|
||||
|
||||
---
|
||||
|
||||
### Temporal Search
|
||||
|
||||
**What it does:** Understands time expressions and filters by when events occurred.
|
||||
|
||||
**Best for:**
|
||||
- Historical queries: "What did Alice do in 2023?"
|
||||
- Time ranges: "What happened last spring?"
|
||||
- Relative time: "What did Bob work on last year?"
|
||||
- Before/after: "What happened before Alice joined Google?"
|
||||
|
||||
**How it works:** Combines semantic understanding with time filtering to find events within specific periods.
|
||||
|
||||
**Why it matters:** Enables precise historical queries without losing old information.
|
||||
|
||||
---
|
||||
|
||||
## Result Fusion
|
||||
|
||||
After the four strategies run, results are **fused together**:
|
||||
|
||||
- Memories appearing in **multiple strategies** rank higher (consensus)
|
||||
- **Rank matters more than score** (robust across different scoring systems)
|
||||
- Final results are **re-ranked** using a neural model that considers query-memory interaction
|
||||
|
||||
**Why fusion matters:** A fact that's both semantically similar AND mentions the right entity will rank higher than one that's only semantically similar.
|
||||
|
||||
---
|
||||
|
||||
## Why Multiple Strategies?
|
||||
|
||||
Consider the query: **"What did Alice say about Python last spring?"**
|
||||
|
||||
- **Semantic** finds facts about Alice's views on programming
|
||||
- **Keyword** ensures "Python" is actually mentioned
|
||||
- **Graph** connects Alice → programming languages → related entities
|
||||
- **Temporal** filters to "last spring" timeframe
|
||||
|
||||
The **fusion** of all four gives you exactly what you're looking for, even though no single strategy would suffice.
|
||||
|
||||
---
|
||||
|
||||
## Token Budget Management
|
||||
|
||||
Hindsight is built for AI agents, not humans. Traditional search systems return "top-k" results, but agents don't think in terms of result counts—they think in tokens. An agent's context window is measured in tokens, and that's exactly how Hindsight measures results.
|
||||
|
||||
**How it works:**
|
||||
- Top-ranked memories selected first
|
||||
- Stops when token budget is exhausted
|
||||
- You specify context budget, Hindsight fills it with the most relevant memories
|
||||
|
||||
**Parameters you control:**
|
||||
- `max_tokens`: How much memory content to return (default: 4096 tokens)
|
||||
- `budget`: Search depth level (low, mid, high)
|
||||
- `types`: Filter by world, experience, observation, or all
|
||||
- `tags`: Filter memories by visibility tags
|
||||
- `tags_match`: How to match tags (see [Recall API](./api/recall) for all options)
|
||||
|
||||
### Expanding Context: Chunks
|
||||
|
||||
Memories are distilled facts—concise but sometimes missing nuance. When your agent needs deeper context, you can optionally retrieve the source material:
|
||||
|
||||
**Chunks** return the raw text that generated each memory—useful when the distilled fact loses important nuance:
|
||||
|
||||
```
|
||||
Memory: "Alice prefers Python over JavaScript"
|
||||
Chunk: "Alice mentioned she prefers Python over JavaScript, mainly because
|
||||
of its data science ecosystem, though she admits JS is better for
|
||||
frontend work and she's been learning TypeScript lately."
|
||||
```
|
||||
|
||||
Use `include_chunks=True` with `max_chunk_tokens` to control the token budget for chunks. This is useful when generating responses that need verbatim quotes or when context matters (e.g., "What exactly did Alice say about the project?").
|
||||
|
||||
---
|
||||
|
||||
## Tuning Recall: Quality vs Latency
|
||||
|
||||
Different use cases require different trade-offs between **recall quality** and **response speed**. Two parameters control this:
|
||||
|
||||
### Budget: Search Depth
|
||||
|
||||
Controls how thoroughly Hindsight explores the memory bank—affecting graph traversal depth, candidate pool size, and cross-encoder re-ranking:
|
||||
|
||||
| Budget | Best For | Trade-off |
|
||||
|--------|----------|-----------|
|
||||
| **low** | Quick lookups, simple queries | Fast, may miss indirect connections |
|
||||
| **mid** | Most queries, balanced | Good coverage, reasonable speed |
|
||||
| **high** | Complex queries requiring deep exploration | Thorough, slower |
|
||||
|
||||
**Example:** "What did Alice's manager's team work on?" benefits from high budget to traverse multiple hops (Alice → manager → team → projects) and evaluate more candidates.
|
||||
|
||||
### Max Tokens: Context Window Size
|
||||
|
||||
Controls how much memory content to return:
|
||||
|
||||
| Max Tokens | ~Pages of Text | Best For | Trade-off |
|
||||
|------------|----------------|----------|-----------|
|
||||
| **2048** | ~2 pages | Focused answers, fast LLM | Fewer memories, faster |
|
||||
| **4096** (default) | ~4 pages | Balanced context | Good coverage, standard |
|
||||
| **8192** | ~8 pages | Comprehensive context | More memories, slower LLM |
|
||||
|
||||
**Example:** "Summarize everything about Alice" benefits from higher max_tokens to include more facts.
|
||||
|
||||
### Two Independent Dimensions
|
||||
|
||||
Budget and max_tokens control different aspects of recall:
|
||||
|
||||
| Parameter | What it controls | Latency impact | Example |
|
||||
|-----------|------------------|----------------|---------|
|
||||
| **Budget** | How thoroughly to explore memories | Search time | High budget finds Alice → manager → team → projects |
|
||||
| **Max Tokens** | How much context to return | LLM processing time | High tokens returns more memories to the agent |
|
||||
|
||||
**They're independent.** Common combinations:
|
||||
|
||||
| Budget | Max Tokens | Use Case |
|
||||
|--------|------------|----------|
|
||||
| high | low | Deep search, return only the best results |
|
||||
| low | high | Quick search, return everything found |
|
||||
| high | high | Comprehensive research queries |
|
||||
| low | low | Fast chatbot responses |
|
||||
|
||||
### Recommended Configurations
|
||||
|
||||
| Use Case | Budget | Max Tokens | Why |
|
||||
|----------|--------|------------|-----|
|
||||
| **Chatbot replies** | low | 2048 | Fast responses, focused context |
|
||||
| **Document Q&A** | mid | 4096 | Balanced coverage and speed |
|
||||
| **Research queries** | high | 8192 | Comprehensive, multi-hop reasoning |
|
||||
| **Real-time search** | low | 2048 | Minimize latency |
|
||||
|
||||
---
|
||||
|
||||
## Graph Retrieval Algorithms
|
||||
|
||||
Hindsight supports multiple graph traversal algorithms. The default (`link_expansion`) is optimized for fast retrieval with target latency under 100ms.
|
||||
|
||||
See [Configuration → Retrieval](./configuration#retrieval) for available algorithms and how to configure them.
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [**Retain**](./retain) — How memories are stored with rich context
|
||||
- [**Reflect**](./reflect) — How disposition influences reasoning
|
||||
- [**Recall API**](./api/recall) — Code examples, parameters, and tag filtering
|
||||
@@ -0,0 +1,66 @@
|
||||
# Services
|
||||
|
||||
Hindsight consists of three services that can run together or separately depending on your deployment needs.
|
||||
|
||||
## API Service
|
||||
|
||||
The core memory engine. Handles all memory operations:
|
||||
|
||||
- **Retain**: Ingests content, extracts facts, builds knowledge graph
|
||||
- **Recall**: Semantic search across memories
|
||||
- **Reflect**: Disposition-aware answer generation
|
||||
|
||||
```bash
|
||||
hindsight-api # Default port: 8888
|
||||
```
|
||||
|
||||
The API service is stateless and can be horizontally scaled behind a load balancer. All state is stored in PostgreSQL.
|
||||
|
||||
By default, the API also processes background tasks (mental model consolidation) internally. For high-throughput deployments, you can disable this and run dedicated workers instead.
|
||||
|
||||
## Worker Service
|
||||
|
||||
Dedicated task processor for background operations. Uses the **same package and Docker image** as the API service, just with a different entry point.
|
||||
|
||||
```bash
|
||||
hindsight-worker # Default metrics port: 8889
|
||||
```
|
||||
|
||||
Workers use PostgreSQL as a task broker, polling for pending tasks. Multiple workers can run simultaneously without conflicts.
|
||||
|
||||
| Deployment | Internal Worker | Dedicated Workers |
|
||||
|------------|-----------------|-------------------|
|
||||
| **Development** | ✅ Simple, all-in-one | ❌ Overkill |
|
||||
| **Small production** | ✅ Less infrastructure | ❌ Overkill |
|
||||
| **High throughput** | ❌ API bottleneck | ✅ Scale independently |
|
||||
| **Long-running tasks** | ❌ Blocks API resources | ✅ Isolated processing |
|
||||
|
||||
To use dedicated workers, disable the internal worker in the API and start worker processes:
|
||||
|
||||
```bash
|
||||
# Disable internal worker in API
|
||||
HINDSIGHT_API_WORKER_ENABLED=false hindsight-api
|
||||
|
||||
# Start dedicated workers (run multiple instances)
|
||||
hindsight-worker --worker-id worker-1
|
||||
hindsight-worker --worker-id worker-2
|
||||
```
|
||||
|
||||
Each worker exposes `/health` and `/metrics` endpoints for monitoring.
|
||||
|
||||
Before scaling down or removing workers, release their tasks with `hindsight-admin decommission-worker <worker-id>`.
|
||||
|
||||
See [Configuration - Distributed Workers](./configuration#distributed-workers) for all worker settings and [Installation - Helm](./installation#distributed-workers) for Kubernetes deployment.
|
||||
|
||||
## Control Plane
|
||||
|
||||
Web UI for managing and exploring your memory banks:
|
||||
|
||||
- Browse agents and memory banks
|
||||
- Explore entities and relationships
|
||||
- View ingestion history and operations
|
||||
- Test recall queries interactively
|
||||
|
||||
The Control Plane connects to the API service and provides a visual interface for development and debugging.
|
||||
|
||||
For bare metal deployments, you can run the Control Plane standalone using npx. See [Installation - Bare Metal](./installation#control-plane) for details.
|
||||
@@ -0,0 +1,79 @@
|
||||
# Storage
|
||||
|
||||
Hindsight uses PostgreSQL as its sole storage backend.
|
||||
|
||||
## Why PostgreSQL?
|
||||
|
||||
PostgreSQL provides all capabilities required for a semantic memory system in a single database:
|
||||
|
||||
| Capability | Implementation |
|
||||
|------------|----------------|
|
||||
| Vector search | pgvector extension with HNSW indexes |
|
||||
| Full-text search | Built-in tsvector with GIN indexes |
|
||||
| Relational data | Native PostgreSQL |
|
||||
| JSON documents | JSONB with indexing |
|
||||
| Graph queries | Recursive CTEs |
|
||||
|
||||
### Reduced System Dependencies
|
||||
|
||||
Building exclusively for PostgreSQL simplifies deployment and operations:
|
||||
|
||||
- Single connection string to configure
|
||||
- Single backup and restore strategy
|
||||
- Single monitoring target
|
||||
- ACID transactions across all data types
|
||||
- Single upgrade path
|
||||
|
||||
### No Storage Abstraction
|
||||
|
||||
Hindsight does not abstract storage behind a generic interface. This is a deliberate trade-off.
|
||||
|
||||
We believe PostgreSQL is becoming the standard database API. Its popularity, extension ecosystem, and modularity mean that PostgreSQL-compatible interfaces are appearing everywhere—from serverless offerings to distributed databases. Building for PostgreSQL today means compatibility with a growing ecosystem tomorrow.
|
||||
|
||||
Supporting multiple databases would increase flexibility but conflict with our core goals: Hindsight is fully open source and designed to be as simple as possible to run and use. Adding database abstractions introduces complexity in code, testing, documentation, and operations—complexity that we pass on to users.
|
||||
|
||||
By committing to PostgreSQL, we keep the system simple:
|
||||
- One set of deployment instructions
|
||||
- One set of performance characteristics to understand
|
||||
- One codebase optimized for one backend
|
||||
- No configuration decisions about which database to use
|
||||
|
||||
## Development with pg0
|
||||
|
||||
For local development, Hindsight uses **[pg0](https://github.com/vectorize-io/pg0)**—an embedded PostgreSQL distribution.
|
||||
|
||||
### What is pg0?
|
||||
|
||||
pg0 is a single binary containing:
|
||||
- PostgreSQL server
|
||||
- pgvector extension (pre-installed)
|
||||
- Automatic initialization
|
||||
|
||||
### Behavior
|
||||
|
||||
When no `DATABASE_URL` is configured, Hindsight:
|
||||
1. Starts an embedded PostgreSQL instance on port 5555
|
||||
2. Initializes the schema
|
||||
3. Stores data in `~/.hindsight/pg0/`
|
||||
|
||||
### Environments
|
||||
|
||||
| Environment | Database | Configuration |
|
||||
|-------------|----------|---------------|
|
||||
| Development | pg0 (embedded) | Automatic |
|
||||
| Production | PostgreSQL 15+ | `DATABASE_URL` environment variable |
|
||||
|
||||
## Requirements
|
||||
|
||||
- PostgreSQL 15 or later
|
||||
- pgvector 0.5.0 or later
|
||||
|
||||
Any PostgreSQL instance that satisfies these requirements should work. If you encounter issues with a specific setup, [open a GitHub issue](https://github.com/hindsight-ai/hindsight/issues).
|
||||
|
||||
### Tested Managed Services
|
||||
|
||||
- AWS RDS (PostgreSQL 15+)
|
||||
- Google Cloud SQL
|
||||
- Azure Database for PostgreSQL
|
||||
- Supabase
|
||||
- Neon
|
||||
@@ -0,0 +1,244 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
# CLI Reference
|
||||
|
||||
The Hindsight CLI provides command-line access to memory operations and bank management. All commands follow the [OpenAPI specification](/api-reference), so you can use `--help` on any command to see all available options.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
curl -fsSL https://hindsight.vectorize.io/get-cli | bash
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Configure the API URL:
|
||||
|
||||
```bash
|
||||
# Interactive configuration
|
||||
hindsight configure
|
||||
|
||||
# Or set directly
|
||||
hindsight configure --api-url http://localhost:8888
|
||||
|
||||
# With API key for authentication
|
||||
hindsight configure --api-url http://localhost:8888 --api-key your-api-key
|
||||
|
||||
# Or use environment variables (highest priority)
|
||||
export HINDSIGHT_API_URL=http://localhost:8888
|
||||
export HINDSIGHT_API_KEY=your-api-key
|
||||
```
|
||||
|
||||
## Core Commands
|
||||
|
||||
### Retain (Store Memory)
|
||||
|
||||
Store a single memory:
|
||||
|
||||
```bash
|
||||
hindsight memory retain <bank_id> "Alice works at Google as a software engineer"
|
||||
|
||||
# With context
|
||||
hindsight memory retain <bank_id> "Bob loves hiking" --context "hobby discussion"
|
||||
|
||||
# Queue for background processing
|
||||
hindsight memory retain <bank_id> "Meeting notes" --async
|
||||
```
|
||||
|
||||
### Retain Files
|
||||
|
||||
Bulk import from files:
|
||||
|
||||
```bash
|
||||
# Single file
|
||||
hindsight memory retain-files <bank_id> notes.txt
|
||||
|
||||
# Directory (recursive by default)
|
||||
hindsight memory retain-files <bank_id> ./documents/
|
||||
|
||||
# With context
|
||||
hindsight memory retain-files <bank_id> meeting-notes.txt --context "team meeting"
|
||||
|
||||
# Background processing
|
||||
hindsight memory retain-files <bank_id> ./data/ --async
|
||||
```
|
||||
|
||||
### Recall (Search)
|
||||
|
||||
Search memories using semantic similarity:
|
||||
|
||||
```bash
|
||||
hindsight memory recall <bank_id> "What does Alice do?"
|
||||
|
||||
# With options
|
||||
hindsight memory recall <bank_id> "hiking recommendations" \
|
||||
--budget high \
|
||||
--max-tokens 8192
|
||||
|
||||
# Filter by fact type
|
||||
hindsight memory recall <bank_id> "query" --fact-type world,observation
|
||||
|
||||
# Show trace information
|
||||
hindsight memory recall <bank_id> "query" --trace
|
||||
```
|
||||
|
||||
### Reflect (Generate Response)
|
||||
|
||||
Generate a response using memories and bank disposition:
|
||||
|
||||
```bash
|
||||
hindsight memory reflect <bank_id> "What do you know about Alice?"
|
||||
|
||||
# With additional context
|
||||
hindsight memory reflect <bank_id> "Should I learn Python?" --context "career advice"
|
||||
|
||||
# Higher budget for complex questions
|
||||
hindsight memory reflect <bank_id> "Summarize my week" --budget high
|
||||
```
|
||||
|
||||
## Bank Management
|
||||
|
||||
### List Banks
|
||||
|
||||
```bash
|
||||
hindsight bank list
|
||||
```
|
||||
|
||||
### View Disposition
|
||||
|
||||
```bash
|
||||
hindsight bank disposition <bank_id>
|
||||
```
|
||||
|
||||
### View Statistics
|
||||
|
||||
```bash
|
||||
hindsight bank stats <bank_id>
|
||||
```
|
||||
|
||||
### Set Bank Name
|
||||
|
||||
```bash
|
||||
hindsight bank name <bank_id> "My Assistant"
|
||||
```
|
||||
|
||||
### Set Mission
|
||||
|
||||
```bash
|
||||
hindsight bank mission <bank_id> "I am a helpful AI assistant interested in technology"
|
||||
```
|
||||
|
||||
## Document Management
|
||||
|
||||
```bash
|
||||
# List documents
|
||||
hindsight document list <bank_id>
|
||||
|
||||
# Get document details
|
||||
hindsight document get <bank_id> <document_id>
|
||||
|
||||
# Delete document and its memories
|
||||
hindsight document delete <bank_id> <document_id>
|
||||
```
|
||||
|
||||
## Entity Management
|
||||
|
||||
```bash
|
||||
# List entities
|
||||
hindsight entity list <bank_id>
|
||||
|
||||
# Get entity details
|
||||
hindsight entity get <bank_id> <entity_id>
|
||||
```
|
||||
|
||||
## Output Formats
|
||||
|
||||
```bash
|
||||
# Pretty (default)
|
||||
hindsight memory recall <bank_id> "query"
|
||||
|
||||
# JSON
|
||||
hindsight memory recall <bank_id> "query" -o json
|
||||
|
||||
# YAML
|
||||
hindsight memory recall <bank_id> "query" -o yaml
|
||||
```
|
||||
|
||||
## Global Options
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-v, --verbose` | Show detailed output including request/response |
|
||||
| `-o, --output <format>` | Output format: pretty, json, yaml |
|
||||
| `--help` | Show help |
|
||||
| `--version` | Show version |
|
||||
|
||||
## Control Plane UI
|
||||
|
||||
Launch the web-based Control Plane UI directly from the CLI:
|
||||
|
||||
```bash
|
||||
hindsight ui
|
||||
```
|
||||
|
||||
This runs the Control Plane locally on port 9999 using the API URL from your configuration. The UI provides:
|
||||
|
||||
- **Memory bank management** — Browse and manage all your banks
|
||||
- **Entity explorer** — Visualize the knowledge graph
|
||||
- **Query testing** — Interactive recall and reflect testing
|
||||
- **Operation history** — View ingestion and processing logs
|
||||
|
||||
:::tip
|
||||
The UI command requires Node.js to be installed. It automatically downloads and runs the `@vectorize-io/hindsight-control-plane` package via npx.
|
||||
:::
|
||||
|
||||
## Interactive Explorer
|
||||
|
||||
Launch the TUI explorer for visual navigation of your memory banks:
|
||||
|
||||
```bash
|
||||
hindsight explore
|
||||
```
|
||||
|
||||
The explorer provides an interactive terminal interface to:
|
||||
|
||||
- **Browse memory banks** — View all banks and their statistics
|
||||
- **Search memories** — Run recall queries with real-time results
|
||||
- **Inspect entities** — Explore the knowledge graph and entity relationships
|
||||
- **View facts** — Browse world facts, experiences, and observations
|
||||
- **Navigate documents** — See source documents and their extracted memories
|
||||
|
||||
### Keyboard Shortcuts
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| `↑/↓` | Navigate items |
|
||||
| `Enter` | Select / Expand |
|
||||
| `Tab` | Switch panels |
|
||||
| `/` | Search |
|
||||
| `q` | Quit |
|
||||
|
||||
<!-- Screenshot placeholder: explore command TUI -->
|
||||
|
||||
## Example Workflow
|
||||
|
||||
```bash
|
||||
# Configure API URL
|
||||
hindsight configure --api-url http://localhost:8888
|
||||
|
||||
# Store some memories
|
||||
hindsight memory retain demo "Alice works at Google"
|
||||
hindsight memory retain demo "Bob is a data scientist"
|
||||
hindsight memory retain demo "Alice and Bob are colleagues"
|
||||
|
||||
# Search memories
|
||||
hindsight memory recall demo "Who works with Alice?"
|
||||
|
||||
# Generate a response
|
||||
hindsight memory reflect demo "What do you know about the team?"
|
||||
|
||||
# Check bank disposition
|
||||
hindsight bank disposition demo
|
||||
```
|
||||
@@ -0,0 +1,247 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
---
|
||||
|
||||
# Embedded SDK (hindsight-embed)
|
||||
|
||||
Zero-configuration local memory system with automatic daemon management. Perfect for development, prototyping, and single-user applications.
|
||||
|
||||
## Overview
|
||||
|
||||
`hindsight-embed` is a zero-configuration SDK that wraps the Hindsight API and PostgreSQL database into a single auto-managed local daemon. It's designed for development, prototyping, and single-user applications where you want memory capabilities without infrastructure overhead.
|
||||
|
||||
**How it works:**
|
||||
|
||||
1. **First command triggers startup**: When you run any `hindsight-embed` command, it checks if a local daemon is running
|
||||
2. **Auto-daemon management**: If no daemon exists, it automatically spawns `hindsight-api --daemon` in the background
|
||||
3. **Embedded database**: The daemon uses `pg0` (embedded PostgreSQL) — no separate database installation required
|
||||
4. **Command forwarding**: Your command is forwarded to the local daemon via HTTP (localhost:8888)
|
||||
5. **Auto-shutdown**: After 5 minutes of inactivity (configurable), the daemon gracefully shuts down to free resources
|
||||
|
||||
**Key features:**
|
||||
|
||||
- **Zero setup** — One `configure` command and you're ready
|
||||
- **Automatic lifecycle** — Daemon starts on-demand, stops when idle
|
||||
- **Isolated storage** — Each bank gets its own embedded PostgreSQL database
|
||||
- **Local-only** — Binds to `127.0.0.1:8888`, not accessible from network
|
||||
- **Production-grade engine** — Uses the same memory engine as the full API service
|
||||
|
||||
Think of it as SQLite for long-term memory — all the power of Hindsight without managing servers.
|
||||
|
||||
## Installation
|
||||
|
||||
Install via `uvx` (recommended - always latest version):
|
||||
|
||||
```bash
|
||||
# Run directly without installation
|
||||
uvx hindsight-embed@latest configure
|
||||
|
||||
# Or use pipx for persistent installation
|
||||
pipx install hindsight-embed
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Configure
|
||||
|
||||
```bash
|
||||
# Interactive configuration
|
||||
hindsight-embed configure
|
||||
|
||||
# Or non-interactive via environment variables
|
||||
export HINDSIGHT_EMBED_LLM_PROVIDER=openai
|
||||
export HINDSIGHT_EMBED_LLM_API_KEY=sk-xxxxxxxxxxxx
|
||||
export HINDSIGHT_EMBED_LLM_MODEL=gpt-4o-mini
|
||||
hindsight-embed configure
|
||||
```
|
||||
|
||||
Configuration is saved to `~/.hindsight/embed`:
|
||||
|
||||
```bash
|
||||
HINDSIGHT_EMBED_LLM_PROVIDER=openai
|
||||
HINDSIGHT_EMBED_LLM_MODEL=gpt-4o-mini
|
||||
HINDSIGHT_EMBED_BANK_ID=default
|
||||
HINDSIGHT_EMBED_LLM_API_KEY=sk-xxxxxxxxxxxx
|
||||
|
||||
# Daemon settings (macOS: force CPU to avoid MPS/XPC issues)
|
||||
HINDSIGHT_API_EMBEDDINGS_LOCAL_FORCE_CPU=1
|
||||
HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU=1
|
||||
```
|
||||
|
||||
### 2. Use Memory Operations
|
||||
|
||||
```bash
|
||||
# Store a memory
|
||||
hindsight-embed memory retain default "User prefers dark mode"
|
||||
|
||||
# Query memories
|
||||
hindsight-embed memory recall default "user preferences"
|
||||
|
||||
# Reasoning with memory
|
||||
hindsight-embed memory reflect default "What color scheme should I use?"
|
||||
```
|
||||
|
||||
The daemon starts automatically on first use!
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `HINDSIGHT_EMBED_LLM_API_KEY` | **Required**. API key for LLM provider | - |
|
||||
| `HINDSIGHT_EMBED_LLM_PROVIDER` | LLM provider: `openai`, `anthropic`, `gemini`, `groq`, `ollama` | `openai` |
|
||||
| `HINDSIGHT_EMBED_LLM_MODEL` | Model name | `gpt-4o-mini` |
|
||||
| `HINDSIGHT_EMBED_BANK_ID` | Default memory bank ID | `default` |
|
||||
| `HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT` | Seconds before daemon auto-exits when idle (0 = never) | `300` |
|
||||
|
||||
**Provider Examples:**
|
||||
|
||||
```bash
|
||||
# OpenAI
|
||||
export HINDSIGHT_EMBED_LLM_PROVIDER=openai
|
||||
export HINDSIGHT_EMBED_LLM_API_KEY=sk-xxxxxxxxxxxx
|
||||
export HINDSIGHT_EMBED_LLM_MODEL=gpt-4o
|
||||
|
||||
# Groq (fast inference)
|
||||
export HINDSIGHT_EMBED_LLM_PROVIDER=groq
|
||||
export HINDSIGHT_EMBED_LLM_API_KEY=gsk_xxxxxxxxxxxx
|
||||
export HINDSIGHT_EMBED_LLM_MODEL=llama-3.3-70b-versatile
|
||||
|
||||
# Anthropic
|
||||
export HINDSIGHT_EMBED_LLM_PROVIDER=anthropic
|
||||
export HINDSIGHT_EMBED_LLM_API_KEY=sk-ant-xxxxxxxxxxxx
|
||||
export HINDSIGHT_EMBED_LLM_MODEL=claude-sonnet-4-20250514
|
||||
```
|
||||
|
||||
## Daemon Management
|
||||
|
||||
### Idle Timeout
|
||||
|
||||
Customize how long the daemon stays alive when idle:
|
||||
|
||||
```bash
|
||||
# Never timeout (daemon runs until manually stopped)
|
||||
export HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT=0
|
||||
|
||||
# Shorter timeout: 1 minute
|
||||
export HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT=60
|
||||
|
||||
# Longer timeout: 30 minutes
|
||||
export HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT=1800
|
||||
```
|
||||
|
||||
### Daemon Commands
|
||||
|
||||
```bash
|
||||
# Check daemon status
|
||||
hindsight-embed daemon status
|
||||
|
||||
# View daemon logs in real-time
|
||||
hindsight-embed daemon logs -f
|
||||
|
||||
# Stop daemon manually
|
||||
hindsight-embed daemon stop
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
All memory operations follow the same interface as the CLI:
|
||||
|
||||
### Retain (Store Memory)
|
||||
|
||||
```bash
|
||||
hindsight-embed memory retain <bank_id> "content"
|
||||
|
||||
# With context
|
||||
hindsight-embed memory retain <bank_id> "content" --context "source information"
|
||||
|
||||
# Background processing
|
||||
hindsight-embed memory retain <bank_id> "content" --async
|
||||
```
|
||||
|
||||
### Recall (Search)
|
||||
|
||||
```bash
|
||||
hindsight-embed memory recall <bank_id> "query"
|
||||
|
||||
# With budget control
|
||||
hindsight-embed memory recall <bank_id> "query" --budget high
|
||||
|
||||
# Show trace
|
||||
hindsight-embed memory recall <bank_id> "query" --trace
|
||||
```
|
||||
|
||||
### Reflect (Generate Response)
|
||||
|
||||
```bash
|
||||
hindsight-embed memory reflect <bank_id> "prompt"
|
||||
|
||||
# With additional context
|
||||
hindsight-embed memory reflect <bank_id> "prompt" --context "additional info"
|
||||
```
|
||||
|
||||
### Bank Management
|
||||
|
||||
```bash
|
||||
# List all banks
|
||||
hindsight-embed bank list
|
||||
|
||||
# View bank stats
|
||||
hindsight-embed bank stats <bank_id>
|
||||
|
||||
# Set bank name
|
||||
hindsight-embed bank name <bank_id> "My Assistant"
|
||||
|
||||
# Set bank mission
|
||||
hindsight-embed bank mission <bank_id> "I am a helpful AI assistant"
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Daemon Won't Start
|
||||
|
||||
Check the daemon logs:
|
||||
|
||||
```bash
|
||||
hindsight-embed daemon logs
|
||||
# Or watch in real-time
|
||||
hindsight-embed daemon logs -f
|
||||
```
|
||||
|
||||
Common issues:
|
||||
- **Missing API key**: Set `HINDSIGHT_EMBED_LLM_API_KEY`
|
||||
- **Port conflict**: Another service using port 8888
|
||||
- **Permissions**: Check `~/.hindsight/` directory permissions
|
||||
|
||||
### Daemon Exits Immediately
|
||||
|
||||
Check if you have the idle timeout set too low:
|
||||
|
||||
```bash
|
||||
# Disable idle timeout for debugging
|
||||
export HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT=0
|
||||
hindsight-embed daemon status
|
||||
```
|
||||
|
||||
### Reset Configuration
|
||||
|
||||
```bash
|
||||
# Remove config file and reconfigure
|
||||
rm ~/.hindsight/embed
|
||||
hindsight-embed configure
|
||||
```
|
||||
|
||||
## When to Use
|
||||
|
||||
**Perfect for:**
|
||||
- Development and prototyping
|
||||
- Single-user applications
|
||||
- Local-first tools
|
||||
- Quick experiments with Hindsight
|
||||
|
||||
**Not suitable for:**
|
||||
- Production multi-user deployments
|
||||
- Network-accessible services
|
||||
- High-availability requirements
|
||||
- Multi-tenant applications
|
||||
|
||||
For production deployments, use the [API Service](/developer/services) with external PostgreSQL instead.
|
||||
@@ -0,0 +1,366 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
---
|
||||
|
||||
# Vercel AI SDK
|
||||
|
||||
Official Hindsight integration for the [Vercel AI SDK](https://ai-sdk.dev).
|
||||
|
||||
## Features
|
||||
|
||||
- **7 Memory Tools**: Core memory operations (retain, recall, reflect), mental models (create, query), documents (get), and directives (create)
|
||||
- **AI SDK 6 Native**: Works seamlessly with `generateText`, `streamText`, and `ToolLoopAgent`
|
||||
- **Multi-User Support**: Dynamic bank IDs per tool call for multi-user/multi-tenant scenarios
|
||||
- **Full Parameter Support**: Complete access to all Hindsight API parameters
|
||||
- **Type-Safe**: Full TypeScript support with Zod schemas for validation
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @vectorize-io/hindsight-ai-sdk @vectorize-io/hindsight-client ai zod
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Set up your Hindsight client
|
||||
|
||||
```typescript
|
||||
import { HindsightClient } from '@vectorize-io/hindsight-client';
|
||||
|
||||
const hindsightClient = new HindsightClient({
|
||||
apiUrl: process.env.HINDSIGHT_API_URL || 'http://localhost:8000',
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Create Hindsight tools
|
||||
|
||||
```typescript
|
||||
import { createHindsightTools } from '@vectorize-io/hindsight-ai-sdk';
|
||||
|
||||
const tools = createHindsightTools({
|
||||
client: hindsightClient,
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Use with AI SDK
|
||||
|
||||
```typescript
|
||||
import { generateText } from 'ai';
|
||||
import { anthropic } from '@ai-sdk/anthropic';
|
||||
|
||||
const result = await generateText({
|
||||
model: anthropic('claude-sonnet-4-20250514'),
|
||||
tools,
|
||||
prompt: 'Remember that Alice loves hiking and prefers spicy food',
|
||||
});
|
||||
|
||||
console.log(result.text);
|
||||
```
|
||||
|
||||
## Memory Tools
|
||||
|
||||
The integration provides seven tools that the AI model can use to manage memory:
|
||||
|
||||
### `retain` - Store Information
|
||||
|
||||
The model calls this tool to store information for future recall.
|
||||
|
||||
**Parameters:**
|
||||
- `bankId` (required): Memory bank ID (usually the user ID)
|
||||
- `content` (required): Content to store
|
||||
- `documentId` (optional): Document ID for grouping/upserting related memories
|
||||
- `timestamp` (optional): ISO timestamp for when the memory occurred
|
||||
- `context` (optional): Additional context about the memory
|
||||
- `metadata` (optional): Key-value metadata for filtering
|
||||
|
||||
**Example tool call:**
|
||||
```typescript
|
||||
{
|
||||
bankId: "user-123",
|
||||
content: "Alice loves hiking and goes to Yosemite every summer",
|
||||
context: "User preferences",
|
||||
timestamp: "2024-01-15T10:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Returns:**
|
||||
```typescript
|
||||
{
|
||||
success: true,
|
||||
itemsCount: 1
|
||||
}
|
||||
```
|
||||
|
||||
### `recall` - Search Memories
|
||||
|
||||
The model calls this tool to search for relevant information in memory.
|
||||
|
||||
**Parameters:**
|
||||
- `bankId` (required): Memory bank ID
|
||||
- `query` (required): What to search for
|
||||
- `types` (optional): Filter by fact types (`['world', 'experience', 'opinion']`)
|
||||
- `maxTokens` (optional): Maximum tokens to return (default: 4096)
|
||||
- `budget` (optional): Processing budget - `'low'`, `'mid'`, or `'high'`
|
||||
- `queryTimestamp` (optional): Query from a specific time (ISO format)
|
||||
- `includeEntities` (optional): Include entity observations
|
||||
- `includeChunks` (optional): Include raw document chunks
|
||||
|
||||
**Example tool call:**
|
||||
```typescript
|
||||
{
|
||||
bankId: "user-123",
|
||||
query: "What does Alice like to do outdoors?",
|
||||
types: ["world", "experience"],
|
||||
maxTokens: 2048,
|
||||
budget: "mid"
|
||||
}
|
||||
```
|
||||
|
||||
**Returns:**
|
||||
```typescript
|
||||
{
|
||||
results: [
|
||||
{
|
||||
id: "mem-123",
|
||||
text: "Alice loves hiking",
|
||||
type: "world",
|
||||
entities: ["Alice"],
|
||||
context: "User preferences",
|
||||
occurred_start: "2024-01-15T10:30:00Z",
|
||||
document_id: "doc-456",
|
||||
metadata: { source: "chat" }
|
||||
}
|
||||
],
|
||||
entities: {
|
||||
"Alice": {
|
||||
canonical_name: "Alice",
|
||||
mention_count: 15,
|
||||
observations: [...]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `reflect` - Synthesize Insights
|
||||
|
||||
The model calls this tool to analyze memories and generate contextual insights.
|
||||
|
||||
**Parameters:**
|
||||
- `bankId` (required): Memory bank ID
|
||||
- `query` (required): Question to reflect on
|
||||
- `context` (optional): Additional context for reflection
|
||||
- `budget` (optional): Processing budget - `'low'`, `'mid'`, or `'high'`
|
||||
|
||||
**Example tool call:**
|
||||
```typescript
|
||||
{
|
||||
bankId: "user-123",
|
||||
query: "What outdoor activities does Alice enjoy?",
|
||||
context: "Planning a weekend trip",
|
||||
budget: "mid"
|
||||
}
|
||||
```
|
||||
|
||||
**Returns:**
|
||||
```typescript
|
||||
{
|
||||
text: "Alice is an avid hiker who particularly enjoys visiting Yosemite National Park during summer months. She has expressed strong preferences for mountain trails over beach activities.",
|
||||
basedOn: [
|
||||
{
|
||||
id: "mem-123",
|
||||
text: "Alice loves hiking",
|
||||
type: "world",
|
||||
context: "User preferences",
|
||||
occurred_start: "2024-01-15T10:30:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `createMentalModel` - Create Knowledge Consolidation
|
||||
|
||||
The model calls this tool to create a mental model that automatically consolidates memories into structured knowledge.
|
||||
|
||||
**Parameters:**
|
||||
- `bankId` (required): Memory bank ID
|
||||
- `mentalModelId` (optional): Custom ID for the mental model (auto-generated if not provided)
|
||||
- `name` (optional): Name for the mental model
|
||||
- `sourceQuery` (optional): Query defining which memories to consolidate
|
||||
- `tags` (optional): Tags for organizing mental models
|
||||
- `maxTokens` (optional): Maximum tokens for the content
|
||||
- `autoRefresh` (optional): Auto-refresh after new consolidations (default: false)
|
||||
|
||||
**Example tool call:**
|
||||
```typescript
|
||||
{
|
||||
bankId: "user-123",
|
||||
name: "User Preferences",
|
||||
sourceQuery: "What are the user's preferences?",
|
||||
tags: ["preferences"],
|
||||
autoRefresh: true
|
||||
}
|
||||
```
|
||||
|
||||
**Returns:**
|
||||
```typescript
|
||||
{
|
||||
mentalModelId: "mm-456",
|
||||
createdAt: "2024-01-15T10:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### `queryMentalModel` - Retrieve Consolidated Knowledge
|
||||
|
||||
The model calls this tool to retrieve synthesized insights from an existing mental model.
|
||||
|
||||
**Parameters:**
|
||||
- `bankId` (required): Memory bank ID
|
||||
- `mentalModelId` (required): ID of the mental model to query
|
||||
|
||||
**Example tool call:**
|
||||
```typescript
|
||||
{
|
||||
bankId: "user-123",
|
||||
mentalModelId: "mm-456"
|
||||
}
|
||||
```
|
||||
|
||||
**Returns:**
|
||||
```typescript
|
||||
{
|
||||
content: "The user prefers outdoor activities, particularly hiking. They enjoy mountain trails and visit Yosemite regularly during summer.",
|
||||
name: "User Preferences",
|
||||
updatedAt: "2024-01-20T15:45:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### `getDocument` - Retrieve Stored Document
|
||||
|
||||
The model calls this tool to retrieve a stored document by its ID.
|
||||
|
||||
**Parameters:**
|
||||
- `bankId` (required): Memory bank ID
|
||||
- `documentId` (required): ID of the document to retrieve
|
||||
|
||||
**Example tool call:**
|
||||
```typescript
|
||||
{
|
||||
bankId: "user-123",
|
||||
documentId: "doc-789"
|
||||
}
|
||||
```
|
||||
|
||||
**Returns:**
|
||||
```typescript
|
||||
{
|
||||
originalText: "User profile: Alice, Software Engineer, loves hiking...",
|
||||
id: "doc-789",
|
||||
createdAt: "2024-01-10T09:00:00Z",
|
||||
updatedAt: "2024-01-15T14:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### `createDirective` - Create Behavioral Rule
|
||||
|
||||
The model calls this tool to create a directive—a hard rule injected into prompts during reflect operations.
|
||||
|
||||
**Parameters:**
|
||||
- `bankId` (required): Memory bank ID
|
||||
- `name` (required): Human-readable name for the directive
|
||||
- `content` (required): The directive text to inject
|
||||
- `priority` (optional): Higher priority directives are injected first (default: 0)
|
||||
- `isActive` (optional): Whether this directive is active (default: true)
|
||||
- `tags` (optional): Tags for filtering (e.g., user-specific directives)
|
||||
|
||||
**Example tool call:**
|
||||
```typescript
|
||||
{
|
||||
bankId: "user-123",
|
||||
name: "Response Format",
|
||||
content: "Always provide responses in bullet-point format",
|
||||
priority: 10,
|
||||
tags: ["formatting"]
|
||||
}
|
||||
```
|
||||
|
||||
**Returns:**
|
||||
```typescript
|
||||
{
|
||||
id: "dir-321",
|
||||
name: "Response Format",
|
||||
content: "Always provide responses in bullet-point format",
|
||||
tags: ["formatting"],
|
||||
createdAt: "2024-01-15T10:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Using with `generateText`
|
||||
|
||||
```typescript
|
||||
import { HindsightClient } from '@vectorize-io/hindsight-client';
|
||||
import { createHindsightTools } from '@vectorize-io/hindsight-ai-sdk';
|
||||
import { generateText } from 'ai';
|
||||
import { anthropic } from '@ai-sdk/anthropic';
|
||||
|
||||
const hindsightClient = new HindsightClient({
|
||||
apiUrl: 'http://localhost:8000',
|
||||
});
|
||||
|
||||
const tools = createHindsightTools({ client: hindsightClient });
|
||||
|
||||
const result = await generateText({
|
||||
model: anthropic('claude-sonnet-4-20250514'),
|
||||
tools,
|
||||
system: `You are a helpful assistant with long-term memory. Use the recall tool to check for relevant memories before responding.`,
|
||||
prompt: 'Remember that Alice loves hiking and prefers spicy food',
|
||||
});
|
||||
|
||||
console.log(result.text);
|
||||
```
|
||||
|
||||
### Using with `streamText`
|
||||
|
||||
```typescript
|
||||
import { streamText } from 'ai';
|
||||
|
||||
const result = streamText({
|
||||
model: anthropic('claude-sonnet-4-20250514'),
|
||||
tools,
|
||||
system: `You have persistent memory. Use retain to store important information and recall to retrieve it.`,
|
||||
prompt: 'What do you know about Alice?',
|
||||
});
|
||||
|
||||
for await (const chunk of result.textStream) {
|
||||
process.stdout.write(chunk);
|
||||
}
|
||||
```
|
||||
|
||||
### Using with `ToolLoopAgent`
|
||||
|
||||
```typescript
|
||||
import { ToolLoopAgent, stopWhen, stepCountIs } from 'ai';
|
||||
|
||||
const agent = new ToolLoopAgent({
|
||||
model: anthropic('claude-sonnet-4-20250514'),
|
||||
tools,
|
||||
instructions: `You are a personal assistant with long-term memory. Always check recall before responding and use retain to store important information.`,
|
||||
stopWhen: stepCountIs(10),
|
||||
});
|
||||
|
||||
const result = await agent.generate({
|
||||
prompt: 'What did I say I wanted to work on this week?',
|
||||
});
|
||||
```
|
||||
|
||||
### Multi-User Support
|
||||
|
||||
```typescript
|
||||
const result = await generateText({
|
||||
model: anthropic('claude-sonnet-4-20250514'),
|
||||
tools,
|
||||
system: `You are a helpful assistant. The user's ID is: ${userId}. Always pass this as the bankId parameter to memory tools.`,
|
||||
prompt: 'Remember that I prefer dark mode',
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,345 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# LiteLLM
|
||||
|
||||
Universal LLM memory integration via [LiteLLM](https://github.com/BerriAI/litellm). Add persistent memory to any LLM application with just a few lines of code.
|
||||
|
||||
## Features
|
||||
|
||||
- **Universal LLM Support** - Works with 100+ LLM providers via LiteLLM (OpenAI, Anthropic, Groq, Azure, AWS Bedrock, Google Vertex AI, and more)
|
||||
- **Simple Integration** - Just configure, enable, and use `hindsight_litellm.completion()`
|
||||
- **Automatic Memory Injection** - Relevant memories are injected into prompts before LLM calls
|
||||
- **Automatic Conversation Storage** - Conversations are stored to Hindsight for future recall
|
||||
- **Two Memory Modes** - Choose between `reflect` (synthesized context) or `recall` (raw memory retrieval)
|
||||
- **Direct Memory APIs** - Query, synthesize, and store memories manually
|
||||
- **Native Client Wrappers** - Alternative wrappers for OpenAI and Anthropic SDKs
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install hindsight-litellm
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```python
|
||||
import hindsight_litellm
|
||||
|
||||
# Configure and enable memory integration
|
||||
hindsight_litellm.configure(
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
bank_id="my-agent",
|
||||
)
|
||||
hindsight_litellm.enable()
|
||||
|
||||
# Use the convenience wrapper - memory is automatically injected and stored
|
||||
response = hindsight_litellm.completion(
|
||||
model="gpt-4o-mini",
|
||||
messages=[{"role": "user", "content": "What did we discuss about AI?"}]
|
||||
)
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
When you call `completion()`, the following happens automatically:
|
||||
|
||||
1. **Memory Retrieval** - Hindsight is queried for relevant memories based on the conversation
|
||||
2. **Prompt Injection** - Memories are injected into the system message
|
||||
3. **LLM Call** - The enriched prompt is sent to the LLM
|
||||
4. **Conversation Storage** - The conversation is stored to Hindsight for future recall
|
||||
5. **Response Returned** - You receive the response as normal
|
||||
|
||||
## Configuration Options
|
||||
|
||||
```python
|
||||
hindsight_litellm.configure(
|
||||
# Required
|
||||
hindsight_api_url="http://localhost:8888", # Hindsight API server URL
|
||||
bank_id="my-agent", # Memory bank ID
|
||||
|
||||
api_key="your-api-key", # Optional API key for authentication
|
||||
|
||||
# Optional - Memory behavior
|
||||
store_conversations=True, # Store conversations after LLM calls
|
||||
inject_memories=True, # Inject relevant memories into prompts
|
||||
use_reflect=False, # Use reflect API (synthesized) vs recall (raw memories)
|
||||
reflect_include_facts=False, # Include source facts with reflect responses
|
||||
max_memories=None, # Maximum memories to inject (None = unlimited)
|
||||
max_memory_tokens=4096, # Maximum tokens for memory context
|
||||
recall_budget="mid", # Recall budget: "low", "mid", "high"
|
||||
fact_types=["world", "agent"], # Filter fact types to inject
|
||||
|
||||
# Optional - Bank Configuration
|
||||
bank_name="My Agent", # Human-readable display name for the memory bank
|
||||
mission="This agent...", # Instructions guiding what Hindsight should remember
|
||||
|
||||
# Optional - Advanced
|
||||
injection_mode="system_message", # or "prepend_user"
|
||||
excluded_models=["gpt-3.5*"], # Exclude certain models
|
||||
verbose=True, # Enable verbose logging and debug info
|
||||
)
|
||||
```
|
||||
|
||||
### Bank Configuration
|
||||
|
||||
The `mission` and `bank_name` parameters configure the memory bank itself. When provided, `configure()` will automatically create or update the bank with these settings.
|
||||
|
||||
```python
|
||||
hindsight_litellm.configure(
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
bank_id="support-router",
|
||||
bank_name="Customer Support Router",
|
||||
mission="""You're a customer support router - keep track of which types of issues
|
||||
should go to which teams (billing, technical, sales), customer preferences for
|
||||
communication channels, and past issue resolutions.""",
|
||||
)
|
||||
```
|
||||
|
||||
### Memory Modes: Reflect vs Recall
|
||||
|
||||
- **Recall mode** (`use_reflect=False`, default): Retrieves raw memory facts and injects them as a numbered list. Best when you need precise, individual memories.
|
||||
- **Reflect mode** (`use_reflect=True`): Synthesizes memories into a coherent context paragraph. Best for natural, conversational memory context.
|
||||
|
||||
```python
|
||||
# Recall mode - raw memories
|
||||
hindsight_litellm.configure(
|
||||
bank_id="my-agent",
|
||||
use_reflect=False, # Default
|
||||
)
|
||||
# Injects: "1. [WORLD] User prefers Python\n2. [MENTAL MODEL] User prefers simple code..."
|
||||
|
||||
# Reflect mode - synthesized context
|
||||
hindsight_litellm.configure(
|
||||
bank_id="my-agent",
|
||||
use_reflect=True,
|
||||
)
|
||||
# Injects: "Based on previous conversations, the user is a Python developer who..."
|
||||
```
|
||||
|
||||
## Multi-Provider Support
|
||||
|
||||
Works with any LiteLLM-supported provider:
|
||||
|
||||
```python
|
||||
import hindsight_litellm
|
||||
|
||||
hindsight_litellm.configure(
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
bank_id="my-agent",
|
||||
)
|
||||
hindsight_litellm.enable()
|
||||
|
||||
# OpenAI
|
||||
hindsight_litellm.completion(model="gpt-4o", messages=[...])
|
||||
|
||||
# Anthropic
|
||||
hindsight_litellm.completion(model="claude-3-5-sonnet-20241022", messages=[...])
|
||||
|
||||
# Groq
|
||||
hindsight_litellm.completion(model="groq/llama-3.1-70b-versatile", messages=[...])
|
||||
|
||||
# Azure OpenAI
|
||||
hindsight_litellm.completion(model="azure/gpt-4", messages=[...])
|
||||
|
||||
# AWS Bedrock
|
||||
hindsight_litellm.completion(model="bedrock/anthropic.claude-3", messages=[...])
|
||||
|
||||
# Google Vertex AI
|
||||
hindsight_litellm.completion(model="vertex_ai/gemini-pro", messages=[...])
|
||||
```
|
||||
|
||||
## Direct Memory APIs
|
||||
|
||||
### Recall - Query raw memories
|
||||
|
||||
```python
|
||||
from hindsight_litellm import configure, recall
|
||||
|
||||
configure(bank_id="my-agent", hindsight_api_url="http://localhost:8888")
|
||||
|
||||
memories = recall("what projects am I working on?", budget="mid")
|
||||
for m in memories:
|
||||
print(f"- [{m.fact_type}] {m.text}")
|
||||
```
|
||||
|
||||
### Reflect - Get synthesized context
|
||||
|
||||
```python
|
||||
from hindsight_litellm import configure, reflect
|
||||
|
||||
configure(bank_id="my-agent", hindsight_api_url="http://localhost:8888")
|
||||
|
||||
result = reflect("what do you know about the user's preferences?")
|
||||
print(result.text)
|
||||
```
|
||||
|
||||
### Retain - Store memories
|
||||
|
||||
```python
|
||||
from hindsight_litellm import configure, retain
|
||||
|
||||
configure(bank_id="my-agent", hindsight_api_url="http://localhost:8888")
|
||||
|
||||
result = retain(
|
||||
content="User mentioned they're working on a machine learning project",
|
||||
context="Discussion about current projects",
|
||||
)
|
||||
```
|
||||
|
||||
### Async APIs
|
||||
|
||||
```python
|
||||
from hindsight_litellm import arecall, areflect, aretain
|
||||
|
||||
# Async versions of all memory APIs
|
||||
memories = await arecall("what do you know about me?")
|
||||
context = await areflect("summarize user preferences")
|
||||
result = await aretain(content="New information to remember")
|
||||
```
|
||||
|
||||
## Native Client Wrappers
|
||||
|
||||
Alternative to LiteLLM callbacks for direct SDK integration.
|
||||
|
||||
### OpenAI Wrapper
|
||||
|
||||
```python
|
||||
from openai import OpenAI
|
||||
from hindsight_litellm import wrap_openai
|
||||
|
||||
client = OpenAI()
|
||||
wrapped = wrap_openai(
|
||||
client,
|
||||
bank_id="my-agent",
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
)
|
||||
|
||||
response = wrapped.chat.completions.create(
|
||||
model="gpt-4",
|
||||
messages=[{"role": "user", "content": "What do you know about me?"}]
|
||||
)
|
||||
```
|
||||
|
||||
### Anthropic Wrapper
|
||||
|
||||
```python
|
||||
from anthropic import Anthropic
|
||||
from hindsight_litellm import wrap_anthropic
|
||||
|
||||
client = Anthropic()
|
||||
wrapped = wrap_anthropic(
|
||||
client,
|
||||
bank_id="my-agent",
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
)
|
||||
|
||||
response = wrapped.messages.create(
|
||||
model="claude-3-5-sonnet-20241022",
|
||||
max_tokens=1024,
|
||||
messages=[{"role": "user", "content": "Hello!"}]
|
||||
)
|
||||
```
|
||||
|
||||
## Debug Mode
|
||||
|
||||
When `verbose=True`, you can inspect exactly what memories are being injected:
|
||||
|
||||
```python
|
||||
from hindsight_litellm import configure, enable, completion, get_last_injection_debug
|
||||
|
||||
configure(
|
||||
bank_id="my-agent",
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
verbose=True,
|
||||
)
|
||||
enable()
|
||||
|
||||
response = completion(
|
||||
model="gpt-4o-mini",
|
||||
messages=[{"role": "user", "content": "What's my favorite color?"}]
|
||||
)
|
||||
|
||||
# Inspect what was injected
|
||||
debug = get_last_injection_debug()
|
||||
if debug:
|
||||
print(f"Mode: {debug.mode}") # "reflect" or "recall"
|
||||
print(f"Injected: {debug.injected}") # True/False
|
||||
print(f"Results: {debug.results_count}")
|
||||
print(f"Memory context:\n{debug.memory_context}")
|
||||
```
|
||||
|
||||
## Context Manager
|
||||
|
||||
```python
|
||||
from hindsight_litellm import hindsight_memory
|
||||
import litellm
|
||||
|
||||
with hindsight_memory(bank_id="user-123"):
|
||||
response = litellm.completion(model="gpt-4", messages=[...])
|
||||
# Memory integration automatically disabled after context
|
||||
```
|
||||
|
||||
## Disabling and Cleanup
|
||||
|
||||
```python
|
||||
from hindsight_litellm import disable, cleanup
|
||||
|
||||
# Temporarily disable memory integration
|
||||
disable()
|
||||
|
||||
# Clean up all resources (call when shutting down)
|
||||
cleanup()
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Main Functions
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `configure(...)` | Configure global Hindsight settings |
|
||||
| `enable()` | Enable memory integration with LiteLLM |
|
||||
| `disable()` | Disable memory integration |
|
||||
| `is_enabled()` | Check if memory integration is enabled |
|
||||
| `cleanup()` | Clean up all resources |
|
||||
|
||||
### Configuration Functions
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `get_config()` | Get current configuration |
|
||||
| `is_configured()` | Check if Hindsight is configured |
|
||||
| `reset_config()` | Reset configuration to defaults |
|
||||
|
||||
### Memory Functions
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `recall(query, ...)` | Synchronously query raw memories |
|
||||
| `arecall(query, ...)` | Asynchronously query raw memories |
|
||||
| `reflect(query, ...)` | Synchronously get synthesized memory context |
|
||||
| `areflect(query, ...)` | Asynchronously get synthesized memory context |
|
||||
| `retain(content, ...)` | Synchronously store a memory |
|
||||
| `aretain(content, ...)` | Asynchronously store a memory |
|
||||
|
||||
### Debug Functions
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `get_last_injection_debug()` | Get debug info from last memory injection |
|
||||
| `clear_injection_debug()` | Clear stored debug info |
|
||||
|
||||
### Client Wrappers
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `wrap_openai(client, ...)` | Wrap OpenAI client with memory |
|
||||
| `wrap_anthropic(client, ...)` | Wrap Anthropic client with memory |
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python >= 3.10
|
||||
- litellm >= 1.40.0
|
||||
- A running Hindsight API server
|
||||
@@ -0,0 +1,193 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
# Local MCP Server
|
||||
|
||||
Hindsight provides a fully local MCP server that runs entirely on your machine with an embedded PostgreSQL database. No external server or database setup required.
|
||||
|
||||
This is ideal for:
|
||||
- **Personal use with Claude Desktop** — Give Claude long-term memory across conversations
|
||||
- **Development and testing** — Quick setup without infrastructure
|
||||
- **Privacy-focused setups** — All data stays on your machine
|
||||
|
||||
## Quick Install
|
||||
|
||||
```bash
|
||||
curl -fsSL https://hindsight.vectorize.io/get-mcp | bash -s -- \
|
||||
--app claude-desktop \
|
||||
--set HINDSIGHT_API_LLM_API_KEY=sk-...
|
||||
```
|
||||
|
||||
This script will:
|
||||
1. Install [uv](https://docs.astral.sh/uv/) if not already installed
|
||||
2. Configure Claude Desktop to use the Hindsight MCP server
|
||||
3. Set the provided environment variables in the MCP configuration
|
||||
|
||||
:::info Other MCP Applications
|
||||
The quick install script currently supports Claude Desktop only. For other MCP-compatible applications (Cursor, Cline, etc.), follow the [Manual Configuration](#manual-configuration) steps below.
|
||||
:::
|
||||
|
||||
## Manual Configuration
|
||||
|
||||
Add the following to your MCP client's configuration. For Claude Desktop:
|
||||
|
||||
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
||||
- **Linux**: `~/.config/Claude/claude_desktop_config.json`
|
||||
|
||||
For other MCP clients, refer to their documentation for the configuration file location.
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"hindsight": {
|
||||
"command": "uvx",
|
||||
"args": ["--from", "hindsight-api", "hindsight-local-mcp"],
|
||||
"env": {
|
||||
"HINDSIGHT_API_LLM_API_KEY": "sk-..."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### With Custom Bank ID
|
||||
|
||||
By default, memories are stored in a bank called `mcp`. To use a different bank:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"hindsight": {
|
||||
"command": "uvx",
|
||||
"args": ["--from", "hindsight-api", "hindsight-local-mcp"],
|
||||
"env": {
|
||||
"HINDSIGHT_API_LLM_API_KEY": "sk-...",
|
||||
"HINDSIGHT_API_MCP_LOCAL_BANK_ID": "my-personal-memory"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
All standard [Hindsight configuration variables](/developer/configuration) are supported.
|
||||
|
||||
### Local MCP Specific
|
||||
|
||||
| Variable | Required | Default | Description |
|
||||
|----------|----------|---------|-------------|
|
||||
| `HINDSIGHT_API_MCP_LOCAL_BANK_ID` | No | `mcp` | Memory bank ID to use |
|
||||
| `HINDSIGHT_API_MCP_INSTRUCTIONS` | No | - | Additional instructions appended to both `retain` and `recall` tools |
|
||||
|
||||
### Customizing Tool Behavior
|
||||
|
||||
You can customize what gets stored by adding instructions to the tools. Re-run the install script with the additional `--set` flag:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://hindsight.vectorize.io/get-mcp | bash -s -- \
|
||||
--app claude-desktop \
|
||||
--set HINDSIGHT_API_LLM_API_KEY=sk-... \
|
||||
--set HINDSIGHT_API_MCP_INSTRUCTIONS="Also store every action you take, code you write, and files you modify."
|
||||
```
|
||||
|
||||
These instructions are appended to the default tool descriptions, guiding Claude on when and how to use the memory tools.
|
||||
|
||||
## Available Tools
|
||||
|
||||
### retain
|
||||
|
||||
Store information to long-term memory. This is a **fire-and-forget** operation — it returns immediately while processing happens in the background.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `content` | string | Yes | The fact or memory to store |
|
||||
| `context` | string | No | Category for the memory (default: `general`) |
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"name": "retain",
|
||||
"arguments": {
|
||||
"content": "User's favorite color is blue",
|
||||
"context": "preferences"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "accepted",
|
||||
"message": "Memory storage initiated"
|
||||
}
|
||||
```
|
||||
|
||||
### recall
|
||||
|
||||
Search memories to provide personalized responses.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `query` | string | Yes | Natural language search query |
|
||||
| `max_tokens` | integer | No | Maximum tokens to return (default: 4096) |
|
||||
| `budget` | string | No | Search depth: `low`, `mid`, or `high` (default: `low`) |
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"name": "recall",
|
||||
"arguments": {
|
||||
"query": "What are the user's color preferences?",
|
||||
"max_tokens": 2048,
|
||||
"budget": "mid"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
The local MCP server:
|
||||
|
||||
1. **Starts an embedded PostgreSQL** (pg0) on an automatically assigned port
|
||||
2. **Initializes the Hindsight memory engine** with local embeddings
|
||||
3. **Connects via stdio** to Claude Code using the MCP protocol
|
||||
|
||||
Data is persisted in the pg0 data directory (`~/.pg0/hindsight-mcp/`), so your memories survive restarts.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "HINDSIGHT_API_LLM_API_KEY required"
|
||||
|
||||
Make sure you've set the API key in your MCP configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"HINDSIGHT_API_LLM_API_KEY": "sk-..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Slow startup
|
||||
|
||||
The first startup may take longer as it:
|
||||
- Downloads the embedding model (~100MB)
|
||||
- Initializes the PostgreSQL database
|
||||
|
||||
Subsequent starts are faster.
|
||||
|
||||
### Checking logs
|
||||
|
||||
Set `HINDSIGHT_API_LOG_LEVEL=debug` for verbose output:
|
||||
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"HINDSIGHT_API_LOG_LEVEL": "debug"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Logs are written to stderr and visible in Claude Code's MCP server output.
|
||||
@@ -0,0 +1,294 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
---
|
||||
|
||||
# OpenClaw
|
||||
|
||||
Local, long term memory for [OpenClaw](https://openclaw.ai) agents using [Hindsight](https://vectorize.io/hindsight).
|
||||
|
||||
This plugin integrates [hindsight-embed](https://vectorize.io/hindsight/cli), a standalone daemon that bundles Hindsight's memory engine (API + PostgreSQL) into a single command. Everything runs locally on your machine, reuses the LLM you're already paying for, and costs nothing extra.
|
||||
|
||||
## Quick Start
|
||||
|
||||
**Step 1: Set up LLM for memory extraction**
|
||||
|
||||
Choose one provider and set its API key:
|
||||
|
||||
```bash
|
||||
# Option A: OpenAI (uses gpt-4o-mini for memory extraction)
|
||||
export OPENAI_API_KEY="sk-your-key"
|
||||
|
||||
# Option B: Anthropic (uses claude-3-5-haiku for memory extraction)
|
||||
export ANTHROPIC_API_KEY="your-key"
|
||||
|
||||
# Option C: Gemini (uses gemini-2.5-flash for memory extraction)
|
||||
export GEMINI_API_KEY="your-key"
|
||||
|
||||
# Option D: Groq (uses openai/gpt-oss-20b for memory extraction)
|
||||
export GROQ_API_KEY="your-key"
|
||||
|
||||
# Option E: Claude Code (uses claude-sonnet-4-20250514, no API key needed)
|
||||
export HINDSIGHT_API_LLM_PROVIDER=claude-code
|
||||
|
||||
# Option F: OpenAI Codex (uses o3-mini, no API key needed)
|
||||
export HINDSIGHT_API_LLM_PROVIDER=openai-codex
|
||||
```
|
||||
|
||||
**Step 2: Install the plugin**
|
||||
|
||||
```bash
|
||||
openclaw plugins install @vectorize-io/hindsight-openclaw
|
||||
```
|
||||
|
||||
**Step 3: Start OpenClaw**
|
||||
|
||||
```bash
|
||||
openclaw gateway
|
||||
```
|
||||
|
||||
The plugin will automatically:
|
||||
- Start a local Hindsight daemon (port 9077)
|
||||
- Capture conversations after each turn
|
||||
- Inject relevant memories before agent responses
|
||||
|
||||
**Important:** The LLM you configure above is **only for memory extraction** (background processing). Your main OpenClaw agent can use any model you configure separately.
|
||||
|
||||
## How It Works
|
||||
|
||||
**Auto-Capture:** Every conversation is automatically stored after each turn. Facts, entities, and relationships are extracted in the background.
|
||||
|
||||
**Auto-Recall:** Before each agent response, relevant memories are automatically injected into the context (up to 1024 tokens). The agent uses past context without needing to call tools.
|
||||
|
||||
Traditional memory systems give agents a `search_memory` tool - but models don't use it consistently. Auto-recall solves this by injecting memories automatically before every turn.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Plugin Settings
|
||||
|
||||
Optional settings in `~/.openclaw/openclaw.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"plugins": {
|
||||
"entries": {
|
||||
"hindsight-openclaw": {
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"apiPort": 9077,
|
||||
"daemonIdleTimeout": 0,
|
||||
"embedVersion": "latest"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `apiPort` - Port for the openclaw profile daemon (default: `9077`)
|
||||
- `daemonIdleTimeout` - Seconds before daemon shuts down from inactivity (default: `0` = never)
|
||||
- `embedVersion` - hindsight-embed version (default: `"latest"`)
|
||||
- `bankMission` - Custom context for the memory bank (optional)
|
||||
|
||||
### LLM Configuration
|
||||
|
||||
The plugin auto-detects your LLM provider from these environment variables:
|
||||
|
||||
| Provider | Env Var | Default Model | Notes |
|
||||
|----------|---------|---------------|-------|
|
||||
| OpenAI | `OPENAI_API_KEY` | `gpt-4o-mini` | |
|
||||
| Anthropic | `ANTHROPIC_API_KEY` | `claude-3-5-haiku-20241022` | |
|
||||
| Gemini | `GEMINI_API_KEY` | `gemini-2.5-flash` | |
|
||||
| Groq | `GROQ_API_KEY` | `openai/gpt-oss-20b` | |
|
||||
| Claude Code | `HINDSIGHT_API_LLM_PROVIDER=claude-code` | `claude-sonnet-4-20250514` | No API key needed |
|
||||
| OpenAI Codex | `HINDSIGHT_API_LLM_PROVIDER=openai-codex` | `o3-mini` | No API key needed |
|
||||
|
||||
**Override with explicit config:**
|
||||
|
||||
```bash
|
||||
export HINDSIGHT_API_LLM_PROVIDER=openai
|
||||
export HINDSIGHT_API_LLM_MODEL=gpt-4o-mini
|
||||
export HINDSIGHT_API_LLM_API_KEY=sk-your-key
|
||||
|
||||
# Optional: custom base URL (OpenRouter, Azure, vLLM, etc.)
|
||||
export HINDSIGHT_API_LLM_BASE_URL=https://openrouter.ai/api/v1
|
||||
```
|
||||
|
||||
**Example: Free OpenRouter model**
|
||||
|
||||
```bash
|
||||
export HINDSIGHT_API_LLM_PROVIDER=openai
|
||||
export HINDSIGHT_API_LLM_MODEL=xiaomi/mimo-v2-flash # FREE!
|
||||
export HINDSIGHT_API_LLM_API_KEY=sk-or-v1-your-openrouter-key
|
||||
export HINDSIGHT_API_LLM_BASE_URL=https://openrouter.ai/api/v1
|
||||
```
|
||||
|
||||
### External API (Advanced)
|
||||
|
||||
Connect to a remote Hindsight API server instead of running a local daemon. This is useful for:
|
||||
|
||||
- **Shared memory** across multiple OpenClaw instances
|
||||
- **Production deployments** with centralized memory storage
|
||||
- **Team environments** where agents share knowledge
|
||||
|
||||
#### Plugin Configuration
|
||||
|
||||
Configure in `~/.openclaw/openclaw.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"plugins": {
|
||||
"entries": {
|
||||
"hindsight-openclaw": {
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"hindsightApiUrl": "https://your-hindsight-server.com",
|
||||
"hindsightApiToken": "your-api-token"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `hindsightApiUrl` - Full URL to external Hindsight API (e.g., `https://mcp.hindsight.example.com`)
|
||||
- `hindsightApiToken` - API token for authentication (optional, only if API requires auth)
|
||||
|
||||
#### Environment Variables (Alternative)
|
||||
|
||||
You can also configure via environment variables:
|
||||
|
||||
```bash
|
||||
export HINDSIGHT_EMBED_API_URL=https://your-hindsight-server.com
|
||||
export HINDSIGHT_EMBED_API_TOKEN=your-api-token # Optional
|
||||
|
||||
openclaw gateway
|
||||
```
|
||||
|
||||
**Note:** Plugin config takes precedence over environment variables.
|
||||
|
||||
#### Behavior
|
||||
|
||||
When external API mode is enabled:
|
||||
- **No local daemon** is started (no hindsight-embed process)
|
||||
- **Health check** runs on startup to verify API connectivity
|
||||
- **All memory operations** (retain, recall, reflect) go to the external API
|
||||
- **Faster startup** since no local PostgreSQL or embedding models are needed
|
||||
|
||||
#### Verification
|
||||
|
||||
Check OpenClaw logs for external API mode:
|
||||
|
||||
```bash
|
||||
tail -f /tmp/openclaw/openclaw-*.log | grep Hindsight
|
||||
|
||||
# Should see on startup:
|
||||
# [Hindsight] External API mode enabled: https://your-hindsight-server.com
|
||||
# [Hindsight] External API health check passed
|
||||
```
|
||||
|
||||
If you see daemon startup messages instead, verify your configuration is correct.
|
||||
|
||||
## Inspecting Memories
|
||||
|
||||
### Check Configuration
|
||||
|
||||
View the daemon config that was written by the plugin:
|
||||
|
||||
```bash
|
||||
cat ~/.hindsight/profiles/openclaw.env
|
||||
```
|
||||
|
||||
This shows the LLM provider, model, port, and other settings the daemon is using.
|
||||
|
||||
### Check Daemon Status
|
||||
|
||||
```bash
|
||||
# Check if daemon is running
|
||||
uvx hindsight-embed@latest -p openclaw daemon status
|
||||
|
||||
# View daemon logs
|
||||
tail -f ~/.hindsight/profiles/openclaw.log
|
||||
```
|
||||
|
||||
### Query Memories
|
||||
|
||||
```bash
|
||||
# Search memories
|
||||
uvx hindsight-embed@latest -p openclaw memory recall openclaw "user preferences"
|
||||
|
||||
# View recent memories
|
||||
uvx hindsight-embed@latest -p openclaw memory list openclaw --limit 10
|
||||
|
||||
# Open web UI (uses openclaw profile's daemon)
|
||||
uvx hindsight-embed@latest -p openclaw ui
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Plugin not loading
|
||||
|
||||
```bash
|
||||
openclaw plugins list | grep hindsight
|
||||
# Should show: ✓ enabled │ Hindsight Memory │ ...
|
||||
|
||||
# Reinstall if needed
|
||||
openclaw plugins install @vectorize-io/hindsight-openclaw
|
||||
```
|
||||
|
||||
### Daemon not starting
|
||||
|
||||
```bash
|
||||
# Check daemon status (note: -p openclaw uses the openclaw profile)
|
||||
uvx hindsight-embed@latest -p openclaw daemon status
|
||||
|
||||
# View logs for errors
|
||||
tail -f ~/.hindsight/profiles/openclaw.log
|
||||
|
||||
# Check configuration
|
||||
cat ~/.hindsight/profiles/openclaw.env
|
||||
|
||||
# List all profiles
|
||||
uvx hindsight-embed@latest profile list
|
||||
```
|
||||
|
||||
### No API key error
|
||||
|
||||
Make sure you've set one of the provider API keys (or use a provider that doesn't require one):
|
||||
|
||||
```bash
|
||||
# Option 1: OpenAI
|
||||
export OPENAI_API_KEY="sk-your-key"
|
||||
|
||||
# Option 2: Anthropic
|
||||
export ANTHROPIC_API_KEY="your-key"
|
||||
|
||||
# Option 3: Claude Code (no API key needed)
|
||||
export HINDSIGHT_API_LLM_PROVIDER=claude-code
|
||||
|
||||
# Option 4: OpenAI Codex (no API key needed)
|
||||
export HINDSIGHT_API_LLM_PROVIDER=openai-codex
|
||||
|
||||
# Verify it's set
|
||||
echo $OPENAI_API_KEY
|
||||
# or
|
||||
echo $HINDSIGHT_API_LLM_PROVIDER
|
||||
```
|
||||
|
||||
### Verify it's working
|
||||
|
||||
Check gateway logs for memory operations:
|
||||
|
||||
```bash
|
||||
tail -f /tmp/openclaw/openclaw-*.log | grep Hindsight
|
||||
|
||||
# Should see on startup:
|
||||
# [Hindsight] ✓ Using provider: openai, model: gpt-4o-mini
|
||||
# or
|
||||
# [Hindsight] ✓ Using provider: claude-code, model: claude-sonnet-4-20250514
|
||||
|
||||
# Should see after conversations:
|
||||
# [Hindsight] Retained X messages for session ...
|
||||
# [Hindsight] Auto-recall: Injecting X memories
|
||||
```
|
||||
@@ -0,0 +1,323 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
# Skills
|
||||
|
||||
Hindsight provides an Agent Skill that gives AI coding assistants persistent memory across sessions. Skills are reusable prompt templates that agents can load when needed to gain specialized capabilities.
|
||||
|
||||
## Supported Platforms
|
||||
|
||||
| Platform | Skills Directory |
|
||||
|----------|-----------------|
|
||||
| [Claude Code](https://docs.anthropic.com/en/docs/claude-code) | `~/.claude/skills/` |
|
||||
| [OpenCode](https://github.com/opencode-ai/opencode) | `~/.opencode/skills/` |
|
||||
| [Codex CLI](https://github.com/openai/codex) | `~/.codex/skills/` |
|
||||
|
||||
## Deployment Modes
|
||||
|
||||
The skill supports two deployment modes:
|
||||
|
||||
| Mode | Best For | Data Location |
|
||||
|------|----------|---------------|
|
||||
| **Local** | Individual developers | Your machine (`~/.pg0/`) |
|
||||
| **Cloud** | Teams sharing knowledge | Hindsight Cloud |
|
||||
|
||||
## Quick Install
|
||||
|
||||
### Option 1: Interactive Installer (Recommended)
|
||||
|
||||
```bash
|
||||
curl -fsSL https://hindsight.vectorize.io/get-skill | bash
|
||||
```
|
||||
|
||||
The installer will:
|
||||
1. Prompt you to select your AI coding assistant
|
||||
2. Select deployment mode (local or cloud)
|
||||
3. Configure the appropriate settings
|
||||
4. Install the skill to the appropriate directory
|
||||
|
||||
### Install for a Specific Platform
|
||||
|
||||
```bash
|
||||
# Claude Code (interactive mode selection)
|
||||
curl -fsSL https://hindsight.vectorize.io/get-skill | bash -s -- --app claude
|
||||
|
||||
# OpenCode
|
||||
curl -fsSL https://hindsight.vectorize.io/get-skill | bash -s -- --app opencode
|
||||
|
||||
# Codex CLI
|
||||
curl -fsSL https://hindsight.vectorize.io/get-skill | bash -s -- --app codex
|
||||
```
|
||||
|
||||
### Install with Cloud Mode
|
||||
|
||||
```bash
|
||||
# Direct cloud setup (skips interactive prompts for mode)
|
||||
curl -fsSL https://hindsight.vectorize.io/get-skill | bash -s -- --app claude --mode cloud
|
||||
```
|
||||
|
||||
### Option 2: Using add-skill
|
||||
|
||||
If you use [add-skill](https://add-skill.org/) to manage your agent skills:
|
||||
|
||||
```bash
|
||||
# For local mode (individual developers)
|
||||
npx add-skill vectorize-io/hindsight --skill hindsight-local
|
||||
|
||||
# For Hindsight Cloud (teams)
|
||||
npx add-skill vectorize-io/hindsight --skill hindsight-cloud
|
||||
|
||||
# For self-hosted Hindsight servers
|
||||
npx add-skill vectorize-io/hindsight --skill hindsight-self-hosted
|
||||
```
|
||||
|
||||
On first use, the AI will guide you through the remaining setup:
|
||||
- **Local**: Run `uvx hindsight-embed configure` to set up your LLM provider
|
||||
- **Cloud**: Provide your API key and bank ID
|
||||
- **Self-hosted**: Provide your server URL, API key, and bank ID
|
||||
|
||||
## What the Skill Provides
|
||||
|
||||
Once installed, your AI assistant gains the ability to:
|
||||
|
||||
- **Retain** - Store user preferences, learnings, and procedure outcomes
|
||||
- **Recall** - Search for relevant context before starting tasks
|
||||
- **Reflect** - Synthesize memories into contextual answers
|
||||
|
||||
The skill uses the `hindsight-embed` CLI which runs a lightweight local daemon with an embedded database.
|
||||
|
||||
## How Skills Work
|
||||
|
||||
Skills are **model-invoked**, meaning the AI assistant automatically decides when to use them based on the context of your conversation. You don't need to explicitly trigger the skill.
|
||||
|
||||
The assistant will:
|
||||
- **Store** when you share preferences, when tasks succeed/fail, or when learnings emerge
|
||||
- **Recall** before starting non-trivial tasks to get relevant context
|
||||
|
||||
### What Gets Stored
|
||||
|
||||
The skill is optimized to store:
|
||||
|
||||
| Category | Examples |
|
||||
|----------|----------|
|
||||
| **User Preferences** | Coding style, tool preferences, language choices |
|
||||
| **Procedure Outcomes** | Commands that worked, configurations that resolved issues |
|
||||
| **Learnings** | Bug solutions, workarounds, architecture decisions |
|
||||
|
||||
## Architecture
|
||||
|
||||
### Local Mode
|
||||
|
||||
```
|
||||
AI Coding Assistant
|
||||
│
|
||||
▼
|
||||
Hindsight Skill (SKILL.md)
|
||||
│
|
||||
▼
|
||||
hindsight-embed CLI
|
||||
│
|
||||
▼
|
||||
Local Daemon (auto-started)
|
||||
│
|
||||
▼
|
||||
Embedded PostgreSQL (~/.pg0/hindsight-embed/)
|
||||
```
|
||||
|
||||
All data stays on your machine. The daemon auto-starts when needed and shuts down after inactivity.
|
||||
|
||||
### Cloud Mode
|
||||
|
||||
```
|
||||
AI Coding Assistant
|
||||
│
|
||||
▼
|
||||
Hindsight Skill (SKILL.md)
|
||||
│
|
||||
▼
|
||||
hindsight-cli
|
||||
│
|
||||
▼
|
||||
Hindsight Cloud API (https://api.hindsight.vectorize.io)
|
||||
│
|
||||
▼
|
||||
Shared Memory Bank (team-accessible)
|
||||
```
|
||||
|
||||
Data is stored in Hindsight Cloud and shared across your team. All team members with the same bank ID can access shared memories.
|
||||
|
||||
---
|
||||
|
||||
## Local Mode Setup
|
||||
|
||||
The skill uses configuration stored in `~/.hindsight/config.env`. Reconfigure anytime:
|
||||
|
||||
```bash
|
||||
uvx hindsight-embed configure
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cloud Mode Setup
|
||||
|
||||
Cloud mode connects to [Hindsight Cloud](https://vectorize.io/hindsight/cloud), allowing teams to share memories about a codebase. When one team member learns something, everyone benefits.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. A Hindsight Cloud account ([request access](https://vectorize.io/hindsight/cloud))
|
||||
2. An API key from your team admin
|
||||
3. A bank ID for your project (e.g., `team-acme-frontend`)
|
||||
|
||||
### Installation
|
||||
|
||||
Run the installer with cloud mode:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://hindsight.vectorize.io/get-skill | bash -s -- --mode cloud
|
||||
```
|
||||
|
||||
You'll be prompted for:
|
||||
|
||||
| Setting | Description | Example |
|
||||
|---------|-------------|---------|
|
||||
| **Cloud API URL** | Hindsight Cloud endpoint | `https://api.hindsight.vectorize.io` |
|
||||
| **API Key** | Your authentication key | `hs_xxx...` |
|
||||
| **Bank ID** | Shared memory bank for your team | `team-acme-frontend` |
|
||||
|
||||
### Configuration Files
|
||||
|
||||
Cloud mode creates two files:
|
||||
|
||||
**`~/.hindsight/config`** — API connection settings (TOML format):
|
||||
```toml
|
||||
api_url = "https://api.hindsight.vectorize.io"
|
||||
api_key = "hs_xxx..."
|
||||
```
|
||||
|
||||
**`~/.claude/skills/hindsight/SKILL.md`** — Skill definition with your bank ID baked in.
|
||||
|
||||
### Team Setup
|
||||
|
||||
To set up cloud mode for your team:
|
||||
|
||||
1. **Team admin** creates a bank in Hindsight Cloud (e.g., `team-acme-frontend`)
|
||||
2. **Team admin** generates API keys for each team member
|
||||
3. **Each developer** runs the installer with their API key and the shared bank ID
|
||||
4. All team members now share the same memory bank
|
||||
|
||||
### What to Store in Team Banks
|
||||
|
||||
Cloud mode uses a **shared team bank**. Be thoughtful about what goes in:
|
||||
|
||||
| Type | Examples | How to Store |
|
||||
|------|----------|--------------|
|
||||
| **Project conventions** | Linting rules, testing requirements, Node version | `"Project uses ESLint with Airbnb config"` |
|
||||
| **Team knowledge** | Architecture decisions, common pitfalls, domain logic | `"Auth module requires Redis 7+"` |
|
||||
| **Individual preferences** | Personal coding style, communication preferences | `"Alice prefers verbose commit messages"` |
|
||||
|
||||
**Key distinction**: Project conventions apply to everyone. Individual preferences should include the person's name so the AI knows when to apply them.
|
||||
|
||||
### Example Workflow
|
||||
|
||||
```
|
||||
Day 1: Alice discovers a requirement
|
||||
─────────────────────────────────────
|
||||
Alice's AI assistant stores:
|
||||
"The auth module requires Redis 7+ due to HEXPIRE command usage"
|
||||
"Alice prefers explicit error handling over silent failures"
|
||||
|
||||
Day 2: Bob starts working on auth
|
||||
─────────────────────────────────
|
||||
Bob's AI assistant recalls:
|
||||
"The auth module requires Redis 7+ due to HEXPIRE command usage"
|
||||
|
||||
Bob avoids the same issue Alice hit!
|
||||
(Alice's personal preference is stored but won't be applied to Bob)
|
||||
```
|
||||
|
||||
### Testing Cloud Connection
|
||||
|
||||
After installation, verify the connection:
|
||||
|
||||
```bash
|
||||
# Store a test memory
|
||||
hindsight memory retain team-acme-frontend "Alice works at Google as a software engineer"
|
||||
|
||||
# Recall it
|
||||
hindsight memory recall team-acme-frontend "Alice"
|
||||
```
|
||||
|
||||
### Switching Between Banks
|
||||
|
||||
If you work on multiple projects, you can have different skills installed for each AI assistant, or manually switch banks:
|
||||
|
||||
```bash
|
||||
# Environment variable override (temporary)
|
||||
HINDSIGHT_API_URL=https://api.hindsight.vectorize.io \
|
||||
HINDSIGHT_API_KEY=hs_xxx \
|
||||
hindsight memory recall different-bank "query"
|
||||
```
|
||||
|
||||
For permanent multi-bank setups, reinstall the skill with a different bank ID.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Skill not activating
|
||||
|
||||
The skill activates based on its description matching your request. Try being explicit:
|
||||
- "Remember that..." triggers storage
|
||||
- "What do you know about..." triggers recall
|
||||
|
||||
### Local Mode Issues
|
||||
|
||||
**Daemon not starting:**
|
||||
```bash
|
||||
uvx hindsight-embed daemon status
|
||||
uvx hindsight-embed daemon logs
|
||||
```
|
||||
|
||||
**Reconfigure LLM provider:**
|
||||
```bash
|
||||
uvx hindsight-embed configure
|
||||
```
|
||||
|
||||
### Cloud Mode Issues
|
||||
|
||||
**Authentication errors:**
|
||||
```bash
|
||||
# Verify your config
|
||||
cat ~/.hindsight/config
|
||||
|
||||
# Test connection manually
|
||||
hindsight bank list
|
||||
```
|
||||
|
||||
**Wrong bank ID:**
|
||||
|
||||
Check your SKILL.md file to see which bank ID is configured:
|
||||
```bash
|
||||
cat ~/.claude/skills/hindsight/SKILL.md | grep "memory retain"
|
||||
```
|
||||
|
||||
To change the bank ID, reinstall the skill:
|
||||
```bash
|
||||
curl -fsSL https://hindsight.vectorize.io/get-skill | bash -s -- --mode cloud
|
||||
```
|
||||
|
||||
**Network/firewall issues:**
|
||||
```bash
|
||||
# Test connectivity to cloud API
|
||||
curl -I https://api.hindsight.vectorize.io/health
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
### Local Mode
|
||||
- Python 3.10+ (for `uvx`)
|
||||
- An LLM API key (OpenAI, Anthropic, Groq, etc.)
|
||||
|
||||
### Cloud Mode
|
||||
- Python 3.10+ (for `uvx`)
|
||||
- Hindsight Cloud API key
|
||||
- Network access to `https://api.hindsight.vectorize.io`
|
||||
@@ -0,0 +1,129 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
# Node.js Client
|
||||
|
||||
Official TypeScript/JavaScript client for the Hindsight API.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @vectorize-io/hindsight-client
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```typescript
|
||||
const { HindsightClient } = require('@vectorize-io/hindsight-client');
|
||||
|
||||
const client = new HindsightClient({ baseUrl: 'http://localhost:8888' });
|
||||
|
||||
// Retain a memory
|
||||
await client.retain('my-bank', 'Alice works at Google');
|
||||
|
||||
// Recall memories
|
||||
const response = await client.recall('my-bank', 'What does Alice do?');
|
||||
for (const r of response.results) {
|
||||
console.log(r.text);
|
||||
}
|
||||
|
||||
// Reflect - generate response with disposition
|
||||
const answer = await client.reflect('my-bank', 'Tell me about Alice');
|
||||
console.log(answer.text);
|
||||
```
|
||||
|
||||
## Client Initialization
|
||||
|
||||
```typescript
|
||||
import { HindsightClient } from '@vectorize-io/hindsight-client';
|
||||
|
||||
const client = new HindsightClient({
|
||||
baseUrl: 'http://localhost:8888',
|
||||
});
|
||||
```
|
||||
|
||||
## Core Operations
|
||||
|
||||
### Retain (Store Memory)
|
||||
|
||||
```typescript
|
||||
// Simple
|
||||
await client.retain('my-bank', 'Alice works at Google');
|
||||
|
||||
// With options
|
||||
await client.retain('my-bank', 'Alice got promoted', {
|
||||
timestamp: new Date('2024-01-15'),
|
||||
context: 'career update',
|
||||
metadata: { source: 'slack' },
|
||||
async: false, // Set true for background processing
|
||||
});
|
||||
```
|
||||
|
||||
### Retain Batch
|
||||
|
||||
```typescript
|
||||
await client.retainBatch('my-bank', [
|
||||
{ content: 'Alice works at Google', context: 'career' },
|
||||
{ content: 'Bob is a data scientist', context: 'career' },
|
||||
], {
|
||||
async: false,
|
||||
});
|
||||
```
|
||||
|
||||
### Recall (Search)
|
||||
|
||||
```typescript
|
||||
// Simple - returns RecallResponse
|
||||
const response = await client.recall('my-bank', 'What does Alice do?');
|
||||
|
||||
for (const r of response.results) {
|
||||
console.log(`${r.text} (type: ${r.type})`);
|
||||
}
|
||||
|
||||
// With options
|
||||
const response = await client.recall('my-bank', 'What does Alice do?', {
|
||||
types: ['world', 'observation'], // Filter by fact type
|
||||
maxTokens: 4096,
|
||||
budget: 'high', // 'low', 'mid', or 'high'
|
||||
});
|
||||
```
|
||||
|
||||
### Reflect (Generate Response)
|
||||
|
||||
```typescript
|
||||
const answer = await client.reflect('my-bank', 'What should I know about Alice?', {
|
||||
budget: 'low', // 'low', 'mid', or 'high'
|
||||
context: 'preparing for a meeting',
|
||||
});
|
||||
|
||||
console.log(answer.text); // Generated response
|
||||
```
|
||||
|
||||
## Bank Management
|
||||
|
||||
### Create Bank
|
||||
|
||||
```typescript
|
||||
await client.createBank('my-bank', {
|
||||
name: 'Assistant',
|
||||
mission: "You're a helpful AI assistant - keep track of user preferences and conversation history.",
|
||||
disposition: {
|
||||
skepticism: 3, // 1-5: trusting to skeptical
|
||||
literalism: 3, // 1-5: flexible to literal
|
||||
empathy: 3, // 1-5: detached to empathetic
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### List Memories
|
||||
|
||||
```typescript
|
||||
const response = await client.listMemories('my-bank', {
|
||||
type: 'world', // Optional filter
|
||||
q: 'Alice', // Optional text search
|
||||
limit: 100,
|
||||
offset: 0,
|
||||
});
|
||||
console.log(response)
|
||||
```
|
||||
@@ -0,0 +1,361 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# Python Client
|
||||
|
||||
Official Python client for the Hindsight API.
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
## Installation
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="all-in-one" label="All-in-One (Recommended)">
|
||||
|
||||
The `hindsight-all` package includes embedded PostgreSQL, HTTP API server, and client:
|
||||
|
||||
```bash
|
||||
pip install hindsight-all
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="client-only" label="Client Only">
|
||||
|
||||
If you already have a Hindsight server running:
|
||||
|
||||
```bash
|
||||
pip install hindsight-client
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Quick Start
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="all-in-one" label="All-in-One">
|
||||
|
||||
```python
|
||||
import os
|
||||
from hindsight import HindsightServer, HindsightClient
|
||||
|
||||
with HindsightServer(
|
||||
llm_provider="openai",
|
||||
llm_model="gpt-4o-mini",
|
||||
llm_api_key=os.environ["OPENAI_API_KEY"]
|
||||
) as server:
|
||||
client = HindsightClient(base_url=server.url)
|
||||
|
||||
# Retain a memory
|
||||
client.retain(bank_id="my-bank", content="Alice works at Google")
|
||||
|
||||
# Recall memories
|
||||
results = client.recall(bank_id="my-bank", query="What does Alice do?")
|
||||
for r in results:
|
||||
print(r.text)
|
||||
|
||||
# Reflect - generate response with disposition
|
||||
answer = client.reflect(bank_id="my-bank", query="Tell me about Alice")
|
||||
print(answer.text)
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="client-only" label="Client Only">
|
||||
|
||||
```python
|
||||
from hindsight_client import Hindsight
|
||||
|
||||
client = Hindsight(base_url="http://localhost:8888")
|
||||
|
||||
# Retain a memory
|
||||
client.retain(bank_id="my-bank", content="Alice works at Google")
|
||||
|
||||
# Recall memories
|
||||
results = client.recall(bank_id="my-bank", query="What does Alice do?")
|
||||
for r in results:
|
||||
print(r.text)
|
||||
|
||||
# Reflect - generate response with disposition
|
||||
answer = client.reflect(bank_id="my-bank", query="Tell me about Alice")
|
||||
print(answer.text)
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Embedded Client (Easiest Option)
|
||||
|
||||
`HindsightEmbedded` provides the simplest way to use Hindsight in Python. It automatically manages a background server for you - no manual setup required:
|
||||
|
||||
```python
|
||||
from hindsight import HindsightEmbedded
|
||||
import os
|
||||
|
||||
# Server starts automatically on first use
|
||||
client = HindsightEmbedded(
|
||||
profile="myapp", # Profile for data isolation
|
||||
llm_provider="openai",
|
||||
llm_model="gpt-4o-mini",
|
||||
llm_api_key=os.environ["OPENAI_API_KEY"],
|
||||
)
|
||||
|
||||
# Use immediately - no manual server management needed
|
||||
client.retain(bank_id="my-bank", content="Alice works at Google")
|
||||
results = client.recall(bank_id="my-bank", query="What does Alice do?")
|
||||
|
||||
# Server continues running (auto-stops after idle timeout)
|
||||
# Or explicitly stop it:
|
||||
client.close(stop_daemon=True)
|
||||
```
|
||||
|
||||
**What's a Profile?**
|
||||
|
||||
A profile is an isolated Hindsight environment. Each profile gets its own PostgreSQL database (stored in `~/.pg0/instances/hindsight-embed-{profile}/`) and its own API server. Use different profiles to separate environments (dev/prod), applications, or users.
|
||||
|
||||
**When to Use HindsightEmbedded**
|
||||
|
||||
Use `HindsightEmbedded` when you want the server to start automatically and manage itself. Use `HindsightServer` when you need explicit control over server lifecycle (e.g., testing where you want immediate startup/shutdown).
|
||||
|
||||
**Advanced Operations**
|
||||
|
||||
`HindsightEmbedded` provides organized API namespaces for advanced operations. Each method call automatically ensures the daemon is running:
|
||||
|
||||
```python
|
||||
from hindsight import HindsightEmbedded
|
||||
import os
|
||||
|
||||
embedded = HindsightEmbedded(
|
||||
profile="myapp",
|
||||
llm_provider="openai",
|
||||
llm_api_key=os.environ["OPENAI_API_KEY"],
|
||||
)
|
||||
|
||||
# Core operations (automatically proxied)
|
||||
embedded.retain(bank_id="test", content="Hello")
|
||||
results = embedded.recall(bank_id="test", query="Hello")
|
||||
|
||||
# Bank management
|
||||
embedded.banks.create(bank_id="test", name="Test Bank", mission="Help users")
|
||||
embedded.banks.set_mission(bank_id="test", mission="Updated mission")
|
||||
embedded.banks.delete(bank_id="test")
|
||||
|
||||
# Mental models
|
||||
embedded.mental_models.create(
|
||||
bank_id="test",
|
||||
name="User Preferences",
|
||||
content="User prefers dark mode"
|
||||
)
|
||||
models = embedded.mental_models.list(bank_id="test")
|
||||
embedded.mental_models.update(bank_id="test", mental_model_id="...", content="New content")
|
||||
|
||||
# Directives
|
||||
embedded.directives.create(
|
||||
bank_id="test",
|
||||
name="Response Style",
|
||||
content="Be concise and friendly"
|
||||
)
|
||||
directives = embedded.directives.list(bank_id="test")
|
||||
|
||||
# List memories
|
||||
memories = embedded.memories.list(bank_id="test", type="world", limit=50)
|
||||
```
|
||||
|
||||
**Why Use API Namespaces?**
|
||||
|
||||
API namespaces (`banks`, `mental_models`, `directives`, `memories`) ensure the daemon is running before each call. This handles daemon crashes gracefully:
|
||||
|
||||
```python
|
||||
# ✅ GOOD - Uses API namespace (daemon restarts handled)
|
||||
embedded.banks.create(bank_id="test", name="Test")
|
||||
|
||||
# ❌ BAD - Direct client access (daemon crashes NOT handled)
|
||||
client = embedded.client
|
||||
client.create_bank(bank_id="test", name="Test") # Fails if daemon crashed
|
||||
```
|
||||
|
||||
## Client Initialization
|
||||
|
||||
```python
|
||||
from hindsight import HindsightClient
|
||||
|
||||
client = HindsightClient(
|
||||
base_url="http://localhost:8888", # Hindsight API URL
|
||||
timeout=30.0, # Request timeout in seconds
|
||||
)
|
||||
|
||||
# Core operations
|
||||
client.retain(bank_id="test", content="Hello world")
|
||||
results = client.recall(bank_id="test", query="Hello")
|
||||
|
||||
# Organized API access (same as HindsightEmbedded)
|
||||
client.banks.create(bank_id="test", name="Test Bank")
|
||||
models = client.mental_models.list(bank_id="test")
|
||||
directives = client.directives.list(bank_id="test")
|
||||
memories = client.memories.list(bank_id="test")
|
||||
```
|
||||
|
||||
Both `HindsightClient` and `HindsightEmbedded` provide the same organized API namespaces (`banks`, `mental_models`, `directives`, `memories`) for consistent developer experience.
|
||||
|
||||
## Core Operations
|
||||
|
||||
### Retain (Store Memory)
|
||||
|
||||
```python
|
||||
# Simple
|
||||
client.retain(
|
||||
bank_id="my-bank",
|
||||
content="Alice works at Google as a software engineer",
|
||||
)
|
||||
|
||||
# With options
|
||||
from datetime import datetime
|
||||
|
||||
client.retain(
|
||||
bank_id="my-bank",
|
||||
content="Alice got promoted",
|
||||
context="career update",
|
||||
timestamp=datetime(2024, 1, 15),
|
||||
document_id="conversation_001",
|
||||
metadata={"source": "slack"},
|
||||
)
|
||||
```
|
||||
|
||||
### Retain Batch
|
||||
|
||||
```python
|
||||
client.retain_batch(
|
||||
bank_id="my-bank",
|
||||
items=[
|
||||
{"content": "Alice works at Google", "context": "career"},
|
||||
{"content": "Bob is a data scientist", "context": "career"},
|
||||
],
|
||||
document_id="conversation_001",
|
||||
retain_async=False, # Set True for background processing
|
||||
)
|
||||
```
|
||||
|
||||
### Recall (Search)
|
||||
|
||||
```python
|
||||
# Simple - returns list of RecallResult
|
||||
results = client.recall(
|
||||
bank_id="my-bank",
|
||||
query="What does Alice do?",
|
||||
)
|
||||
|
||||
for r in results.results:
|
||||
print(f"{r.text} (type: {r.type})")
|
||||
|
||||
# With options
|
||||
results = client.recall(
|
||||
bank_id="my-bank",
|
||||
query="What does Alice do?",
|
||||
types=["world", "observation"], # Filter by fact type
|
||||
max_tokens=4096,
|
||||
budget="high", # low, mid, or high
|
||||
)
|
||||
```
|
||||
|
||||
### Recall with Chunks
|
||||
|
||||
```python
|
||||
# Returns RecallResponse with source chunks
|
||||
response = client.recall(
|
||||
bank_id="my-bank",
|
||||
query="What does Alice do?",
|
||||
types=["world", "experience"],
|
||||
budget="mid",
|
||||
max_tokens=4096,
|
||||
include_chunks=True,
|
||||
max_chunk_tokens=500
|
||||
)
|
||||
|
||||
print(f"Found {len(response.results)} memories")
|
||||
for r in response.results:
|
||||
print(f" - {r.text}")
|
||||
if r.chunks:
|
||||
print(f" Source: {r.chunks[0].text[:100]}...")
|
||||
```
|
||||
|
||||
### Reflect (Generate Response)
|
||||
|
||||
```python
|
||||
answer = client.reflect(
|
||||
bank_id="my-bank",
|
||||
query="What should I know about Alice?",
|
||||
budget="low", # low, mid, or high
|
||||
context="preparing for a meeting",
|
||||
)
|
||||
|
||||
print(answer.text) # Generated response
|
||||
```
|
||||
|
||||
## Bank Management
|
||||
|
||||
### Create Bank
|
||||
|
||||
```python
|
||||
client.create_bank(
|
||||
bank_id="my-bank",
|
||||
name="Assistant",
|
||||
mission="You're a helpful AI assistant - keep track of user preferences and conversation history.",
|
||||
disposition={
|
||||
"skepticism": 3, # 1-5: trusting to skeptical
|
||||
"literalism": 3, # 1-5: flexible to literal
|
||||
"empathy": 3, # 1-5: detached to empathetic
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
### List Memories
|
||||
|
||||
```python
|
||||
client.list_memories(
|
||||
bank_id="my-bank",
|
||||
type="world", # Optional: filter by type
|
||||
search_query="Alice", # Optional: text search
|
||||
limit=100,
|
||||
offset=0,
|
||||
)
|
||||
```
|
||||
|
||||
## Async Support
|
||||
|
||||
All methods have async versions prefixed with `a`:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from hindsight_client import Hindsight
|
||||
|
||||
async def main():
|
||||
client = Hindsight(base_url="http://localhost:8888")
|
||||
|
||||
# Async retain
|
||||
await client.aretain(bank_id="my-bank", content="Hello world")
|
||||
|
||||
# Async recall
|
||||
results = await client.arecall(bank_id="my-bank", query="Hello")
|
||||
for r in results:
|
||||
print(r.text)
|
||||
|
||||
# Async reflect
|
||||
answer = await client.areflect(bank_id="my-bank", query="What did I say?")
|
||||
print(answer.text)
|
||||
|
||||
client.close()
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## Context Manager
|
||||
|
||||
```python
|
||||
from hindsight_client import Hindsight
|
||||
|
||||
with Hindsight(base_url="http://localhost:8888") as client:
|
||||
client.retain(bank_id="my-bank", content="Hello")
|
||||
results = client.recall(bank_id="my-bank", query="Hello")
|
||||
# Client automatically closed
|
||||
```
|
||||
Reference in New Issue
Block a user