Compare commits
37
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06d11cf867 | ||
|
|
7f5576cdee | ||
|
|
3572387051 | ||
|
|
3a91c0b87f | ||
|
|
706204bc4f | ||
|
|
147d46fc91 | ||
|
|
285bed65f9 | ||
|
|
c85a1ca58b | ||
|
|
583683b0a2 | ||
|
|
0d5503c892 | ||
|
|
af2756f2da | ||
|
|
b1e380bdae | ||
|
|
b8ec743962 | ||
|
|
d891124835 | ||
|
|
04ff24be8d | ||
|
|
076c33e854 | ||
|
|
a03c942296 | ||
|
|
b4e42bd0c6 | ||
|
|
95b2b7e78f | ||
|
|
2c1be4cf47 | ||
|
|
f148d3e338 | ||
|
|
99db7b26c3 | ||
|
|
ebc85a5c3d | ||
|
|
ae30882ec9 | ||
|
|
fa554b8980 | ||
|
|
f813a807e7 | ||
|
|
2aa8700db8 | ||
|
|
c0093f1a97 | ||
|
|
460f045f16 | ||
|
|
0673d4813d | ||
|
|
f7e8b1097b | ||
|
|
522a491fc1 | ||
|
|
1056a20e71 | ||
|
|
01ba9744e5 | ||
|
|
44e79feb3e | ||
|
|
94665b2111 | ||
|
|
f42476bf94 |
+2
-1
@@ -2,8 +2,9 @@
|
||||
# Copy this file to .env and fill in your values
|
||||
|
||||
# LLM Configuration (Required)
|
||||
HINDSIGHT_API_LLM_PROVIDER=openai
|
||||
HINDSIGHT_API_LLM_API_KEY=your-api-key-here
|
||||
HINDSIGHT_API_LLM_MODEL=gpt-4o-mini
|
||||
HINDSIGHT_API_LLM_MODEL=o3-mini
|
||||
HINDSIGHT_API_LLM_BASE_URL=https://api.openai.com/v1
|
||||
|
||||
# API Configuration (Optional)
|
||||
|
||||
@@ -219,6 +219,9 @@ jobs:
|
||||
|
||||
release-helm-chart:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -228,12 +231,18 @@ jobs:
|
||||
with:
|
||||
version: 'latest'
|
||||
|
||||
- name: Log in to GHCR
|
||||
run: echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io -u ${{ github.actor }} --password-stdin
|
||||
|
||||
- name: Lint Helm chart
|
||||
run: helm lint helm/hindsight
|
||||
|
||||
- name: Package Helm chart
|
||||
run: helm package helm/hindsight --destination ./helm-packages
|
||||
|
||||
- name: Push to GHCR OCI
|
||||
run: helm push helm-packages/*.tgz oci://ghcr.io/${{ github.repository_owner }}/charts
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
|
||||
@@ -108,6 +108,8 @@ jobs:
|
||||
HINDSIGHT_API_LLM_API_KEY: ${{ secrets.GROQ_API_KEY }}
|
||||
HINDSIGHT_API_LLM_MODEL: openai/gpt-oss-20b
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Prefer CPU-only PyTorch in CI (but keep PyPI for everything else)
|
||||
UV_INDEX: pytorch=https://download.pytorch.org/whl/cpu
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -132,7 +134,27 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ./hindsight-api
|
||||
run: uv sync --extra test
|
||||
run: uv sync --extra test --no-install-project --index-strategy unsafe-best-match
|
||||
|
||||
- name: Cache HuggingFace models
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/huggingface
|
||||
key: ${{ runner.os }}-huggingface-${{ hashFiles('hindsight-api/pyproject.toml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-huggingface-
|
||||
|
||||
- name: Pre-download models
|
||||
working-directory: ./hindsight-api
|
||||
run: |
|
||||
uv run python -c "
|
||||
from sentence_transformers import SentenceTransformer, CrossEncoder
|
||||
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('Models downloaded successfully')
|
||||
"
|
||||
|
||||
- name: Run tests
|
||||
working-directory: ./hindsight-api
|
||||
@@ -146,6 +168,8 @@ jobs:
|
||||
HINDSIGHT_API_LLM_MODEL: openai/gpt-oss-20b
|
||||
HINDSIGHT_API_URL: http://localhost:8888
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Prefer CPU-only PyTorch in CI (but keep PyPI for everything else)
|
||||
UV_INDEX: pytorch=https://download.pytorch.org/whl/cpu
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -174,11 +198,11 @@ jobs:
|
||||
|
||||
- name: Install client test dependencies
|
||||
working-directory: ./hindsight-clients/python
|
||||
run: uv sync --extra test
|
||||
run: uv sync --extra test --index-strategy unsafe-best-match
|
||||
|
||||
- name: Install API dependencies
|
||||
working-directory: ./hindsight-api
|
||||
run: uv sync
|
||||
run: uv sync --no-install-project --index-strategy unsafe-best-match
|
||||
|
||||
- name: Create .env file
|
||||
run: |
|
||||
@@ -223,6 +247,8 @@ jobs:
|
||||
HINDSIGHT_API_LLM_MODEL: openai/gpt-oss-20b
|
||||
HINDSIGHT_API_URL: http://localhost:8888
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Prefer CPU-only PyTorch in CI (but keep PyPI for everything else)
|
||||
UV_INDEX: pytorch=https://download.pytorch.org/whl/cpu
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -252,7 +278,7 @@ jobs:
|
||||
|
||||
- name: Install API dependencies
|
||||
working-directory: ./hindsight-api
|
||||
run: uv sync
|
||||
run: uv sync --no-install-project --index-strategy unsafe-best-match
|
||||
|
||||
- name: Install TypeScript client dependencies
|
||||
working-directory: ./hindsight-clients/typescript
|
||||
@@ -305,6 +331,8 @@ jobs:
|
||||
HINDSIGHT_API_LLM_MODEL: openai/gpt-oss-20b
|
||||
HINDSIGHT_API_URL: http://localhost:8888
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Prefer CPU-only PyTorch in CI (but keep PyPI for everything else)
|
||||
UV_INDEX: pytorch=https://download.pytorch.org/whl/cpu
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -341,7 +369,7 @@ jobs:
|
||||
|
||||
- name: Install API dependencies
|
||||
working-directory: ./hindsight-api
|
||||
run: uv sync
|
||||
run: uv sync --no-install-project --index-strategy unsafe-best-match
|
||||
|
||||
- name: Create .env file
|
||||
run: |
|
||||
|
||||
@@ -145,3 +145,7 @@ Note: The maintained wrapper `hindsight_client.py` and `README.md` are preserved
|
||||
- PostgreSQL with pgvector extension
|
||||
- Schema managed via Alembic migrations in `hindsight-api/alembic/`, db migrations happen during api startup, no manual commands
|
||||
- Key tables: `banks`, `memory_units`, `documents`, `entities`, `entity_links`
|
||||
|
||||
# Branding
|
||||
## Colors
|
||||
- Primary: gradient from #0074d9 to #009296
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<div align="center">
|
||||
|
||||
# Hindsight
|
||||

|
||||
|
||||
**Agent Memory that Works Like Human Memory**
|
||||
[Documentation](https://vectorize-io.github.io/hindsight) • [Paper](#coming-soon) • [Examples](https://github.com/vectorize-io/hindsight-cookbook)
|
||||
|
||||
[](https://github.com/vectorize-io/hindsight/actions/workflows/test.yml)
|
||||
[](https://github.com/vectorize-io/hindsight/actions/workflows/release.yml)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://pypi.org/project/hindsight-api/)
|
||||
[](https://pypi.org/project/hindsight-client/)
|
||||
[](https://www.npmjs.com/package/@vectorize-io/hindsight-client)
|
||||
[](https://www.npmjs.com/package/@vectorize-io/hindsight-client)
|
||||
[](https://join.slack.com/t/hindsight-space/shared_invite/zt-3klo21kua-VUCC_zHP5rIcXFB1_5yw6A)
|
||||
|
||||
[Documentation](https://vectorize-io.github.io/hindsight) • [Paper](./Hindsight.pdf) • [Examples](./examples)
|
||||
|
||||
</div>
|
||||
|
||||
@@ -28,7 +28,7 @@ Hindsight addresses common challenges that have frustrated AI engineers building
|
||||
|
||||
## How Hindsight Works
|
||||
|
||||

|
||||

|
||||
|
||||
Hindsight organizes memory into four networks to mimic the way human memory works:
|
||||
|
||||
@@ -54,12 +54,11 @@ Memories in Hindsight are stored in banks (e.g. memory banks). When memories are
|
||||
```bash
|
||||
export OPENAI_API_KEY=your-key
|
||||
|
||||
docker run -p 8888:8888 -p 9999:9999 \
|
||||
-e HINDSIGHT_API_LLM_PROVIDER=openai \
|
||||
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 \
|
||||
-e HINDSIGHT_API_LLM_MODEL=o3-mini \
|
||||
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
|
||||
ghcr.io/vectorize-io/hindsight
|
||||
ghcr.io/vectorize-io/hindsight:latest
|
||||
```
|
||||
|
||||
API: http://localhost:8888
|
||||
@@ -68,7 +67,7 @@ UI: http://localhost:9999
|
||||
Install client:
|
||||
|
||||
```bash
|
||||
pip install hindsight-client
|
||||
pip install hindsight-client -U
|
||||
# or
|
||||
npm install @vectorize-io/hindsight-client
|
||||
```
|
||||
@@ -76,25 +75,24 @@ npm install @vectorize-io/hindsight-client
|
||||
Python example:
|
||||
|
||||
```python
|
||||
from hindsight import HindsightClient
|
||||
from hindsight_client import Hindsight
|
||||
|
||||
client = HindsightClient(base_url="http://localhost:8888")
|
||||
client = Hindsight(base_url="http://localhost:8888")
|
||||
|
||||
# Store
|
||||
client.retain(bank_id="my-agent", content="Alice works at Google as a software engineer")
|
||||
# Retain: Store information
|
||||
client.retain(bank_id="my-bank", content="Alice works at Google as a software engineer")
|
||||
|
||||
# Query
|
||||
results = client.recall(bank_id="my-agent", query="What does Alice do?")
|
||||
# Recall: Search memories
|
||||
client.recall(bank_id="my-bank", query="What does Alice do?")
|
||||
|
||||
# Reflect
|
||||
response = client.reflect(bank_id="my-agent", query="Tell me about Alice")
|
||||
print(response.text)
|
||||
# Reflect: Generate disposition-aware response
|
||||
client.reflect(bank_id="my-bank", query="Tell me about Alice")
|
||||
```
|
||||
|
||||
### Python (embedded, no Docker)
|
||||
|
||||
```bash
|
||||
pip install hindsight-all
|
||||
pip install hindsight-all -U
|
||||
```
|
||||
|
||||
```python
|
||||
@@ -103,27 +101,27 @@ from hindsight import HindsightServer, HindsightClient
|
||||
|
||||
with HindsightServer(
|
||||
llm_provider="openai",
|
||||
llm_model="gpt-4o-mini",
|
||||
llm_model="gpt-5-mini",
|
||||
llm_api_key=os.environ["OPENAI_API_KEY"]
|
||||
) as server:
|
||||
client = HindsightClient(base_url=server.url)
|
||||
client.retain(bank_id="my-agent", content="Alice works at Google")
|
||||
results = client.recall(bank_id="my-agent", query="Where does Alice work?")
|
||||
client.retain(bank_id="my-bank", content="Alice works at Google")
|
||||
results = client.recall(bank_id="my-bank", query="Where does Alice work?")
|
||||
```
|
||||
|
||||
### TypeScript
|
||||
### Node.js / TypeScript
|
||||
|
||||
```bash
|
||||
npm install @vectorize-io/hindsight-client
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { HindsightClient } from '@vectorize-io/hindsight-client';
|
||||
```javascript
|
||||
const { HindsightClient } = require('@vectorize-io/hindsight-client');
|
||||
|
||||
const client = new HindsightClient({ baseUrl: 'http://localhost:8888' });
|
||||
|
||||
await client.retain('my-agent', 'Alice loves hiking in Yosemite');
|
||||
const response = await client.recall('my-agent', 'What does Alice like?');
|
||||
await client.retain('my-bank', 'Alice loves hiking in Yosemite');
|
||||
await client.recall('my-bank', 'What does Alice like?');
|
||||
```
|
||||
|
||||
---
|
||||
@@ -156,7 +154,7 @@ client.retain(
|
||||
|
||||
Behind the scenes, the retain operation uses an LLM to extract key facts, temporal data, entities, and relationships. It passes these through a normalization process to transform extracted data into canonical entities, time series, and search indexes along with metadata. These representations create the pathways for accurate memory retrieval in the recall and reflect operations.
|
||||
|
||||

|
||||

|
||||
|
||||
### Recall
|
||||
|
||||
@@ -171,9 +169,7 @@ client = Hindsight(base_url="http://localhost:8888")
|
||||
client.recall(bank_id="my-bank", query="What does Alice do?")
|
||||
|
||||
# Temporal
|
||||
results = client.recall(bank_id="my-bank", query="What happened in June?")
|
||||
|
||||
|
||||
client.recall(bank_id="my-bank", query="What happened in June?")
|
||||
```
|
||||
|
||||
Recall performs 4 retrieval strategies in parallel:
|
||||
@@ -182,7 +178,7 @@ Recall performs 4 retrieval strategies in parallel:
|
||||
- Graph: Entity/temporal/causal links
|
||||
- Temporal: Time range filtering
|
||||
|
||||

|
||||

|
||||
|
||||
The individual results from the retrievals are merged, then ordered by relevance using reciprocal rank fusion and a cross-encoder reranking model.
|
||||
|
||||
@@ -208,31 +204,20 @@ client = Hindsight(base_url="http://localhost:8888")
|
||||
client.reflect(bank_id="my-bank", query="What should I know about Alice?")
|
||||
```
|
||||
|
||||

|
||||
|
||||
## Integrations
|
||||
|
||||
### Examples
|
||||
|
||||
[Examples Repo]([./examples](https://github.com/vectorize-io/hindsight-cookbook)) includes:
|
||||
|
||||
- Basic usage
|
||||
- Multi-session conversations
|
||||
- Temporal queries
|
||||
- Entity reasoning
|
||||
- Opinion tracking
|
||||
- Production setup (Docker Compose + monitoring)
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
**Documentation:** [vectorize-io.github.io/hindsight](https://vectorize-io.github.io/hindsight)
|
||||
**Documentation:**
|
||||
- [https://hindsight.vectorize.io](https://hindsight.vectorize.io)
|
||||
|
||||
**Clients:**
|
||||
- [Python](http://hindsight.vectorize.io/sdks/python)
|
||||
- [Node.js](http://hindsight.vectorize.io/sdks/nodejs)
|
||||
- [REST API](http://hindsight.vectorize.io/api-reference)
|
||||
- [REST API](https://hindsight.vectorize.io/api-reference)
|
||||
- [CLI](https://hindsight.vectorize.io/sdks/cli)
|
||||
|
||||
**Community:**
|
||||
- [Slack](https://join.slack.com/t/hindsight-space/shared_invite/zt-3klo21kua-VUCC_zHP5rIcXFB1_5yw6A)
|
||||
@@ -250,4 +235,4 @@ MIT — see [LICENSE](./LICENSE)
|
||||
|
||||
---
|
||||
|
||||
Built by [Vectorize.io](https://vectorize.io)
|
||||
Built by [Vectorize.io](https://vectorize.io)
|
||||
|
||||
@@ -43,6 +43,9 @@ RUN uv sync
|
||||
# Copy source code (alembic migrations are inside hindsight_api/)
|
||||
COPY hindsight-api/hindsight_api ./hindsight_api
|
||||
|
||||
# Install the local package (uv sync only installed dependencies, not the package itself)
|
||||
RUN uv pip install -e .
|
||||
|
||||
# =============================================================================
|
||||
# Stage: SDK Builder (needed for Control Plane)
|
||||
# =============================================================================
|
||||
@@ -144,9 +147,13 @@ RUN mkdir -p /home/hindsight/.hindsight/bin && \
|
||||
curl -fsSL -o /home/hindsight/.hindsight/bin/pg0 \
|
||||
"https://github.com/vectorize-io/pg0/releases/latest/download/$PG0_BINARY" && \
|
||||
chmod +x /home/hindsight/.hindsight/bin/pg0 && \
|
||||
ls -lh /home/hindsight/.hindsight/bin/pg0 && \
|
||||
file /home/hindsight/.hindsight/bin/pg0 && \
|
||||
ldd /home/hindsight/.hindsight/bin/pg0 2>&1 || true && \
|
||||
break || (echo "Retry $i failed, waiting..." && sleep 10); \
|
||||
done && \
|
||||
/home/hindsight/.hindsight/bin/pg0 --version
|
||||
echo "Testing pg0 binary..." && \
|
||||
/home/hindsight/.hindsight/bin/pg0 --version || (echo "pg0 --version failed with exit code $?"; ldd /home/hindsight/.hindsight/bin/pg0; exit 1)
|
||||
|
||||
# Pre-download PostgreSQL binaries
|
||||
ENV PG0_HOME=/home/hindsight/.pg0-cache
|
||||
@@ -174,6 +181,7 @@ ENV HINDSIGHT_API_PORT=8888
|
||||
ENV HINDSIGHT_API_LOG_LEVEL=info
|
||||
ENV HINDSIGHT_ENABLE_API=true
|
||||
ENV HINDSIGHT_ENABLE_CP=false
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
CMD ["/app/start-all.sh"]
|
||||
|
||||
@@ -276,9 +284,13 @@ RUN mkdir -p /home/hindsight/.hindsight/bin && \
|
||||
curl -fsSL -o /home/hindsight/.hindsight/bin/pg0 \
|
||||
"https://github.com/vectorize-io/pg0/releases/latest/download/$PG0_BINARY" && \
|
||||
chmod +x /home/hindsight/.hindsight/bin/pg0 && \
|
||||
ls -lh /home/hindsight/.hindsight/bin/pg0 && \
|
||||
file /home/hindsight/.hindsight/bin/pg0 && \
|
||||
ldd /home/hindsight/.hindsight/bin/pg0 2>&1 || true && \
|
||||
break || (echo "Retry $i failed, waiting..." && sleep 10); \
|
||||
done && \
|
||||
/home/hindsight/.hindsight/bin/pg0 --version
|
||||
echo "Testing pg0 binary..." && \
|
||||
/home/hindsight/.hindsight/bin/pg0 --version || (echo "pg0 --version failed with exit code $?"; ldd /home/hindsight/.hindsight/bin/pg0; exit 1)
|
||||
|
||||
# Pre-download PostgreSQL binaries
|
||||
ENV PG0_HOME=/home/hindsight/.pg0-cache
|
||||
@@ -308,6 +320,7 @@ ENV NODE_ENV=production
|
||||
ENV HINDSIGHT_CP_DATAPLANE_API_URL=http://localhost:8888
|
||||
ENV HINDSIGHT_ENABLE_API=true
|
||||
ENV HINDSIGHT_ENABLE_CP=true
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
CMD ["/app/start-all.sh"]
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "🚀 Starting Hindsight..."
|
||||
echo ""
|
||||
|
||||
# Service flags (default to true if not set)
|
||||
ENABLE_API="${HINDSIGHT_ENABLE_API:-true}"
|
||||
ENABLE_CP="${HINDSIGHT_ENABLE_CP:-true}"
|
||||
@@ -31,16 +28,14 @@ if [ "$ENABLE_API" = "true" ]; then
|
||||
PIDS+=($API_PID)
|
||||
|
||||
# Wait for API to be ready
|
||||
echo "⏳ Waiting for API..."
|
||||
for i in {1..60}; do
|
||||
if curl -sf http://localhost:8888/health &>/dev/null; then
|
||||
echo "✅ API is ready"
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
else
|
||||
echo "⏭️ API disabled (HINDSIGHT_ENABLE_API=false)"
|
||||
echo "API disabled (HINDSIGHT_ENABLE_API=false)"
|
||||
fi
|
||||
|
||||
# Start Control Plane if enabled
|
||||
@@ -51,7 +46,7 @@ if [ "$ENABLE_CP" = "true" ]; then
|
||||
CP_PID=$!
|
||||
PIDS+=($CP_PID)
|
||||
else
|
||||
echo "⏭️ Control Plane disabled (HINDSIGHT_ENABLE_CP=false)"
|
||||
echo "Control Plane disabled (HINDSIGHT_ENABLE_CP=false)"
|
||||
fi
|
||||
|
||||
# Print status
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
HINDSIGHT HELM CHART INSTALLATION GUIDE
|
||||
=====================================
|
||||
|
||||
PREREQUISITES
|
||||
-------------
|
||||
- Kubernetes cluster (1.19+)
|
||||
- kubectl configured
|
||||
- Helm 3.x installed
|
||||
- PostgreSQL database with pgvector extension (if not using bundled PostgreSQL)
|
||||
|
||||
BASIC INSTALLATION
|
||||
------------------
|
||||
|
||||
1. Install with default values (requires external PostgreSQL):
|
||||
|
||||
helm install hindsight ./hindsight \
|
||||
--set postgresql.external.host=your-postgres-host \
|
||||
--set postgresql.external.password=your-password \
|
||||
--set api.secrets.MEMORY_LLM_API_KEY=your-api-key
|
||||
|
||||
2. Install with custom values file:
|
||||
|
||||
helm install hindsight ./hindsight -f hindsight/values-production.yaml
|
||||
|
||||
3. Install in a specific namespace:
|
||||
|
||||
kubectl create namespace hindsight
|
||||
helm install hindsight ./hindsight -n hindsight
|
||||
|
||||
CONFIGURATION OPTIONS
|
||||
---------------------
|
||||
|
||||
Development setup (using values-development.yaml):
|
||||
helm install hindsight ./hindsight -f hindsight/values-development.yaml
|
||||
|
||||
Production setup (using values-production.yaml):
|
||||
helm install hindsight ./hindsight -f hindsight/values-production.yaml
|
||||
|
||||
Custom LLM provider:
|
||||
helm install hindsight ./hindsight \
|
||||
--set api.env.MEMORY_LLM_PROVIDER=openai \
|
||||
--set api.env.MEMORY_LLM_MODEL=gpt-4 \
|
||||
--set api.secrets.MEMORY_LLM_API_KEY=sk-your-key
|
||||
|
||||
Enable ingress:
|
||||
helm install hindsight ./hindsight \
|
||||
--set ingress.enabled=true \
|
||||
--set ingress.hosts[0].host=hindsight.example.com
|
||||
|
||||
Enable autoscaling:
|
||||
helm install hindsight ./hindsight \
|
||||
--set autoscaling.enabled=true \
|
||||
--set autoscaling.minReplicas=2 \
|
||||
--set autoscaling.maxReplicas=10
|
||||
|
||||
UPGRADE
|
||||
-------
|
||||
|
||||
Upgrade existing installation:
|
||||
helm upgrade hindsight ./hindsight
|
||||
|
||||
Upgrade with new values:
|
||||
helm upgrade hindsight ./hindsight -f hindsight/values-production.yaml
|
||||
|
||||
UNINSTALL
|
||||
---------
|
||||
|
||||
Remove the Helm release:
|
||||
helm uninstall hindsight
|
||||
|
||||
Remove with namespace:
|
||||
helm uninstall hindsight -n hindsight
|
||||
|
||||
TESTING
|
||||
-------
|
||||
|
||||
Test the installation with dry-run:
|
||||
helm install hindsight ./hindsight --dry-run --debug
|
||||
|
||||
Validate templates:
|
||||
helm template hindsight ./hindsight
|
||||
|
||||
Lint the chart:
|
||||
helm lint ./hindsight
|
||||
|
||||
ACCESSING THE SERVICES
|
||||
----------------------
|
||||
|
||||
Port-forward control plane:
|
||||
kubectl port-forward svc/hindsight-control-plane 3000:3000
|
||||
|
||||
Port-forward API:
|
||||
kubectl port-forward svc/hindsight-api 8888:8888
|
||||
|
||||
Get service URLs:
|
||||
helm status hindsight
|
||||
|
||||
DATABASE INITIALIZATION
|
||||
-----------------------
|
||||
|
||||
NOTE: Database migrations now run automatically when the API service starts.
|
||||
You typically don't need to run migrations manually.
|
||||
|
||||
If you want to pre-initialize the database before deploying (optional):
|
||||
kubectl run hindsight-init --rm -it --restart=Never \
|
||||
--image=hindsight/api:latest \
|
||||
--env="DATABASE_URL=postgresql://user:pass@host:5432/hindsight" \
|
||||
-- python -c "from hindsight.migrations import run_migrations; run_migrations()"
|
||||
|
||||
TROUBLESHOOTING
|
||||
---------------
|
||||
|
||||
Check pod status:
|
||||
kubectl get pods -l app.kubernetes.io/name=hindsight
|
||||
|
||||
View logs for API:
|
||||
kubectl logs -l app.kubernetes.io/component=api
|
||||
|
||||
View logs for control plane:
|
||||
kubectl logs -l app.kubernetes.io/component=control-plane
|
||||
|
||||
Describe a pod:
|
||||
kubectl describe pod <pod-name>
|
||||
|
||||
Check configuration:
|
||||
kubectl get configmap hindsight-config -o yaml
|
||||
kubectl get secret hindsight-secret -o yaml
|
||||
|
||||
NOTES
|
||||
-----
|
||||
- Make sure PostgreSQL has pgvector extension enabled
|
||||
- Run database migrations before first use
|
||||
- Configure proper resource limits for production
|
||||
- Use external secrets management for production
|
||||
- Enable TLS/SSL for production deployments
|
||||
@@ -0,0 +1,6 @@
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
repository: https://charts.bitnami.com/bitnami
|
||||
version: 15.5.38
|
||||
digest: sha256:f67c7612736803ece8a669f8ca6b0555f3b78557bc0ecb732aa2e43f0df7750d
|
||||
generated: "2025-12-10T17:20:57.058794+01:00"
|
||||
@@ -1,9 +1,9 @@
|
||||
apiVersion: v2
|
||||
name: hindsight
|
||||
description: A Helm chart for Hindsight - temporal-semantic-entity memory system for AI agents
|
||||
description: Hindsight helm chart
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "0.1.0"
|
||||
version: 0.1.4
|
||||
appVersion: "0.1.4"
|
||||
keywords:
|
||||
- ai
|
||||
- memory
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
# Hindsight Helm Chart
|
||||
|
||||
Helm chart for deploying Hindsight - a temporal-semantic-entity memory system for AI agents.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Kubernetes 1.19+
|
||||
- Helm 3.0+
|
||||
- PostgreSQL database (external or bundled)
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Update dependencies first
|
||||
helm dependency update ./helm/hindsight
|
||||
|
||||
# Install (PostgreSQL included by default)
|
||||
export OPENAI_API_KEY="sk-your-openai-key"
|
||||
helm upgrade hindsight --install ./helm/hindsight -n hindsight --create-namespace \
|
||||
--set api.secrets.HINDSIGHT_API_LLM_API_KEY="$OPENAI_API_KEY"
|
||||
```
|
||||
|
||||
To use an external database instead:
|
||||
|
||||
```bash
|
||||
helm install hindsight ./helm/hindsight -n hindsight --create-namespace \
|
||||
--set api.secrets.HINDSIGHT_API_LLM_API_KEY="sk-your-openai-key" \
|
||||
--set postgresql.enabled=false \
|
||||
--set postgresql.external.host=my-postgres.example.com \
|
||||
--set postgresql.external.password=mypassword
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
### Add the repository (if published)
|
||||
|
||||
```bash
|
||||
helm repo add hindsight https://your-helm-repo.com
|
||||
helm repo update
|
||||
```
|
||||
|
||||
### Install with custom values file
|
||||
|
||||
Create a `values-override.yaml`:
|
||||
|
||||
```yaml
|
||||
api:
|
||||
secrets:
|
||||
HINDSIGHT_API_LLM_API_KEY: "sk-your-openai-key"
|
||||
|
||||
postgresql:
|
||||
external:
|
||||
host: "my-postgres.example.com"
|
||||
password: "mypassword"
|
||||
```
|
||||
|
||||
Then install:
|
||||
|
||||
```bash
|
||||
helm install hindsight ./helm/hindsight -n hindsight --create-namespace -f values-override.yaml
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Key Values
|
||||
|
||||
| Parameter | Description | Default |
|
||||
|-----------|-------------|---------|
|
||||
| `version` | Default image tag for all components | `0.1.0` |
|
||||
| `api.enabled` | Enable the API component | `true` |
|
||||
| `api.image.repository` | API image repository | `hindsight/api` |
|
||||
| `api.image.tag` | API image tag (defaults to `version`) | - |
|
||||
| `api.service.port` | API service port | `8888` |
|
||||
| `controlPlane.enabled` | Enable the control plane | `true` |
|
||||
| `controlPlane.image.repository` | Control plane image repository | `hindsight/control-plane` |
|
||||
| `controlPlane.image.tag` | Control plane image tag (defaults to `version`) | - |
|
||||
| `controlPlane.service.port` | Control plane service port | `3000` |
|
||||
| `postgresql.enabled` | Deploy PostgreSQL as subchart | `true` |
|
||||
| `postgresql.external.host` | External PostgreSQL host | `postgresql` |
|
||||
| `postgresql.external.port` | External PostgreSQL port | `5432` |
|
||||
| `postgresql.external.database` | Database name | `hindsight` |
|
||||
| `postgresql.external.username` | Database username | `hindsight` |
|
||||
| `ingress.enabled` | Enable ingress | `false` |
|
||||
| `autoscaling.enabled` | Enable HPA | `false` |
|
||||
|
||||
### Environment Variables
|
||||
|
||||
All environment variables in `api.env` and `controlPlane.env` are automatically added to the respective pods. Sensitive values should go in `api.secrets` or `controlPlane.secrets`.
|
||||
|
||||
```yaml
|
||||
api:
|
||||
env:
|
||||
HINDSIGHT_API_LLM_PROVIDER: "openai"
|
||||
HINDSIGHT_API_LLM_MODEL: "gpt-4"
|
||||
secrets:
|
||||
HINDSIGHT_API_LLM_API_KEY: "your-api-key"
|
||||
HINDSIGHT_API_LLM_BASE_URL: "https://api.openai.com/v1"
|
||||
|
||||
controlPlane:
|
||||
env:
|
||||
NODE_ENV: "production"
|
||||
secrets: {}
|
||||
```
|
||||
|
||||
### External Database
|
||||
|
||||
To connect to an external PostgreSQL database:
|
||||
|
||||
```yaml
|
||||
postgresql:
|
||||
enabled: false
|
||||
external:
|
||||
host: "my-postgres.example.com"
|
||||
port: 5432
|
||||
database: "hindsight"
|
||||
username: "hindsight"
|
||||
password: "your-password"
|
||||
```
|
||||
|
||||
### Ingress
|
||||
|
||||
To expose the services via ingress:
|
||||
|
||||
```yaml
|
||||
ingress:
|
||||
enabled: true
|
||||
className: "nginx"
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
hosts:
|
||||
- host: hindsight.example.com
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
service: controlPlane
|
||||
- path: /api
|
||||
pathType: Prefix
|
||||
service: api
|
||||
tls:
|
||||
- secretName: hindsight-tls
|
||||
hosts:
|
||||
- hindsight.example.com
|
||||
```
|
||||
|
||||
## Upgrading
|
||||
|
||||
```bash
|
||||
helm upgrade hindsight ./helm/hindsight -n hindsight
|
||||
```
|
||||
|
||||
## Uninstalling
|
||||
|
||||
```bash
|
||||
helm uninstall hindsight -n hindsight
|
||||
```
|
||||
|
||||
## Components
|
||||
|
||||
The chart deploys:
|
||||
|
||||
- **API**: The main Hindsight API server for memory operations
|
||||
- **Control Plane**: Web UI for managing agents and viewing memories
|
||||
|
||||
## Development
|
||||
|
||||
### Lint the chart
|
||||
|
||||
```bash
|
||||
helm lint ./helm/hindsight
|
||||
```
|
||||
|
||||
### Template locally
|
||||
|
||||
```bash
|
||||
helm template hindsight ./helm/hindsight --debug
|
||||
```
|
||||
|
||||
### Dry run installation
|
||||
|
||||
```bash
|
||||
helm install hindsight ./helm/hindsight --dry-run --debug
|
||||
```
|
||||
@@ -1,71 +1,2 @@
|
||||
Thank you for installing {{ .Chart.Name }}!
|
||||
|
||||
Your release is named {{ .Release.Name }}.
|
||||
|
||||
To learn more about the release, try:
|
||||
|
||||
$ helm status {{ .Release.Name }}
|
||||
$ helm get all {{ .Release.Name }}
|
||||
|
||||
{{- if .Values.ingress.enabled }}
|
||||
|
||||
The application is accessible via the following URL(s):
|
||||
{{- range .Values.ingress.hosts }}
|
||||
- http{{ if $.Values.ingress.tls }}s{{ end }}://{{ .host }}
|
||||
{{- end }}
|
||||
|
||||
{{- else }}
|
||||
|
||||
1. Get the Control Plane URL by running these commands:
|
||||
{{- if contains "NodePort" .Values.controlPlane.service.type }}
|
||||
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "hindsight.fullname" . }}-control-plane)
|
||||
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
|
||||
echo "Control Plane URL: http://$NODE_IP:$NODE_PORT"
|
||||
{{- else if contains "LoadBalancer" .Values.controlPlane.service.type }}
|
||||
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
|
||||
You can watch the status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "hindsight.fullname" . }}-control-plane'
|
||||
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "hindsight.fullname" . }}-control-plane --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
|
||||
echo "Control Plane URL: http://$SERVICE_IP:{{ .Values.controlPlane.service.port }}"
|
||||
{{- else if contains "ClusterIP" .Values.controlPlane.service.type }}
|
||||
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/component=control-plane,app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
||||
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
|
||||
echo "Control Plane URL: http://127.0.0.1:3000"
|
||||
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 3000:$CONTAINER_PORT
|
||||
{{- end }}
|
||||
|
||||
2. Get the API URL by running these commands:
|
||||
{{- if contains "NodePort" .Values.api.service.type }}
|
||||
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "hindsight.fullname" . }}-api)
|
||||
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
|
||||
echo "API URL: http://$NODE_IP:$NODE_PORT"
|
||||
{{- else if contains "LoadBalancer" .Values.api.service.type }}
|
||||
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
|
||||
You can watch the status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "hindsight.fullname" . }}-api'
|
||||
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "hindsight.fullname" . }}-api --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
|
||||
echo "API URL: http://$SERVICE_IP:{{ .Values.api.service.port }}"
|
||||
{{- else if contains "ClusterIP" .Values.api.service.type }}
|
||||
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/component=api,app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
||||
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
|
||||
echo "API URL: http://127.0.0.1:8888"
|
||||
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8888:$CONTAINER_PORT
|
||||
{{- end }}
|
||||
|
||||
{{- end }}
|
||||
|
||||
{{- if not .Values.postgresql.enabled }}
|
||||
|
||||
NOTE: You are using an external PostgreSQL database.
|
||||
Please ensure that:
|
||||
1. The database is accessible from the cluster
|
||||
2. The pgvector extension is enabled
|
||||
|
||||
Database migrations run automatically when the API service starts.
|
||||
|
||||
If you want to pre-initialize the database before deploying (optional):
|
||||
kubectl run --namespace {{ .Release.Namespace }} hindsight-init --rm -it --restart=Never \
|
||||
--image={{ .Values.api.image.repository }}:{{ .Values.api.image.tag }} \
|
||||
--env="DATABASE_URL={{ include "hindsight.databaseUrl" . }}" \
|
||||
-- python -c "from hindsight.migrations import run_migrations; run_migrations()"
|
||||
{{- end }}
|
||||
|
||||
For more information, visit: https://github.com/yourusername/hindsight
|
||||
Hindsight installed. Access the control plane:
|
||||
kubectl port-forward -n {{ .Release.Namespace }} svc/{{ include "hindsight.fullname" . }}-control-plane 3000:3000
|
||||
|
||||
@@ -98,7 +98,7 @@ Generate database URL
|
||||
{{- if .Values.databaseUrl }}
|
||||
{{- .Values.databaseUrl }}
|
||||
{{- else if .Values.postgresql.enabled }}
|
||||
{{- printf "postgresql://%s:%s@%s-postgresql:%d/%s" .Values.postgresql.auth.username .Values.postgresql.auth.password (include "hindsight.fullname" .) (.Values.postgresql.primary.service.port | int) .Values.postgresql.auth.database }}
|
||||
{{- printf "postgresql://%s:%s@%s-postgresql:%d/%s" .Values.postgresql.auth.username .Values.postgresql.auth.password (include "hindsight.fullname" .) (.Values.postgresql.service.port | int) .Values.postgresql.auth.database }}
|
||||
{{- else }}
|
||||
{{- printf "postgresql://%s:$(POSTGRES_PASSWORD)@%s:%d/%s" .Values.postgresql.external.username .Values.postgresql.external.host (.Values.postgresql.external.port | int) .Values.postgresql.external.database }}
|
||||
{{- end }}
|
||||
|
||||
@@ -15,7 +15,6 @@ spec:
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
|
||||
checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
|
||||
{{- with .Values.podAnnotations }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
@@ -32,7 +31,7 @@ spec:
|
||||
- name: api
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 10 }}
|
||||
image: "{{ .Values.api.image.repository }}:{{ .Values.api.image.tag }}"
|
||||
image: "{{ .Values.api.image.repository }}:{{ .Values.api.image.tag | default .Values.version }}"
|
||||
imagePullPolicy: {{ .Values.api.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
@@ -48,29 +47,16 @@ spec:
|
||||
name: {{ include "hindsight.fullname" . }}-secret
|
||||
key: postgres-password
|
||||
{{- end }}
|
||||
- name: HINDSIGHT_API_LLM_PROVIDER
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ include "hindsight.fullname" . }}-config
|
||||
key: llm-provider
|
||||
- name: HINDSIGHT_API_LLM_MODEL
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ include "hindsight.fullname" . }}-config
|
||||
key: llm-model
|
||||
{{- if and .Values.api.secrets (hasKey .Values.api.secrets "HINDSIGHT_API_LLM_API_KEY") }}
|
||||
- name: HINDSIGHT_API_LLM_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "hindsight.fullname" . }}-secret
|
||||
key: llm-api-key
|
||||
{{- range $key, $value := .Values.api.env }}
|
||||
- name: {{ $key }}
|
||||
value: {{ $value | quote }}
|
||||
{{- end }}
|
||||
{{- if and .Values.api.secrets (hasKey .Values.api.secrets "HINDSIGHT_API_LLM_BASE_URL") }}
|
||||
- name: HINDSIGHT_API_LLM_BASE_URL
|
||||
{{- range $key, $value := .Values.api.secrets }}
|
||||
- name: {{ $key }}
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "hindsight.fullname" . }}-secret
|
||||
key: llm-base-url
|
||||
name: {{ include "hindsight.fullname" $ }}-secret
|
||||
key: {{ $key }}
|
||||
{{- end }}
|
||||
livenessProbe:
|
||||
{{- toYaml .Values.api.livenessProbe | nindent 10 }}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "hindsight.fullname" . }}-config
|
||||
labels:
|
||||
{{- include "hindsight.labels" . | nindent 4 }}
|
||||
data:
|
||||
# API configuration
|
||||
llm-provider: {{ .Values.api.env.HINDSIGHT_API_LLM_PROVIDER | quote }}
|
||||
llm-model: {{ .Values.api.env.HINDSIGHT_API_LLM_MODEL | quote }}
|
||||
|
||||
# Control plane configuration
|
||||
node-env: {{ .Values.controlPlane.env.NODE_ENV | quote }}
|
||||
hostname: {{ .Values.controlPlane.env.HINDSIGHT_CP_HOSTNAME | quote }}
|
||||
control-plane-port: {{ .Values.controlPlane.env.HINDSIGHT_CP_PORT | quote }}
|
||||
@@ -15,7 +15,7 @@ spec:
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
|
||||
checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
|
||||
{{- with .Values.podAnnotations }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
@@ -31,30 +31,26 @@ spec:
|
||||
- name: control-plane
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 10 }}
|
||||
image: "{{ .Values.controlPlane.image.repository }}:{{ .Values.controlPlane.image.tag }}"
|
||||
image: "{{ .Values.controlPlane.image.repository }}:{{ .Values.controlPlane.image.tag | default .Values.version }}"
|
||||
imagePullPolicy: {{ .Values.controlPlane.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.controlPlane.service.targetPort }}
|
||||
protocol: TCP
|
||||
env:
|
||||
- name: NODE_ENV
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ include "hindsight.fullname" . }}-config
|
||||
key: node-env
|
||||
- name: HINDSIGHT_CP_HOSTNAME
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ include "hindsight.fullname" . }}-config
|
||||
key: hostname
|
||||
- name: HINDSIGHT_CP_PORT
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: {{ include "hindsight.fullname" . }}-config
|
||||
key: control-plane-port
|
||||
- name: HINDSIGHT_CP_DATAPLANE_API_URL
|
||||
value: {{ include "hindsight.apiUrl" . | quote }}
|
||||
{{- range $key, $value := .Values.controlPlane.env }}
|
||||
- name: {{ $key }}
|
||||
value: {{ $value | quote }}
|
||||
{{- end }}
|
||||
{{- range $key, $value := .Values.controlPlane.secrets }}
|
||||
- name: {{ $key }}
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "hindsight.fullname" $ }}-secret
|
||||
key: {{ $key }}
|
||||
{{- end }}
|
||||
livenessProbe:
|
||||
{{- toYaml .Values.controlPlane.livenessProbe | nindent 10 }}
|
||||
readinessProbe:
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{{- if .Values.postgresql.enabled }}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "hindsight.fullname" . }}-postgresql
|
||||
labels:
|
||||
{{- include "hindsight.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: postgresql
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- port: {{ .Values.postgresql.service.port }}
|
||||
targetPort: postgresql
|
||||
protocol: TCP
|
||||
name: postgresql
|
||||
selector:
|
||||
{{- include "hindsight.selectorLabels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: postgresql
|
||||
{{- end }}
|
||||
@@ -0,0 +1,85 @@
|
||||
{{- if .Values.postgresql.enabled }}
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: {{ include "hindsight.fullname" . }}-postgresql
|
||||
labels:
|
||||
{{- include "hindsight.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: postgresql
|
||||
spec:
|
||||
serviceName: {{ include "hindsight.fullname" . }}-postgresql
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "hindsight.selectorLabels" . | nindent 6 }}
|
||||
app.kubernetes.io/component: postgresql
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "hindsight.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/component: postgresql
|
||||
spec:
|
||||
containers:
|
||||
- name: postgresql
|
||||
image: "{{ .Values.postgresql.image.repository }}:{{ .Values.postgresql.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.postgresql.image.pullPolicy }}
|
||||
ports:
|
||||
- name: postgresql
|
||||
containerPort: 5432
|
||||
protocol: TCP
|
||||
env:
|
||||
- name: POSTGRES_USER
|
||||
value: {{ .Values.postgresql.auth.username | quote }}
|
||||
- name: POSTGRES_PASSWORD
|
||||
value: {{ .Values.postgresql.auth.password | quote }}
|
||||
- name: POSTGRES_DB
|
||||
value: {{ .Values.postgresql.auth.database | quote }}
|
||||
- name: PGDATA
|
||||
value: /var/lib/postgresql/data/pgdata
|
||||
livenessProbe:
|
||||
exec:
|
||||
command:
|
||||
- pg_isready
|
||||
- -U
|
||||
- {{ .Values.postgresql.auth.username }}
|
||||
- -d
|
||||
- {{ .Values.postgresql.auth.database }}
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
exec:
|
||||
command:
|
||||
- pg_isready
|
||||
- -U
|
||||
- {{ .Values.postgresql.auth.username }}
|
||||
- -d
|
||||
- {{ .Values.postgresql.auth.database }}
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
resources:
|
||||
{{- toYaml .Values.postgresql.resources | nindent 10 }}
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /var/lib/postgresql/data
|
||||
{{- if .Values.postgresql.persistence.enabled }}
|
||||
volumeClaimTemplates:
|
||||
- metadata:
|
||||
name: data
|
||||
spec:
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
{{- if .Values.postgresql.persistence.storageClass }}
|
||||
storageClassName: {{ .Values.postgresql.persistence.storageClass | quote }}
|
||||
{{- end }}
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.postgresql.persistence.size }}
|
||||
{{- else }}
|
||||
volumes:
|
||||
- name: data
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -6,14 +6,12 @@ metadata:
|
||||
{{- include "hindsight.labels" . | nindent 4 }}
|
||||
type: Opaque
|
||||
data:
|
||||
{{- if and .Values.api.secrets (hasKey .Values.api.secrets "MEMORY_LLM_API_KEY") }}
|
||||
llm-api-key: {{ .Values.api.secrets.MEMORY_LLM_API_KEY | b64enc | quote }}
|
||||
{{- range $key, $value := .Values.api.secrets }}
|
||||
{{ $key }}: {{ $value | b64enc | quote }}
|
||||
{{- end }}
|
||||
{{- if and .Values.api.secrets (hasKey .Values.api.secrets "MEMORY_LLM_BASE_URL") }}
|
||||
llm-base-url: {{ .Values.api.secrets.MEMORY_LLM_BASE_URL | b64enc | quote }}
|
||||
{{- range $key, $value := .Values.controlPlane.secrets }}
|
||||
{{ $key }}: {{ $value | b64enc | quote }}
|
||||
{{- end }}
|
||||
{{- if not .Values.postgresql.enabled }}
|
||||
{{- if .Values.postgresql.external.password }}
|
||||
{{- if and (not .Values.postgresql.enabled) .Values.postgresql.external.password }}
|
||||
postgres-password: {{ .Values.postgresql.external.password | b64enc | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
+41
-18
@@ -1,5 +1,8 @@
|
||||
# Default values for hindsight
|
||||
|
||||
# Chart version - use this to set a consistent image tag across all components
|
||||
version: "0.1.1"
|
||||
|
||||
# Global settings
|
||||
replicaCount: 1
|
||||
|
||||
@@ -8,9 +11,9 @@ api:
|
||||
enabled: true
|
||||
replicaCount: 1
|
||||
image:
|
||||
repository: hindsight/api
|
||||
repository: ghcr.io/vectorize-io/hindsight-api
|
||||
pullPolicy: IfNotPresent
|
||||
tag: "latest"
|
||||
# tag defaults to .Values.version if not specified
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
@@ -29,7 +32,7 @@ api:
|
||||
# Liveness and readiness probes
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
path: /health
|
||||
port: 8888
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
@@ -38,7 +41,7 @@ api:
|
||||
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
path: /health
|
||||
port: 8888
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 5
|
||||
@@ -47,7 +50,7 @@ api:
|
||||
|
||||
# Environment variables
|
||||
env:
|
||||
HINDSIGHT_API_LLM_PROVIDER: "groq"
|
||||
#HINDSIGHT_API_LLM_PROVIDER: "groq"
|
||||
HINDSIGHT_API_LLM_MODEL: "openai/gpt-oss-120b"
|
||||
|
||||
# Secret environment variables
|
||||
@@ -60,9 +63,9 @@ controlPlane:
|
||||
enabled: true
|
||||
replicaCount: 1
|
||||
image:
|
||||
repository: hindsight/hindsight-control-plane
|
||||
repository: ghcr.io/vectorize-io/hindsight-control-plane
|
||||
pullPolicy: IfNotPresent
|
||||
tag: "latest"
|
||||
# tag defaults to .Values.version if not specified
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
@@ -78,10 +81,9 @@ controlPlane:
|
||||
cpu: 250m
|
||||
memory: 512Mi
|
||||
|
||||
# Liveness and readiness probes
|
||||
# Liveness and readiness probes (TCP check)
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
tcpSocket:
|
||||
port: 3000
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
@@ -89,8 +91,7 @@ controlPlane:
|
||||
failureThreshold: 3
|
||||
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
tcpSocket:
|
||||
port: 3000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 5
|
||||
@@ -106,21 +107,43 @@ controlPlane:
|
||||
# PostgreSQL configuration
|
||||
postgresql:
|
||||
# Set to true to deploy PostgreSQL as part of this chart
|
||||
enabled: false
|
||||
enabled: true
|
||||
|
||||
image:
|
||||
repository: ankane/pgvector
|
||||
tag: latest
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
auth:
|
||||
username: "hindsight"
|
||||
password: "hindsight"
|
||||
database: "hindsight"
|
||||
|
||||
service:
|
||||
port: 5432
|
||||
|
||||
persistence:
|
||||
enabled: true
|
||||
size: 8Gi
|
||||
# storageClass: ""
|
||||
|
||||
resources:
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 1Gi
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 256Mi
|
||||
|
||||
# External PostgreSQL connection details
|
||||
# If postgresql.enabled is false, provide external database details
|
||||
# Only used if postgresql.enabled is false
|
||||
external:
|
||||
host: "postgresql"
|
||||
port: 5432
|
||||
database: "hindsight"
|
||||
username: "hindsight"
|
||||
# Password should be provided via secret
|
||||
# password: ""
|
||||
|
||||
# Database URL (auto-generated from postgresql config if not provided)
|
||||
# databaseUrl: "postgresql://user:pass@host:5432/database"
|
||||
|
||||
# Ingress configuration
|
||||
ingress:
|
||||
enabled: false
|
||||
|
||||
@@ -672,11 +672,15 @@ class DeleteResponse(BaseModel):
|
||||
"""Response model for delete operations."""
|
||||
model_config = ConfigDict(json_schema_extra={
|
||||
"example": {
|
||||
"success": True
|
||||
"success": True,
|
||||
"message": "Deleted successfully",
|
||||
"deleted_count": 10
|
||||
}
|
||||
})
|
||||
|
||||
success: bool
|
||||
message: Optional[str] = None
|
||||
deleted_count: Optional[int] = None
|
||||
|
||||
|
||||
def create_app(memory: MemoryEngine, initialize_memory: bool = True) -> FastAPI:
|
||||
@@ -1696,6 +1700,31 @@ def _register_routes(app: FastAPI):
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.delete(
|
||||
"/v1/default/banks/{bank_id}",
|
||||
response_model=DeleteResponse,
|
||||
summary="Delete memory bank",
|
||||
description="Delete an entire memory bank including all memories, entities, documents, and the bank profile itself. "
|
||||
"This is a destructive operation that cannot be undone.",
|
||||
operation_id="delete_bank",
|
||||
tags=["Banks"]
|
||||
)
|
||||
async def api_delete_bank(bank_id: str):
|
||||
"""Delete an entire memory bank and all its data."""
|
||||
try:
|
||||
result = await app.state.memory.delete_bank(bank_id)
|
||||
return DeleteResponse(
|
||||
success=True,
|
||||
message=f"Bank '{bank_id}' and all associated data deleted successfully",
|
||||
deleted_count=result.get("memory_units_deleted", 0) + result.get("entities_deleted", 0) + result.get("documents_deleted", 0)
|
||||
)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
||||
logger.error(f"Error in DELETE /v1/default/banks/{bank_id}: {error_detail}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.post(
|
||||
"/v1/default/banks/{bank_id}/memories",
|
||||
response_model=RetainResponse,
|
||||
@@ -1760,7 +1789,7 @@ def _register_routes(app: FastAPI):
|
||||
|
||||
# Submit task to background queue
|
||||
await app.state.memory._task_backend.submit_task({
|
||||
'type': 'batch_put',
|
||||
'type': 'batch_retain',
|
||||
'operation_id': str(operation_id),
|
||||
'bank_id': bank_id,
|
||||
'contents': contents
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
"""
|
||||
Banner display for Hindsight API startup.
|
||||
|
||||
Shows the logo and tagline with gradient colors.
|
||||
"""
|
||||
|
||||
# Gradient colors: #0074d9 -> #009296
|
||||
GRADIENT_START = (0, 116, 217) # #0074d9
|
||||
GRADIENT_END = (0, 146, 150) # #009296
|
||||
|
||||
# Pre-generated logo (generated by test-logo.py)
|
||||
LOGO = """\
|
||||
\033[38;2;9;127;184m\u2584\033[0m\033[48;2;8;130;178m\033[38;2;5;133;186m\u2584\033[0m \033[48;2;10;143;160m\033[38;2;10;143;165m\u2584\033[0m\033[38;2;7;140;156m\u2584\033[0m
|
||||
\033[38;2;8;125;192m\u2584\033[0m \033[38;2;3;132;191m\u2580\033[0m\033[38;2;2;133;192m\u2584\033[0m \033[38;2;3;132;180m\u2584\033[0m\033[38;2;1;137;184m\u2584\033[0m\033[38;2;3;133;174m\u2584\033[0m \033[38;2;3;142;176m\u2584\033[0m\033[38;2;4;142;169m\u2580\033[0m \033[38;2;10;144;164m\u2584\033[0m
|
||||
\033[38;2;6;121;195m\u2580\033[0m\033[38;2;5;128;203m\u2580\033[0m\033[48;2;5;124;195m\033[38;2;3;125;200m\u2584\033[0m\033[38;2;2;126;196m\u2584\033[0m\033[48;2;3;128;188m\033[38;2;1;131;196m\u2584\033[0m\033[48;2;0;152;219m\033[38;2;2;131;191m\u2584\033[0m\033[38;2;1;141;196m\u2580\033[0m\033[38;2;1;135;183m\u2580\033[0m\033[38;2;1;148;198m\u2580\033[0m\033[48;2;1;156;202m\033[38;2;2;135;180m\u2584\033[0m\033[48;2;4;134;169m\033[38;2;1;137;177m\u2584\033[0m\033[38;2;3;138;173m\u2584\033[0m\033[48;2;6;137;165m\033[38;2;2;140;170m\u2584\033[0m\033[38;2;7;144;169m\u2580\033[0m\033[38;2;7;139;158m\u2580\033[0m
|
||||
\033[48;2;2;128;202m\033[38;2;2;124;201m\u2584\033[0m\033[48;2;1;130;201m\033[38;2;0;135;212m\u2584\033[0m\033[38;2;2;128;196m\u2584\033[0m \033[48;2;2;142;204m\033[38;2;7;138;199m\u2584\033[0m \033[38;2;1;135;186m\u2584\033[0m\033[48;2;1;142;186m\033[38;2;2;144;194m\u2584\033[0m\033[48;2;3;138;176m\033[38;2;2;134;176m\u2584\033[0m
|
||||
\033[48;2;8;118;200m\033[38;2;8;121;209m\u2584\033[0m\033[38;2;3;121;203m\u2580\033[0m \033[38;2;3;122;192m\u2580\033[0m\033[38;2;1;138;216m\u2580\033[0m\033[48;2;0;138;210m\033[38;2;3;128;198m\u2584\033[0m\033[48;2;0;126;188m\033[38;2;2;131;198m\u2584\033[0m\033[48;2;0;142;205m\033[38;2;3;132;193m\u2584\033[0m\033[38;2;1;140;196m\u2580\033[0m \033[38;2;4;134;175m\u2580\033[0m\033[48;2;13;135;167m\033[38;2;8;136;174m\u2584\033[0m """
|
||||
|
||||
|
||||
def _interpolate_color(start: tuple, end: tuple, t: float) -> tuple:
|
||||
"""Interpolate between two RGB colors."""
|
||||
return (
|
||||
int(start[0] + (end[0] - start[0]) * t),
|
||||
int(start[1] + (end[1] - start[1]) * t),
|
||||
int(start[2] + (end[2] - start[2]) * t),
|
||||
)
|
||||
|
||||
|
||||
def gradient_text(text: str, start: tuple = GRADIENT_START, end: tuple = GRADIENT_END) -> str:
|
||||
"""Render text with a gradient color effect."""
|
||||
result = []
|
||||
length = len(text)
|
||||
for i, char in enumerate(text):
|
||||
if char == ' ':
|
||||
result.append(' ')
|
||||
else:
|
||||
t = i / max(length - 1, 1)
|
||||
r, g, b = _interpolate_color(start, end, t)
|
||||
result.append(f"\033[38;2;{r};{g};{b}m{char}")
|
||||
result.append("\033[0m")
|
||||
return "".join(result)
|
||||
|
||||
|
||||
def print_banner():
|
||||
"""Print the Hindsight startup banner."""
|
||||
print(LOGO)
|
||||
tagline = gradient_text("Hindsight: Agent Memory That Works Like Human Memory")
|
||||
print(f"\n {tagline}\n")
|
||||
|
||||
|
||||
def color(text: str, t: float = 0.0) -> str:
|
||||
"""Color text using gradient position (0.0 = start, 1.0 = end)."""
|
||||
r, g, b = _interpolate_color(GRADIENT_START, GRADIENT_END, t)
|
||||
return f"\033[38;2;{r};{g};{b}m{text}\033[0m"
|
||||
|
||||
|
||||
def color_start(text: str) -> str:
|
||||
"""Color text with gradient start color (#0074d9)."""
|
||||
return color(text, 0.0)
|
||||
|
||||
|
||||
def color_end(text: str) -> str:
|
||||
"""Color text with gradient end color (#009296)."""
|
||||
return color(text, 1.0)
|
||||
|
||||
|
||||
def color_mid(text: str) -> str:
|
||||
"""Color text with gradient middle color."""
|
||||
return color(text, 0.5)
|
||||
|
||||
|
||||
def dim(text: str) -> str:
|
||||
"""Dim/gray text."""
|
||||
return f"\033[38;2;128;128;128m{text}\033[0m"
|
||||
|
||||
|
||||
def print_startup_info(host: str, port: int, database_url: str, llm_provider: str,
|
||||
llm_model: str, embeddings_provider: str, reranker_provider: str,
|
||||
mcp_enabled: bool = False):
|
||||
"""Print styled startup information."""
|
||||
print(color_start("Starting Hindsight API..."))
|
||||
print(f" {dim('URL:')} {color(f'http://{host}:{port}', 0.2)}")
|
||||
print(f" {dim('Database:')} {color(database_url, 0.4)}")
|
||||
print(f" {dim('LLM:')} {color(f'{llm_provider} / {llm_model}', 0.6)}")
|
||||
print(f" {dim('Embeddings:')} {color(embeddings_provider, 0.8)}")
|
||||
print(f" {dim('Reranker:')} {color(reranker_provider, 1.0)}")
|
||||
if mcp_enabled:
|
||||
print(f" {dim('MCP:')} {color_end('enabled at /mcp')}")
|
||||
print()
|
||||
@@ -32,8 +32,8 @@ ENV_MCP_ENABLED = "HINDSIGHT_API_MCP_ENABLED"
|
||||
|
||||
# Default values
|
||||
DEFAULT_DATABASE_URL = "pg0"
|
||||
DEFAULT_LLM_PROVIDER = "groq"
|
||||
DEFAULT_LLM_MODEL = "openai/gpt-oss-20b"
|
||||
DEFAULT_LLM_PROVIDER = "openai"
|
||||
DEFAULT_LLM_MODEL = "gpt-5-mini"
|
||||
|
||||
DEFAULT_EMBEDDINGS_PROVIDER = "local"
|
||||
DEFAULT_EMBEDDINGS_LOCAL_MODEL = "BAAI/bge-small-en-v1.5"
|
||||
|
||||
@@ -101,12 +101,7 @@ class LocalSTCrossEncoder(CrossEncoderModel):
|
||||
)
|
||||
|
||||
logger.info(f"Reranker: initializing local provider with model {self.model_name}")
|
||||
# Disable lazy loading (meta tensors) which causes issues with newer transformers/accelerate
|
||||
# Setting low_cpu_mem_usage=False and device_map=None ensures tensors are fully materialized
|
||||
self._model = CrossEncoder(
|
||||
self.model_name,
|
||||
model_kwargs={"low_cpu_mem_usage": False, "device_map": None},
|
||||
)
|
||||
self._model = CrossEncoder(self.model_name)
|
||||
logger.info("Reranker: local provider initialized")
|
||||
|
||||
def predict(self, pairs: List[Tuple[str, str]]) -> List[float]:
|
||||
|
||||
@@ -6,6 +6,9 @@ import time
|
||||
import asyncio
|
||||
from typing import Optional, Any, Dict, List
|
||||
from openai import AsyncOpenAI, RateLimitError, APIError, APIStatusError, APIConnectionError, LengthFinishReasonError
|
||||
from google import genai
|
||||
from google.genai import types as genai_types
|
||||
from google.genai import errors as genai_errors
|
||||
import logging
|
||||
|
||||
# Seed applied to every Groq request for deterministic behavior.
|
||||
@@ -33,9 +36,9 @@ class OutputTooLongError(Exception):
|
||||
|
||||
class LLMProvider:
|
||||
"""
|
||||
Unified LLM provider using OpenAI-compatible API.
|
||||
Unified LLM provider.
|
||||
|
||||
Supports OpenAI, Groq, and Ollama (any OpenAI-compatible endpoint).
|
||||
Supports OpenAI, Groq, Ollama (OpenAI-compatible), and Gemini.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -50,7 +53,7 @@ class LLMProvider:
|
||||
Initialize LLM provider.
|
||||
|
||||
Args:
|
||||
provider: Provider name ("openai", "groq", "ollama").
|
||||
provider: Provider name ("openai", "groq", "ollama", "gemini").
|
||||
api_key: API key.
|
||||
base_url: Base URL for the API.
|
||||
model: Model name.
|
||||
@@ -63,7 +66,7 @@ class LLMProvider:
|
||||
self.reasoning_effort = reasoning_effort
|
||||
|
||||
# Validate provider
|
||||
valid_providers = ["openai", "groq", "ollama"]
|
||||
valid_providers = ["openai", "groq", "ollama", "gemini"]
|
||||
if self.provider not in valid_providers:
|
||||
raise ValueError(
|
||||
f"Invalid LLM provider: {self.provider}. Must be one of: {', '.join(valid_providers)}"
|
||||
@@ -80,15 +83,43 @@ class LLMProvider:
|
||||
if self.provider != "ollama" and not self.api_key:
|
||||
raise ValueError(f"API key not found for {self.provider}")
|
||||
|
||||
# Create OpenAI-compatible client for all providers
|
||||
if self.provider == "ollama":
|
||||
# Create client based on provider
|
||||
if self.provider == "gemini":
|
||||
self._gemini_client = genai.Client(api_key=self.api_key)
|
||||
self._client = None
|
||||
elif self.provider == "ollama":
|
||||
self._client = AsyncOpenAI(api_key="ollama", base_url=self.base_url, max_retries=0)
|
||||
self._gemini_client = None
|
||||
else:
|
||||
self._client = AsyncOpenAI(api_key=self.api_key, base_url=self.base_url, max_retries=0)
|
||||
# Only pass base_url if it's set (OpenAI uses default URL otherwise)
|
||||
client_kwargs = {"api_key": self.api_key, "max_retries": 0}
|
||||
if self.base_url:
|
||||
client_kwargs["base_url"] = self.base_url
|
||||
self._client = AsyncOpenAI(**client_kwargs)
|
||||
self._gemini_client = None
|
||||
|
||||
logger.info(
|
||||
f"Initialized LLM: provider={self.provider}, model={self.model}, base_url={self.base_url}"
|
||||
)
|
||||
async def verify_connection(self) -> None:
|
||||
"""
|
||||
Verify that the LLM provider is configured correctly by making a simple test call.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the connection test fails.
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Verifying LLM: provider={self.provider}, model={self.model}, base_url={self.base_url or 'default'}...")
|
||||
await self.call(
|
||||
messages=[{"role": "user", "content": "Say 'ok'"}],
|
||||
max_completion_tokens=10,
|
||||
max_retries=2,
|
||||
initial_backoff=0.5,
|
||||
max_backoff=2.0,
|
||||
)
|
||||
# If we get here without exception, the connection is working
|
||||
logger.info(f"LLM verified: {self.provider}/{self.model}")
|
||||
except Exception as e:
|
||||
raise RuntimeError(
|
||||
f"LLM connection verification failed for {self.provider}/{self.model}: {e}"
|
||||
) from e
|
||||
|
||||
async def call(
|
||||
self,
|
||||
@@ -127,24 +158,50 @@ class LLMProvider:
|
||||
start_time = time.time()
|
||||
import json
|
||||
|
||||
# Handle Gemini provider separately
|
||||
if self.provider == "gemini":
|
||||
return await self._call_gemini(
|
||||
messages, response_format, max_retries, initial_backoff,
|
||||
max_backoff, skip_validation, start_time
|
||||
)
|
||||
|
||||
call_params = {
|
||||
"model": self.model,
|
||||
"messages": messages,
|
||||
}
|
||||
|
||||
# Check if model supports reasoning parameter (o1, o3, gpt-5 families)
|
||||
model_lower = self.model.lower()
|
||||
is_reasoning_model = any(x in model_lower for x in ["gpt-5", "o1", "o3"])
|
||||
|
||||
# For GPT-4 and GPT-4.1 models, cap max_completion_tokens to 32000
|
||||
is_gpt4_model = any(x in model_lower for x in ["gpt-4.1", "gpt-4-"])
|
||||
if max_completion_tokens is not None:
|
||||
if is_gpt4_model and max_completion_tokens > 32000:
|
||||
max_completion_tokens = 32000
|
||||
# For reasoning models, max_completion_tokens includes reasoning + output tokens
|
||||
# Enforce minimum of 16000 to ensure enough space for both
|
||||
if is_reasoning_model and max_completion_tokens < 16000:
|
||||
max_completion_tokens = 16000
|
||||
call_params["max_completion_tokens"] = max_completion_tokens
|
||||
if temperature is not None:
|
||||
|
||||
# GPT-5/o1/o3 family doesn't support custom temperature (only default 1)
|
||||
if temperature is not None and not is_reasoning_model:
|
||||
call_params["temperature"] = temperature
|
||||
|
||||
# Set reasoning_effort for reasoning models (OpenAI gpt-5, o1, o3)
|
||||
if is_reasoning_model and self.provider == "openai":
|
||||
call_params["reasoning_effort"] = self.reasoning_effort
|
||||
|
||||
# Provider-specific parameters
|
||||
if self.provider == "groq":
|
||||
call_params["seed"] = DEFAULT_LLM_SEED
|
||||
call_params["extra_body"] = {
|
||||
"service_tier": "auto",
|
||||
"reasoning_effort": self.reasoning_effort,
|
||||
"include_reasoning": False,
|
||||
}
|
||||
extra_body = {"service_tier": "auto"}
|
||||
# Only add reasoning parameters for reasoning models
|
||||
if is_reasoning_model:
|
||||
extra_body["reasoning_effort"] = self.reasoning_effort
|
||||
extra_body["include_reasoning"] = False
|
||||
call_params["extra_body"] = extra_body
|
||||
|
||||
last_exception = None
|
||||
|
||||
@@ -201,7 +258,8 @@ class LLMProvider:
|
||||
except APIConnectionError as e:
|
||||
last_exception = e
|
||||
if attempt < max_retries:
|
||||
logger.warning(f"Connection error, retrying... (attempt {attempt + 1}/{max_retries + 1})")
|
||||
status_code = getattr(e, 'status_code', None) or getattr(getattr(e, 'response', None), 'status_code', None)
|
||||
logger.warning(f"Connection error, retrying... (attempt {attempt + 1}/{max_retries + 1}) - status_code={status_code}, message={e}")
|
||||
backoff = min(initial_backoff * (2 ** attempt), max_backoff)
|
||||
await asyncio.sleep(backoff)
|
||||
continue
|
||||
@@ -233,6 +291,150 @@ class LLMProvider:
|
||||
raise last_exception
|
||||
raise RuntimeError(f"LLM call failed after all retries with no exception captured")
|
||||
|
||||
async def _call_gemini(
|
||||
self,
|
||||
messages: List[Dict[str, str]],
|
||||
response_format: Optional[Any],
|
||||
max_retries: int,
|
||||
initial_backoff: float,
|
||||
max_backoff: float,
|
||||
skip_validation: bool,
|
||||
start_time: float,
|
||||
) -> Any:
|
||||
"""Handle Gemini-specific API calls."""
|
||||
import json
|
||||
|
||||
# Convert OpenAI-style messages to Gemini format
|
||||
system_instruction = None
|
||||
gemini_contents = []
|
||||
|
||||
for msg in messages:
|
||||
role = msg.get('role', 'user')
|
||||
content = msg.get('content', '')
|
||||
|
||||
if role == 'system':
|
||||
if system_instruction:
|
||||
system_instruction += "\n\n" + content
|
||||
else:
|
||||
system_instruction = content
|
||||
elif role == 'assistant':
|
||||
gemini_contents.append(genai_types.Content(
|
||||
role="model",
|
||||
parts=[genai_types.Part(text=content)]
|
||||
))
|
||||
else:
|
||||
gemini_contents.append(genai_types.Content(
|
||||
role="user",
|
||||
parts=[genai_types.Part(text=content)]
|
||||
))
|
||||
|
||||
# Add JSON schema instruction if response_format is provided
|
||||
if response_format is not None and hasattr(response_format, 'model_json_schema'):
|
||||
schema = response_format.model_json_schema()
|
||||
schema_msg = f"\n\nYou must respond with valid JSON matching this schema:\n{json.dumps(schema, indent=2)}"
|
||||
if system_instruction:
|
||||
system_instruction += schema_msg
|
||||
else:
|
||||
system_instruction = schema_msg
|
||||
|
||||
# Build generation config
|
||||
config_kwargs = {}
|
||||
if system_instruction:
|
||||
config_kwargs['system_instruction'] = system_instruction
|
||||
if response_format is not None:
|
||||
config_kwargs['response_mime_type'] = 'application/json'
|
||||
config_kwargs['response_schema'] = response_format
|
||||
|
||||
generation_config = genai_types.GenerateContentConfig(**config_kwargs) if config_kwargs else None
|
||||
|
||||
last_exception = None
|
||||
|
||||
for attempt in range(max_retries + 1):
|
||||
try:
|
||||
response = await self._gemini_client.aio.models.generate_content(
|
||||
model=self.model,
|
||||
contents=gemini_contents,
|
||||
config=generation_config,
|
||||
)
|
||||
|
||||
content = response.text
|
||||
|
||||
# Handle empty response
|
||||
if content is None:
|
||||
block_reason = None
|
||||
if hasattr(response, 'candidates') and response.candidates:
|
||||
candidate = response.candidates[0]
|
||||
if hasattr(candidate, 'finish_reason'):
|
||||
block_reason = candidate.finish_reason
|
||||
|
||||
if attempt < max_retries:
|
||||
logger.warning(f"Gemini returned empty response (reason: {block_reason}), retrying...")
|
||||
backoff = min(initial_backoff * (2 ** attempt), max_backoff)
|
||||
await asyncio.sleep(backoff)
|
||||
continue
|
||||
else:
|
||||
raise RuntimeError(f"Gemini returned empty response after {max_retries + 1} attempts")
|
||||
|
||||
if response_format is not None:
|
||||
json_data = json.loads(content)
|
||||
if skip_validation:
|
||||
result = json_data
|
||||
else:
|
||||
result = response_format.model_validate(json_data)
|
||||
else:
|
||||
result = content
|
||||
|
||||
# Log slow calls
|
||||
duration = time.time() - start_time
|
||||
if duration > 10.0 and hasattr(response, 'usage_metadata') and response.usage_metadata:
|
||||
usage = response.usage_metadata
|
||||
logger.info(
|
||||
f"slow llm call: model={self.provider}/{self.model}, "
|
||||
f"input_tokens={usage.prompt_token_count}, output_tokens={usage.candidates_token_count}, "
|
||||
f"time={duration:.3f}s"
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
last_exception = e
|
||||
if attempt < max_retries:
|
||||
logger.warning(f"Gemini returned invalid JSON, retrying...")
|
||||
backoff = min(initial_backoff * (2 ** attempt), max_backoff)
|
||||
await asyncio.sleep(backoff)
|
||||
continue
|
||||
else:
|
||||
logger.error(f"Gemini returned invalid JSON after {max_retries + 1} attempts")
|
||||
raise
|
||||
|
||||
except genai_errors.APIError as e:
|
||||
# Fast fail on 4xx client errors (except 429 rate limit)
|
||||
if e.code and 400 <= e.code < 500 and e.code != 429:
|
||||
logger.error(f"Gemini client error (HTTP {e.code}), not retrying: {str(e)}")
|
||||
raise
|
||||
|
||||
# Retry on 429 and 5xx
|
||||
if e.code in (429, 500, 502, 503, 504):
|
||||
last_exception = e
|
||||
if attempt < max_retries:
|
||||
backoff = min(initial_backoff * (2 ** attempt), max_backoff)
|
||||
jitter = backoff * 0.2 * (2 * (time.time() % 1) - 1)
|
||||
await asyncio.sleep(backoff + jitter)
|
||||
else:
|
||||
logger.error(f"Gemini API error after {max_retries + 1} attempts: {str(e)}")
|
||||
raise
|
||||
else:
|
||||
logger.error(f"Gemini API error: {type(e).__name__}: {str(e)}")
|
||||
raise
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error during Gemini call: {type(e).__name__}: {str(e)}")
|
||||
raise
|
||||
|
||||
if last_exception:
|
||||
raise last_exception
|
||||
raise RuntimeError(f"Gemini call failed after all retries")
|
||||
|
||||
@classmethod
|
||||
def for_memory(cls) -> "LLMProvider":
|
||||
"""Create provider for memory operations from environment variables."""
|
||||
|
||||
@@ -330,7 +330,7 @@ class MemoryEngine:
|
||||
await self._handle_reinforce_opinion(task_dict)
|
||||
elif task_type == 'form_opinion':
|
||||
await self._handle_form_opinion(task_dict)
|
||||
elif task_type == 'batch_put':
|
||||
elif task_type == 'batch_retain':
|
||||
await self._handle_batch_retain(task_dict)
|
||||
elif task_type == 'regenerate_observations':
|
||||
await self._handle_regenerate_observations(task_dict)
|
||||
@@ -453,12 +453,17 @@ class MemoryEngine:
|
||||
# Query analyzer load is sync and CPU-bound
|
||||
await loop.run_in_executor(None, self.query_analyzer.load)
|
||||
|
||||
async def verify_llm():
|
||||
"""Verify LLM connection is working."""
|
||||
await self._llm_config.verify_connection()
|
||||
|
||||
# Run pg0 and all model initializations in parallel
|
||||
await asyncio.gather(
|
||||
start_pg0(),
|
||||
init_embeddings(),
|
||||
init_cross_encoder(),
|
||||
init_query_analyzer(),
|
||||
verify_llm(),
|
||||
)
|
||||
|
||||
# Run database migrations if enabled
|
||||
@@ -1791,10 +1796,14 @@ class MemoryEngine:
|
||||
# Delete entities (cascades to unit_entities, entity_cooccurrences, memory_links with entity_id)
|
||||
await conn.execute("DELETE FROM entities WHERE bank_id = $1", bank_id)
|
||||
|
||||
# Delete the bank profile itself
|
||||
await conn.execute("DELETE FROM banks WHERE bank_id = $1", bank_id)
|
||||
|
||||
return {
|
||||
"memory_units_deleted": units_count,
|
||||
"entities_deleted": entities_count,
|
||||
"documents_deleted": documents_count
|
||||
"documents_deleted": documents_count,
|
||||
"bank_deleted": True
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
@@ -1839,10 +1848,11 @@ class MemoryEngine:
|
||||
""", *query_params)
|
||||
|
||||
# Get links, filtering to only include links between units of the selected agent
|
||||
# Use DISTINCT ON with LEAST/GREATEST to deduplicate bidirectional links
|
||||
unit_ids = [row['id'] for row in units]
|
||||
if unit_ids:
|
||||
links = await conn.fetch("""
|
||||
SELECT
|
||||
SELECT DISTINCT ON (LEAST(ml.from_unit_id, ml.to_unit_id), GREATEST(ml.from_unit_id, ml.to_unit_id), ml.link_type, COALESCE(ml.entity_id, '00000000-0000-0000-0000-000000000000'::uuid))
|
||||
ml.from_unit_id,
|
||||
ml.to_unit_id,
|
||||
ml.link_type,
|
||||
@@ -1851,7 +1861,7 @@ class MemoryEngine:
|
||||
FROM memory_links ml
|
||||
LEFT JOIN entities e ON ml.entity_id = e.id
|
||||
WHERE ml.from_unit_id = ANY($1::uuid[]) AND ml.to_unit_id = ANY($1::uuid[])
|
||||
ORDER BY ml.link_type, ml.weight DESC
|
||||
ORDER BY LEAST(ml.from_unit_id, ml.to_unit_id), GREATEST(ml.from_unit_id, ml.to_unit_id), ml.link_type, COALESCE(ml.entity_id, '00000000-0000-0000-0000-000000000000'::uuid), ml.weight DESC
|
||||
""", unit_ids)
|
||||
else:
|
||||
links = []
|
||||
|
||||
@@ -390,6 +390,27 @@ async def create_temporal_links_batch_per_fact(
|
||||
# Filter and create links in memory (much faster than N queries)
|
||||
link_gen_start = time_mod.time()
|
||||
links = compute_temporal_links(new_units, all_candidates, time_window_hours)
|
||||
|
||||
# Also compute temporal links WITHIN the new batch (new units to each other)
|
||||
if len(new_units) > 1:
|
||||
# Convert new_units dict to candidate format for within-batch linking
|
||||
new_unit_items = list(new_units.items())
|
||||
for i, (unit_id, event_date) in enumerate(new_unit_items):
|
||||
unit_event_date_norm = _normalize_datetime(event_date)
|
||||
|
||||
# Compare with other new units (only those after this one to avoid duplicates)
|
||||
for j in range(i + 1, len(new_unit_items)):
|
||||
other_id, other_event_date = new_unit_items[j]
|
||||
other_event_date_norm = _normalize_datetime(other_event_date)
|
||||
|
||||
# Check if within time window
|
||||
time_diff_hours = abs((unit_event_date_norm - other_event_date_norm).total_seconds() / 3600)
|
||||
if time_diff_hours <= time_window_hours:
|
||||
weight = max(0.3, 1.0 - (time_diff_hours / time_window_hours))
|
||||
# Create bidirectional links
|
||||
links.append((unit_id, other_id, 'temporal', weight, None))
|
||||
links.append((other_id, unit_id, 'temporal', weight, None))
|
||||
|
||||
_log(log_buffer, f" [7.3] Generate {len(links)} temporal links: {time_mod.time() - link_gen_start:.3f}s")
|
||||
|
||||
if links:
|
||||
@@ -514,9 +535,38 @@ async def create_semantic_links_batch(
|
||||
|
||||
for idx in sorted_indices:
|
||||
similar_id = existing_ids[idx]
|
||||
similarity = float(similarities[idx])
|
||||
# Clamp to [0, 1] to handle floating point precision issues
|
||||
similarity = float(min(1.0, max(0.0, similarities[idx])))
|
||||
all_links.append((unit_id, similar_id, 'semantic', similarity, None))
|
||||
|
||||
# Also compute similarities WITHIN the new batch (new units to each other)
|
||||
# Apply the same top_k limit per unit as we do for existing units
|
||||
if len(unit_ids) > 1:
|
||||
new_embeddings_matrix = np.array(embeddings)
|
||||
|
||||
for i, unit_id in enumerate(unit_ids):
|
||||
# Compute similarities with all OTHER new units
|
||||
other_indices = [j for j in range(len(unit_ids)) if j != i]
|
||||
if not other_indices:
|
||||
continue
|
||||
|
||||
other_embeddings = new_embeddings_matrix[other_indices]
|
||||
similarities = np.dot(other_embeddings, new_embeddings_matrix[i])
|
||||
|
||||
# Find top-k above threshold (same logic as existing units)
|
||||
above_threshold = np.where(similarities >= threshold)[0]
|
||||
|
||||
if len(above_threshold) > 0:
|
||||
# Sort by similarity (descending) and take top-k
|
||||
sorted_local_indices = above_threshold[np.argsort(-similarities[above_threshold])][:top_k]
|
||||
|
||||
for local_idx in sorted_local_indices:
|
||||
other_idx = other_indices[local_idx]
|
||||
other_id = unit_ids[other_idx]
|
||||
# Clamp to [0, 1] to handle floating point precision issues
|
||||
similarity = float(min(1.0, max(0.0, similarities[local_idx])))
|
||||
all_links.append((unit_id, other_id, 'semantic', similarity, None))
|
||||
|
||||
_log(log_buffer, f" [8.2] Compute similarities & generate {len(all_links)} semantic links: {time_mod.time() - compute_start:.3f}s")
|
||||
|
||||
if all_links:
|
||||
|
||||
@@ -21,6 +21,10 @@ from . import MemoryEngine
|
||||
from .api import create_app
|
||||
from .config import get_config, HindsightConfig
|
||||
|
||||
from .banner import print_banner
|
||||
print()
|
||||
print_banner()
|
||||
|
||||
# Filter deprecation warnings from third-party libraries
|
||||
warnings.filterwarnings("ignore", message="websockets.legacy is deprecated")
|
||||
warnings.filterwarnings("ignore", message="websockets.server.WebSocketServerProtocol is deprecated")
|
||||
@@ -184,15 +188,19 @@ def main():
|
||||
if args.ssl_certfile:
|
||||
uvicorn_config["ssl_certfile"] = args.ssl_certfile
|
||||
|
||||
print(f"\nStarting Hindsight API...")
|
||||
print(f" URL: http://{args.host}:{args.port}")
|
||||
print(f" Database: {config.database_url}")
|
||||
print(f" LLM: {config.llm_provider} / {config.llm_model}")
|
||||
print(f" Embeddings: {config.embeddings_provider}")
|
||||
print(f" Reranker: {config.reranker_provider}")
|
||||
if config.mcp_enabled:
|
||||
print(f" MCP: enabled at /mcp")
|
||||
print()
|
||||
|
||||
|
||||
from .banner import print_startup_info
|
||||
print_startup_info(
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
database_url=config.database_url,
|
||||
llm_provider=config.llm_provider,
|
||||
llm_model=config.llm_model,
|
||||
embeddings_provider=config.embeddings_provider,
|
||||
reranker_provider=config.reranker_provider,
|
||||
mcp_enabled=config.mcp_enabled,
|
||||
)
|
||||
|
||||
uvicorn.run(**uvicorn_config)
|
||||
|
||||
|
||||
@@ -257,16 +257,17 @@ class EmbeddedPostgres:
|
||||
last_error = stderr or f"pg0 start returned exit code {returncode}"
|
||||
if attempt < max_retries:
|
||||
delay = retry_delay * (2 ** (attempt - 1))
|
||||
logger.warning(f"pg0 start attempt {attempt}/{max_retries} failed: {last_error.strip()}")
|
||||
logger.info(f"Retrying in {delay:.1f}s...")
|
||||
logger.debug(f"pg0 start attempt {attempt}/{max_retries} failed: {last_error.strip()}")
|
||||
logger.debug(f"Retrying in {delay:.1f}s...")
|
||||
await asyncio.sleep(delay)
|
||||
else:
|
||||
logger.warning(f"pg0 start attempt {attempt}/{max_retries} failed: {last_error.strip()}")
|
||||
logger.debug(f"pg0 start attempt {attempt}/{max_retries} failed: {last_error.strip()}")
|
||||
|
||||
# All retries exhausted - use constructed URI as fallback
|
||||
uri = f"postgresql://{self.username}:{self.password}@localhost:{self.port}/{self.database}"
|
||||
logger.warning(f"All pg0 start attempts failed, using constructed URI: {uri}")
|
||||
return uri
|
||||
# All retries exhausted - fail
|
||||
raise RuntimeError(
|
||||
f"Failed to start embedded PostgreSQL after {max_retries} attempts. "
|
||||
f"Last error: {last_error.strip() if last_error else 'unknown'}"
|
||||
)
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""Stop the PostgreSQL server."""
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "hindsight-api"
|
||||
version = "0.1.0"
|
||||
version = "0.1.4"
|
||||
description = "Temporal + Semantic + Entity Memory System for AI agents using PostgreSQL"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
@@ -14,7 +14,7 @@ dependencies = [
|
||||
"openai>=1.0.0",
|
||||
"pydantic>=2.0.0",
|
||||
"rich>=13.0.0",
|
||||
"sentence-transformers>=3.0.0",
|
||||
"sentence-transformers>=3.0.0,<3.3.0",
|
||||
"langchain-text-splitters>=0.3.0",
|
||||
"fastapi[standard]>=0.120.3",
|
||||
"uvicorn>=0.38.0",
|
||||
@@ -24,8 +24,8 @@ dependencies = [
|
||||
"pgvector>=0.4.1",
|
||||
"greenlet>=3.2.4",
|
||||
"psycopg2-binary>=2.9.11",
|
||||
"transformers>=4.30.0",
|
||||
"torch>=2.0.0",
|
||||
"transformers>=4.30.0,<4.46.0",
|
||||
"torch>=2.0.0,<2.6.0",
|
||||
"tiktoken>=0.12.0",
|
||||
"httpx>=0.27.0",
|
||||
"fastmcp>=2.0.0",
|
||||
|
||||
@@ -281,7 +281,8 @@ async def test_full_api_workflow(api_client, test_bank_id):
|
||||
final_banks_data = response.json()["banks"]
|
||||
final_banks = [a["bank_id"] for a in final_banks_data]
|
||||
assert test_bank_id in final_banks
|
||||
assert len(final_banks) >= len(initial_banks) + 1
|
||||
# Don't assert count increases due to parallel test cleanup races
|
||||
# Just verify our bank exists in the list
|
||||
|
||||
# ================================================================
|
||||
# 10. Clean Up
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
"""
|
||||
Test LLM provider with different models and providers.
|
||||
"""
|
||||
import os
|
||||
import pytest
|
||||
from hindsight_api.engine.llm_wrapper import LLMProvider
|
||||
|
||||
|
||||
# Model matrix: (provider, model)
|
||||
MODEL_MATRIX = [
|
||||
# OpenAI models
|
||||
("openai", "gpt-4o-mini"),
|
||||
("openai", "gpt-4.1-mini"),
|
||||
("openai", "gpt-4.1-nano"),
|
||||
("openai", "gpt-5-mini"),
|
||||
("openai", "gpt-5-nano"),
|
||||
("openai", "gpt-5"),
|
||||
# Groq models
|
||||
("groq", "llama-3.3-70b-versatile"),
|
||||
("groq", "openai/gpt-oss-120b"),
|
||||
("groq", "openai/gpt-oss-20b"),
|
||||
# Gemini models
|
||||
("gemini", "gemini-2.5-flash"),
|
||||
("gemini", "gemini-2.5-flash-lite"),
|
||||
]
|
||||
|
||||
|
||||
def get_api_key_for_provider(provider: str) -> str | None:
|
||||
"""Get API key for provider from environment variables."""
|
||||
provider_key_map = {
|
||||
"openai": "OPENAI_API_KEY",
|
||||
"groq": "GROQ_API_KEY",
|
||||
"gemini": "GEMINI_API_KEY",
|
||||
}
|
||||
env_var = provider_key_map.get(provider)
|
||||
return os.getenv(env_var) if env_var else None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("provider,model", MODEL_MATRIX)
|
||||
@pytest.mark.asyncio
|
||||
async def test_llm_provider_call(provider: str, model: str):
|
||||
"""
|
||||
Test LLM provider can make a basic call with different models.
|
||||
Skips if the required API key is not available.
|
||||
"""
|
||||
api_key = get_api_key_for_provider(provider)
|
||||
if not api_key:
|
||||
pytest.skip(f"Skipping {provider}/{model}: no API key available")
|
||||
|
||||
llm = LLMProvider(
|
||||
provider=provider,
|
||||
api_key=api_key,
|
||||
base_url="",
|
||||
model=model,
|
||||
)
|
||||
|
||||
# Test basic call
|
||||
response = await llm.call(
|
||||
messages=[{"role": "user", "content": "Say 'hello' and nothing else."}],
|
||||
max_completion_tokens=50,
|
||||
temperature=0.1,
|
||||
)
|
||||
|
||||
print(f"\n{provider}/{model} response: {response}")
|
||||
assert response is not None, f"{provider}/{model} returned None"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("provider,model", MODEL_MATRIX)
|
||||
@pytest.mark.asyncio
|
||||
async def test_llm_provider_verify_connection(provider: str, model: str):
|
||||
"""
|
||||
Test LLM provider verify_connection method with different models.
|
||||
Skips if the required API key is not available.
|
||||
"""
|
||||
api_key = get_api_key_for_provider(provider)
|
||||
if not api_key:
|
||||
pytest.skip(f"Skipping {provider}/{model}: no API key available")
|
||||
|
||||
llm = LLMProvider(
|
||||
provider=provider,
|
||||
api_key=api_key,
|
||||
base_url="",
|
||||
model=model,
|
||||
)
|
||||
|
||||
# Test verify_connection
|
||||
await llm.verify_connection()
|
||||
print(f"\n{provider}/{model} connection verified")
|
||||
|
||||
|
||||
# Models that support large output (65000+ tokens)
|
||||
LARGE_OUTPUT_MODELS = [
|
||||
("openai", "gpt-5-mini"),
|
||||
("openai", "gpt-5-nano"),
|
||||
("openai", "gpt-5"),
|
||||
("gemini", "gemini-2.5-flash"),
|
||||
("gemini", "gemini-2.5-flash-lite"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("provider,model", LARGE_OUTPUT_MODELS)
|
||||
@pytest.mark.asyncio
|
||||
async def test_llm_provider_large_output(provider: str, model: str):
|
||||
"""
|
||||
Test LLM provider with large max_completion_tokens (65000).
|
||||
Only tests models that support large outputs.
|
||||
Skips if the required API key is not available.
|
||||
"""
|
||||
api_key = get_api_key_for_provider(provider)
|
||||
if not api_key:
|
||||
pytest.skip(f"Skipping {provider}/{model}: no API key available")
|
||||
|
||||
llm = LLMProvider(
|
||||
provider=provider,
|
||||
api_key=api_key,
|
||||
base_url="",
|
||||
model=model,
|
||||
)
|
||||
|
||||
# Test call with large max_completion_tokens
|
||||
response = await llm.call(
|
||||
messages=[{"role": "user", "content": "Say 'ok'"}],
|
||||
max_completion_tokens=65000,
|
||||
)
|
||||
|
||||
print(f"\n{provider}/{model} large output response: {response}")
|
||||
assert response is not None, f"{provider}/{model} returned None"
|
||||
@@ -3,7 +3,7 @@ Test retain function and chunk storage.
|
||||
"""
|
||||
import pytest
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from hindsight_api.engine.memory_engine import Budget
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -1595,3 +1595,133 @@ async def test_all_link_types_together(memory):
|
||||
|
||||
finally:
|
||||
await memory.delete_bank(bank_id)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_links_within_same_batch(memory):
|
||||
"""
|
||||
Test that semantic links are created between facts retained in the SAME batch.
|
||||
|
||||
This is a regression test - semantic links should connect similar facts
|
||||
even when they are retained together in a single call.
|
||||
"""
|
||||
bank_id = f"test_semantic_batch_{datetime.now(timezone.utc).timestamp()}"
|
||||
|
||||
try:
|
||||
# Retain multiple semantically similar facts in ONE batch
|
||||
contents = [
|
||||
{"content": "Alice is an expert in Python programming and machine learning.", "context": "team skills"},
|
||||
{"content": "Bob specializes in Python development and data science.", "context": "team skills"},
|
||||
{"content": "Charlie works with Python for backend API development.", "context": "team skills"},
|
||||
]
|
||||
|
||||
result = await memory.retain_batch_async(
|
||||
bank_id=bank_id,
|
||||
contents=contents
|
||||
)
|
||||
|
||||
# Flatten the list of lists
|
||||
unit_ids = [uid for sublist in result for uid in sublist]
|
||||
|
||||
assert len(unit_ids) >= 3, f"Should have created at least 3 facts, got {len(unit_ids)}"
|
||||
logger.info(f"Created {len(unit_ids)} facts in single batch")
|
||||
|
||||
# Query semantic links between these units
|
||||
async with memory._pool.acquire() as conn:
|
||||
semantic_links = await conn.fetch(
|
||||
"""
|
||||
SELECT from_unit_id, to_unit_id, weight
|
||||
FROM memory_links
|
||||
WHERE from_unit_id::text = ANY($1)
|
||||
AND to_unit_id::text = ANY($1)
|
||||
AND link_type = 'semantic'
|
||||
""",
|
||||
unit_ids
|
||||
)
|
||||
|
||||
logger.info(f"Found {len(semantic_links)} semantic links within the batch")
|
||||
|
||||
# All three facts mention Python - they should be linked to each other
|
||||
assert len(semantic_links) > 0, (
|
||||
"REGRESSION: Semantic links should be created between similar facts "
|
||||
"retained in the same batch, but none were found"
|
||||
)
|
||||
|
||||
# Log the links for debugging
|
||||
for link in semantic_links:
|
||||
logger.info(f" Semantic link: {str(link['from_unit_id'])[:8]}... -> {str(link['to_unit_id'])[:8]}... (weight: {link['weight']:.3f})")
|
||||
|
||||
finally:
|
||||
await memory.delete_bank(bank_id)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_temporal_links_within_same_batch(memory):
|
||||
"""
|
||||
Test that temporal links are created between facts retained in the SAME batch.
|
||||
|
||||
This is a regression test - temporal links should connect facts with nearby
|
||||
event dates even when they are retained together in a single call.
|
||||
"""
|
||||
bank_id = f"test_temporal_batch_{datetime.now(timezone.utc).timestamp()}"
|
||||
|
||||
try:
|
||||
# Retain multiple facts with nearby timestamps in ONE batch
|
||||
base_date = datetime(2024, 6, 15, 10, 0, 0, tzinfo=timezone.utc)
|
||||
|
||||
contents = [
|
||||
{
|
||||
"content": "Morning standup: Alice presented the sprint goals.",
|
||||
"context": "daily meeting",
|
||||
"event_date": base_date
|
||||
},
|
||||
{
|
||||
"content": "Bob demoed the new feature after standup.",
|
||||
"context": "daily meeting",
|
||||
"event_date": base_date + timedelta(hours=1) # 1 hour later
|
||||
},
|
||||
{
|
||||
"content": "Charlie reviewed the pull requests in the afternoon.",
|
||||
"context": "daily meeting",
|
||||
"event_date": base_date + timedelta(hours=4) # 4 hours later
|
||||
},
|
||||
]
|
||||
|
||||
result = await memory.retain_batch_async(
|
||||
bank_id=bank_id,
|
||||
contents=contents
|
||||
)
|
||||
|
||||
# Flatten the list of lists
|
||||
unit_ids = [uid for sublist in result for uid in sublist]
|
||||
|
||||
assert len(unit_ids) >= 3, f"Should have created at least 3 facts, got {len(unit_ids)}"
|
||||
logger.info(f"Created {len(unit_ids)} facts in single batch")
|
||||
|
||||
# Query temporal links between these units
|
||||
async with memory._pool.acquire() as conn:
|
||||
temporal_links = await conn.fetch(
|
||||
"""
|
||||
SELECT from_unit_id, to_unit_id, weight
|
||||
FROM memory_links
|
||||
WHERE from_unit_id::text = ANY($1)
|
||||
AND to_unit_id::text = ANY($1)
|
||||
AND link_type = 'temporal'
|
||||
""",
|
||||
unit_ids
|
||||
)
|
||||
|
||||
logger.info(f"Found {len(temporal_links)} temporal links within the batch")
|
||||
|
||||
# All three facts are within 24 hours - they should be linked to each other
|
||||
assert len(temporal_links) > 0, (
|
||||
"REGRESSION: Temporal links should be created between facts with nearby dates "
|
||||
"retained in the same batch, but none were found"
|
||||
)
|
||||
|
||||
# Log the links for debugging
|
||||
for link in temporal_links:
|
||||
logger.info(f" Temporal link: {str(link['from_unit_id'])[:8]}... -> {str(link['to_unit_id'])[:8]}... (weight: {link['weight']:.3f})")
|
||||
|
||||
finally:
|
||||
await memory.delete_bank(bank_id)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "hindsight-cli"
|
||||
version = "0.1.0"
|
||||
version = "0.1.4"
|
||||
edition = "2021"
|
||||
authors = ["Hindsight Team"]
|
||||
description = "A beautiful CLI for Hindsight - semantic memory system"
|
||||
@@ -20,6 +20,9 @@ clap = { version = "4.5", features = ["derive", "env"] }
|
||||
# Async runtime
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
||||
# HTTP client (for timeout configuration)
|
||||
reqwest = "0.12"
|
||||
|
||||
# Serialization (for config and output formatting)
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
@@ -13,7 +13,7 @@ use std::collections::HashMap;
|
||||
// Types not defined in OpenAPI spec (TODO: add to openapi.json)
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct AgentStats {
|
||||
pub agent_id: String,
|
||||
pub bank_id: String,
|
||||
pub total_nodes: i32,
|
||||
pub total_links: i32,
|
||||
pub total_documents: i32,
|
||||
@@ -38,7 +38,7 @@ pub struct Operation {
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct OperationsResponse {
|
||||
pub agent_id: String,
|
||||
pub bank_id: String,
|
||||
pub operations: Vec<Operation>,
|
||||
}
|
||||
|
||||
@@ -66,7 +66,13 @@ pub struct ApiClient {
|
||||
impl ApiClient {
|
||||
pub fn new(base_url: String) -> Result<Self> {
|
||||
let runtime = std::sync::Arc::new(tokio::runtime::Runtime::new()?);
|
||||
let client = AsyncClient::new(&base_url);
|
||||
|
||||
// Create HTTP client with 2-minute timeout
|
||||
let http_client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(120))
|
||||
.build()?;
|
||||
|
||||
let client = AsyncClient::new_with_client(&base_url, http_client);
|
||||
Ok(ApiClient { client, runtime })
|
||||
}
|
||||
|
||||
@@ -231,6 +237,13 @@ impl ApiClient {
|
||||
Ok(response.into_inner())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn delete_bank(&self, bank_id: &str, _verbose: bool) -> Result<types::DeleteResponse> {
|
||||
self.runtime.block_on(async {
|
||||
let response = self.client.delete_bank(bank_id).await?;
|
||||
Ok(response.into_inner())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Re-export types from the generated client for use in commands
|
||||
|
||||
@@ -12,8 +12,8 @@ pub fn list(client: &ApiClient, verbose: bool, output_format: OutputFormat) -> R
|
||||
|
||||
let response = client.list_agents(verbose);
|
||||
|
||||
if let Some(sp) = spinner {
|
||||
sp.finish_and_clear();
|
||||
if let Some(mut sp) = spinner {
|
||||
sp.finish();
|
||||
}
|
||||
|
||||
match response {
|
||||
@@ -36,23 +36,23 @@ pub fn list(client: &ApiClient, verbose: bool, output_format: OutputFormat) -> R
|
||||
}
|
||||
}
|
||||
|
||||
pub fn profile(client: &ApiClient, bank_id: &str, verbose: bool, output_format: OutputFormat) -> Result<()> {
|
||||
pub fn disposition(client: &ApiClient, bank_id: &str, verbose: bool, output_format: OutputFormat) -> Result<()> {
|
||||
let spinner = if output_format == OutputFormat::Pretty {
|
||||
Some(ui::create_spinner("Fetching profile..."))
|
||||
Some(ui::create_spinner("Fetching disposition..."))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let response = client.get_profile(bank_id, verbose);
|
||||
|
||||
if let Some(sp) = spinner {
|
||||
sp.finish_and_clear();
|
||||
if let Some(mut sp) = spinner {
|
||||
sp.finish();
|
||||
}
|
||||
|
||||
match response {
|
||||
Ok(profile) => {
|
||||
if output_format == OutputFormat::Pretty {
|
||||
ui::print_profile(&profile);
|
||||
ui::print_disposition(&profile);
|
||||
} else {
|
||||
output::print_output(&profile, output_format)?;
|
||||
}
|
||||
@@ -71,92 +71,69 @@ pub fn stats(client: &ApiClient, bank_id: &str, verbose: bool, output_format: Ou
|
||||
|
||||
let response = client.get_stats(bank_id, verbose);
|
||||
|
||||
if let Some(sp) = spinner {
|
||||
sp.finish_and_clear();
|
||||
if let Some(mut sp) = spinner {
|
||||
sp.finish();
|
||||
}
|
||||
|
||||
match response {
|
||||
Ok(stats) => {
|
||||
if output_format == OutputFormat::Pretty {
|
||||
ui::print_info(&format!("Statistics for bank '{}'", bank_id));
|
||||
ui::print_section_header(&format!("Statistics: {}", bank_id));
|
||||
|
||||
println!(" {} {}", ui::dim("memory units:"), ui::gradient_start(&stats.total_nodes.to_string()));
|
||||
println!(" {} {}", ui::dim("links:"), ui::gradient_mid(&stats.total_links.to_string()));
|
||||
println!(" {} {}", ui::dim("documents:"), ui::gradient_end(&stats.total_documents.to_string()));
|
||||
println!();
|
||||
|
||||
println!(" 📊 Overview");
|
||||
println!(" Total Memory Units: {}", stats.total_nodes);
|
||||
println!(" Total Links: {}", stats.total_links);
|
||||
println!(" Total Documents: {}", stats.total_documents);
|
||||
println!();
|
||||
|
||||
println!(" 🧠 Memory Units by Type");
|
||||
println!("{}", ui::gradient_text("─── Memory Units by Type ───"));
|
||||
let mut fact_types: Vec<_> = stats.nodes_by_fact_type.iter().collect();
|
||||
fact_types.sort_by_key(|(k, _)| *k);
|
||||
for (fact_type, count) in fact_types {
|
||||
let icon = match fact_type.as_str() {
|
||||
"world" => "🌍",
|
||||
"agent" => "🤖",
|
||||
"opinion" => "💭",
|
||||
_ => "•"
|
||||
};
|
||||
println!(" {} {:<10} {}", icon, fact_type, count);
|
||||
for (i, (fact_type, count)) in fact_types.iter().enumerate() {
|
||||
let t = i as f32 / fact_types.len().max(1) as f32;
|
||||
println!(" {:<10} {}", fact_type, ui::gradient(&count.to_string(), t));
|
||||
}
|
||||
println!();
|
||||
|
||||
println!(" 🔗 Links by Type");
|
||||
println!("{}", ui::gradient_text("─── Links by Type ───"));
|
||||
let mut link_types: Vec<_> = stats.links_by_link_type.iter().collect();
|
||||
link_types.sort_by_key(|(k, _)| *k);
|
||||
for (link_type, count) in link_types {
|
||||
let icon = match link_type.as_str() {
|
||||
"temporal" => "⏰",
|
||||
"semantic" => "🔤",
|
||||
"entity" => "🏷️",
|
||||
_ => "•"
|
||||
};
|
||||
println!(" {} {:<10} {}", icon, link_type, count);
|
||||
for (i, (link_type, count)) in link_types.iter().enumerate() {
|
||||
let t = i as f32 / link_types.len().max(1) as f32;
|
||||
println!(" {:<10} {}", link_type, ui::gradient(&count.to_string(), t));
|
||||
}
|
||||
println!();
|
||||
|
||||
println!(" 🔗 Links by Fact Type");
|
||||
println!("{}", ui::gradient_text("─── Links by Fact Type ───"));
|
||||
let mut fact_type_links: Vec<_> = stats.links_by_fact_type.iter().collect();
|
||||
fact_type_links.sort_by_key(|(k, _)| *k);
|
||||
for (fact_type, count) in fact_type_links {
|
||||
let icon = match fact_type.as_str() {
|
||||
"world" => "🌍",
|
||||
"agent" => "🤖",
|
||||
"opinion" => "💭",
|
||||
_ => "•"
|
||||
};
|
||||
println!(" {} {:<10} {}", icon, fact_type, count);
|
||||
for (i, (fact_type, count)) in fact_type_links.iter().enumerate() {
|
||||
let t = i as f32 / fact_type_links.len().max(1) as f32;
|
||||
println!(" {:<10} {}", fact_type, ui::gradient(&count.to_string(), t));
|
||||
}
|
||||
println!();
|
||||
|
||||
if !stats.links_breakdown.is_empty() {
|
||||
println!(" 📈 Detailed Link Breakdown");
|
||||
println!("{}", ui::gradient_text("─── Detailed Link Breakdown ───"));
|
||||
let mut fact_types: Vec<_> = stats.links_breakdown.iter().collect();
|
||||
fact_types.sort_by_key(|(k, _)| *k);
|
||||
for (fact_type, link_types) in fact_types {
|
||||
let icon = match fact_type.as_str() {
|
||||
"world" => "🌍",
|
||||
"agent" => "🤖",
|
||||
"opinion" => "💭",
|
||||
_ => "•"
|
||||
};
|
||||
println!(" {} {}", icon, fact_type);
|
||||
println!(" {}", fact_type);
|
||||
let mut sorted_links: Vec<_> = link_types.iter().collect();
|
||||
sorted_links.sort_by_key(|(k, _)| *k);
|
||||
for (link_type, count) in sorted_links {
|
||||
println!(" - {:<10} {}", link_type, count);
|
||||
println!(" {:<10} {}", ui::dim(link_type), count);
|
||||
}
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
if stats.pending_operations > 0 || stats.failed_operations > 0 {
|
||||
println!(" ⚙️ Operations");
|
||||
println!("{}", ui::gradient_text("─── Operations ───"));
|
||||
if stats.pending_operations > 0 {
|
||||
println!(" ⏳ Pending: {}", stats.pending_operations);
|
||||
println!(" {} {}", ui::dim("pending:"), stats.pending_operations);
|
||||
}
|
||||
if stats.failed_operations > 0 {
|
||||
println!(" ❌ Failed: {}", stats.failed_operations);
|
||||
println!(" {} {}", ui::dim("failed:"), stats.failed_operations);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -177,8 +154,8 @@ pub fn update_name(client: &ApiClient, bank_id: &str, name: &str, verbose: bool,
|
||||
|
||||
let response = client.update_agent_name(bank_id, name, verbose);
|
||||
|
||||
if let Some(sp) = spinner {
|
||||
sp.finish_and_clear();
|
||||
if let Some(mut sp) = spinner {
|
||||
sp.finish();
|
||||
}
|
||||
|
||||
match response {
|
||||
@@ -216,8 +193,8 @@ pub fn update_background(
|
||||
|
||||
let response = client.add_background(bank_id, content, !no_update_disposition, verbose);
|
||||
|
||||
if let Some(sp) = spinner {
|
||||
sp.finish_and_clear();
|
||||
if let Some(mut sp) = spinner {
|
||||
sp.finish();
|
||||
}
|
||||
|
||||
match response {
|
||||
@@ -244,3 +221,57 @@ pub fn update_background(
|
||||
Err(e) => Err(e)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete(
|
||||
client: &ApiClient,
|
||||
bank_id: &str,
|
||||
yes: bool,
|
||||
verbose: bool,
|
||||
output_format: OutputFormat
|
||||
) -> Result<()> {
|
||||
// Confirmation prompt unless -y flag is used
|
||||
if !yes && output_format == OutputFormat::Pretty {
|
||||
let message = format!(
|
||||
"Are you sure you want to delete bank '{}' and ALL its data? This cannot be undone.",
|
||||
bank_id
|
||||
);
|
||||
|
||||
let confirmed = ui::prompt_confirmation(&message)?;
|
||||
|
||||
if !confirmed {
|
||||
ui::print_info("Operation cancelled");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let spinner = if output_format == OutputFormat::Pretty {
|
||||
Some(ui::create_spinner("Deleting bank..."))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let response = client.delete_bank(bank_id, verbose);
|
||||
|
||||
if let Some(mut sp) = spinner {
|
||||
sp.finish();
|
||||
}
|
||||
|
||||
match response {
|
||||
Ok(result) => {
|
||||
if output_format == OutputFormat::Pretty {
|
||||
if result.success {
|
||||
ui::print_success(&format!("Bank '{}' deleted successfully", bank_id));
|
||||
if let Some(count) = result.deleted_count {
|
||||
println!(" Items deleted: {}", count);
|
||||
}
|
||||
} else {
|
||||
ui::print_error("Failed to delete bank");
|
||||
}
|
||||
} else {
|
||||
output::print_output(&result, output_format)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => Err(e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,14 +20,14 @@ pub fn list(
|
||||
|
||||
let response = client.list_documents(agent_id, query.as_deref(), Some(limit), Some(offset), verbose);
|
||||
|
||||
if let Some(sp) = spinner {
|
||||
sp.finish_and_clear();
|
||||
if let Some(mut sp) = spinner {
|
||||
sp.finish();
|
||||
}
|
||||
|
||||
match response {
|
||||
Ok(docs_response) => {
|
||||
if output_format == OutputFormat::Pretty {
|
||||
ui::print_info(&format!("Documents for agent '{}' (total: {})", agent_id, docs_response.total));
|
||||
ui::print_info(&format!("Documents for bank '{}' (total: {})", agent_id, docs_response.total));
|
||||
for doc in &docs_response.items {
|
||||
let id = doc.get("id").and_then(|v| v.as_str()).unwrap_or("unknown");
|
||||
let created = doc.get("created_at").and_then(|v| v.as_str()).unwrap_or("unknown");
|
||||
@@ -65,8 +65,8 @@ pub fn get(
|
||||
|
||||
let response = client.get_document(agent_id, document_id, verbose);
|
||||
|
||||
if let Some(sp) = spinner {
|
||||
sp.finish_and_clear();
|
||||
if let Some(mut sp) = spinner {
|
||||
sp.finish();
|
||||
}
|
||||
|
||||
match response {
|
||||
@@ -102,8 +102,8 @@ pub fn delete(
|
||||
|
||||
let response = client.delete_document(agent_id, document_id, verbose);
|
||||
|
||||
if let Some(sp) = spinner {
|
||||
sp.finish_and_clear();
|
||||
if let Some(mut sp) = spinner {
|
||||
sp.finish();
|
||||
}
|
||||
|
||||
match response {
|
||||
|
||||
@@ -18,8 +18,8 @@ pub fn list(
|
||||
|
||||
let response = client.list_entities(bank_id, Some(limit), verbose)?;
|
||||
|
||||
if let Some(sp) = spinner {
|
||||
sp.finish_and_clear();
|
||||
if let Some(mut sp) = spinner {
|
||||
sp.finish();
|
||||
}
|
||||
|
||||
if output_format == OutputFormat::Pretty {
|
||||
@@ -66,8 +66,8 @@ pub fn get(
|
||||
|
||||
let response = client.get_entity(bank_id, entity_id, verbose)?;
|
||||
|
||||
if let Some(sp) = spinner {
|
||||
sp.finish_and_clear();
|
||||
if let Some(mut sp) = spinner {
|
||||
sp.finish();
|
||||
}
|
||||
|
||||
if output_format == OutputFormat::Pretty {
|
||||
@@ -84,17 +84,6 @@ pub fn get(
|
||||
println!("Last seen: {}", last_seen);
|
||||
}
|
||||
|
||||
// Show observations (always included)
|
||||
if !response.observations.is_empty() {
|
||||
println!("\nObservations ({}):", response.observations.len());
|
||||
for obs in &response.observations {
|
||||
println!(" - {}", obs.text);
|
||||
if let Some(mentioned_at) = &obs.mentioned_at {
|
||||
println!(" Mentioned at: {}", mentioned_at);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
} else {
|
||||
output::print_output(&response, output_format)?;
|
||||
@@ -118,8 +107,8 @@ pub fn regenerate(
|
||||
|
||||
let response = client.regenerate_entity(bank_id, entity_id, verbose)?;
|
||||
|
||||
if let Some(sp) = spinner {
|
||||
sp.finish_and_clear();
|
||||
if let Some(mut sp) = spinner {
|
||||
sp.finish();
|
||||
}
|
||||
|
||||
if output_format == OutputFormat::Pretty {
|
||||
|
||||
@@ -16,8 +16,15 @@ use ratatui::{
|
||||
Frame, Terminal,
|
||||
};
|
||||
use std::io;
|
||||
use std::sync::mpsc::{self, Receiver, TryRecvError};
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
// Brand gradient colors: #0074d9 -> #009296
|
||||
const BRAND_START: Color = Color::Rgb(0, 116, 217); // #0074d9
|
||||
const BRAND_END: Color = Color::Rgb(0, 146, 150); // #009296
|
||||
const BRAND_MID: Color = Color::Rgb(0, 131, 183); // Midpoint
|
||||
|
||||
/// Main view types (like k9s contexts)
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
enum View {
|
||||
@@ -61,6 +68,12 @@ enum InputMode {
|
||||
Query,
|
||||
}
|
||||
|
||||
/// Query result from background thread
|
||||
enum QueryResult {
|
||||
Recall(Result<Vec<RecallResult>, String>),
|
||||
Reflect(Result<String, String>),
|
||||
}
|
||||
|
||||
/// Application state
|
||||
struct App {
|
||||
client: ApiClient,
|
||||
@@ -114,6 +127,9 @@ struct App {
|
||||
auto_refresh_enabled: bool,
|
||||
last_refresh: Instant,
|
||||
refresh_interval: Duration,
|
||||
|
||||
// Background query receiver
|
||||
query_receiver: Option<Receiver<QueryResult>>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
@@ -160,6 +176,8 @@ impl App {
|
||||
auto_refresh_enabled: true,
|
||||
last_refresh: Instant::now(),
|
||||
refresh_interval: Duration::from_secs(5),
|
||||
|
||||
query_receiver: None,
|
||||
};
|
||||
|
||||
// Select first item by default
|
||||
@@ -288,58 +306,106 @@ impl App {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn execute_query(&mut self) -> Result<()> {
|
||||
fn execute_query(&mut self) {
|
||||
if let View::Query(bank_id) = &self.view {
|
||||
if self.query_text.is_empty() {
|
||||
self.error_message = "Query cannot be empty".to_string();
|
||||
return Ok(());
|
||||
return;
|
||||
}
|
||||
|
||||
self.loading = true;
|
||||
self.error_message.clear();
|
||||
self.input_mode = InputMode::Normal;
|
||||
|
||||
match self.query_mode {
|
||||
QueryMode::Recall => {
|
||||
let request = RecallRequest {
|
||||
query: self.query_text.clone(),
|
||||
types: None,
|
||||
budget: Some(self.query_budget.clone()),
|
||||
max_tokens: self.query_max_tokens,
|
||||
trace: false,
|
||||
query_timestamp: None,
|
||||
include: None,
|
||||
};
|
||||
// Create channel for receiving results
|
||||
let (tx, rx) = mpsc::channel();
|
||||
self.query_receiver = Some(rx);
|
||||
|
||||
let response = self.client.recall(bank_id, &request, false)?;
|
||||
self.query_results = response.results;
|
||||
// Clone data for the thread
|
||||
let client = self.client.clone();
|
||||
let bank_id = bank_id.clone();
|
||||
let query_mode = self.query_mode.clone();
|
||||
let query_text = self.query_text.clone();
|
||||
let query_budget = self.query_budget.clone();
|
||||
let query_max_tokens = self.query_max_tokens;
|
||||
|
||||
// Spawn background thread
|
||||
thread::spawn(move || {
|
||||
match query_mode {
|
||||
QueryMode::Recall => {
|
||||
let request = RecallRequest {
|
||||
query: query_text,
|
||||
types: None,
|
||||
budget: Some(query_budget),
|
||||
max_tokens: query_max_tokens,
|
||||
trace: false,
|
||||
query_timestamp: None,
|
||||
include: None,
|
||||
};
|
||||
|
||||
let result = client.recall(&bank_id, &request, false)
|
||||
.map(|r| r.results)
|
||||
.map_err(|e| e.to_string());
|
||||
|
||||
let _ = tx.send(QueryResult::Recall(result));
|
||||
}
|
||||
QueryMode::Reflect => {
|
||||
let request = ReflectRequest {
|
||||
query: query_text,
|
||||
budget: Some(query_budget),
|
||||
context: None,
|
||||
include: None,
|
||||
};
|
||||
|
||||
let result = client.reflect(&bank_id, &request, false)
|
||||
.map(|r| r.text)
|
||||
.map_err(|e| e.to_string());
|
||||
|
||||
let _ = tx.send(QueryResult::Reflect(result));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn check_query_result(&mut self) {
|
||||
if let Some(receiver) = &self.query_receiver {
|
||||
match receiver.try_recv() {
|
||||
Ok(QueryResult::Recall(Ok(results))) => {
|
||||
self.query_results = results;
|
||||
if !self.query_results.is_empty() {
|
||||
self.query_results_state.select(Some(0));
|
||||
}
|
||||
|
||||
self.loading = false;
|
||||
self.status_message = format!("Found {} results", self.query_results.len());
|
||||
self.query_receiver = None;
|
||||
}
|
||||
QueryMode::Reflect => {
|
||||
let request = ReflectRequest {
|
||||
query: self.query_text.clone(),
|
||||
budget: Some(self.query_budget.clone()),
|
||||
context: None,
|
||||
include: None,
|
||||
};
|
||||
|
||||
let response = self.client.reflect(bank_id, &request, false)?;
|
||||
self.query_response = response.text;
|
||||
|
||||
Ok(QueryResult::Recall(Err(e))) => {
|
||||
self.error_message = format!("Recall failed: {}", e);
|
||||
self.loading = false;
|
||||
self.query_receiver = None;
|
||||
}
|
||||
Ok(QueryResult::Reflect(Ok(text))) => {
|
||||
self.query_response = text;
|
||||
self.loading = false;
|
||||
self.status_message = "Reflection complete".to_string();
|
||||
self.query_receiver = None;
|
||||
}
|
||||
Ok(QueryResult::Reflect(Err(e))) => {
|
||||
self.error_message = format!("Reflect failed: {}", e);
|
||||
self.loading = false;
|
||||
self.query_receiver = None;
|
||||
}
|
||||
Err(TryRecvError::Empty) => {
|
||||
// Still waiting for result
|
||||
}
|
||||
Err(TryRecvError::Disconnected) => {
|
||||
self.error_message = "Query thread disconnected".to_string();
|
||||
self.loading = false;
|
||||
self.query_receiver = None;
|
||||
}
|
||||
}
|
||||
|
||||
self.input_mode = InputMode::Normal;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn toggle_query_mode(&mut self) {
|
||||
@@ -700,64 +766,64 @@ fn render_control_bar(f: &mut Frame, app: &App, area: Rect) {
|
||||
// Build contextual shortcuts based on view and input mode
|
||||
let shortcuts = match (&app.view, &app.input_mode) {
|
||||
(View::Banks, InputMode::Normal) => vec![
|
||||
("Enter", "Select", Color::Cyan),
|
||||
("R", "Refresh", Color::Yellow),
|
||||
("?", "Help", Color::Magenta),
|
||||
("Enter", "Select", BRAND_START),
|
||||
("R", "Refresh", BRAND_MID),
|
||||
("?", "Help", BRAND_END),
|
||||
("q", "Quit", Color::Red),
|
||||
],
|
||||
(View::Memories(_), InputMode::Normal) => vec![
|
||||
("Enter", "View", Color::Cyan),
|
||||
("/", "Query", Color::Green),
|
||||
("←→", "Scroll", Color::Cyan),
|
||||
("n", "Next", Color::Green),
|
||||
("p", "Prev", Color::Green),
|
||||
("Esc", "Back", Color::Yellow),
|
||||
("R", "Refresh", Color::Yellow),
|
||||
("?", "Help", Color::Magenta),
|
||||
("Enter", "View", BRAND_START),
|
||||
("/", "Query", BRAND_MID),
|
||||
("←→", "Scroll", BRAND_START),
|
||||
("n", "Next", BRAND_MID),
|
||||
("p", "Prev", BRAND_MID),
|
||||
("Esc", "Back", BRAND_END),
|
||||
("R", "Refresh", BRAND_END),
|
||||
("?", "Help", BRAND_END),
|
||||
("q", "Quit", Color::Red),
|
||||
],
|
||||
(View::Entities(_), InputMode::Normal) => vec![
|
||||
("Enter", "View", Color::Cyan),
|
||||
("/", "Query", Color::Green),
|
||||
("←→", "Scroll", Color::Cyan),
|
||||
("Esc", "Back", Color::Yellow),
|
||||
("R", "Refresh", Color::Yellow),
|
||||
("?", "Help", Color::Magenta),
|
||||
("Enter", "View", BRAND_START),
|
||||
("/", "Query", BRAND_MID),
|
||||
("←→", "Scroll", BRAND_START),
|
||||
("Esc", "Back", BRAND_END),
|
||||
("R", "Refresh", BRAND_END),
|
||||
("?", "Help", BRAND_END),
|
||||
("q", "Quit", Color::Red),
|
||||
],
|
||||
(View::Documents(_), InputMode::Normal) => vec![
|
||||
("Enter", "View", Color::Cyan),
|
||||
("/", "Query", Color::Green),
|
||||
("←→", "Scroll", Color::Cyan),
|
||||
("Enter", "View", BRAND_START),
|
||||
("/", "Query", BRAND_MID),
|
||||
("←→", "Scroll", BRAND_START),
|
||||
("Del", "Delete", Color::Red),
|
||||
("Esc", "Back", Color::Yellow),
|
||||
("R", "Refresh", Color::Yellow),
|
||||
("?", "Help", Color::Magenta),
|
||||
("Esc", "Back", BRAND_END),
|
||||
("R", "Refresh", BRAND_END),
|
||||
("?", "Help", BRAND_END),
|
||||
("q", "Quit", Color::Red),
|
||||
],
|
||||
(View::Query(_), InputMode::Normal) => {
|
||||
let mut shortcuts = vec![
|
||||
("/", "Query", Color::Green),
|
||||
("m", "Mode", Color::Cyan),
|
||||
("/", "Query", BRAND_MID),
|
||||
("m", "Mode", BRAND_START),
|
||||
];
|
||||
if app.query_mode == QueryMode::Recall {
|
||||
shortcuts.push(("←→", "Scroll", Color::Cyan));
|
||||
shortcuts.push(("←→", "Scroll", BRAND_START));
|
||||
}
|
||||
shortcuts.extend_from_slice(&[
|
||||
("b", "Budget", Color::Yellow),
|
||||
("+/-", "Tokens", Color::Yellow),
|
||||
("Esc", "Back", Color::Yellow),
|
||||
("?", "Help", Color::Magenta),
|
||||
("b", "Budget", BRAND_END),
|
||||
("+/-", "Tokens", BRAND_END),
|
||||
("Esc", "Back", BRAND_END),
|
||||
("?", "Help", BRAND_END),
|
||||
("q", "Quit", Color::Red),
|
||||
]);
|
||||
shortcuts
|
||||
},
|
||||
(View::Query(_), InputMode::Query) => vec![
|
||||
("Enter", "Execute", Color::Green),
|
||||
("Enter", "Execute", BRAND_MID),
|
||||
("Esc", "Cancel", Color::Red),
|
||||
],
|
||||
_ => vec![
|
||||
("?", "Help", Color::Magenta),
|
||||
("?", "Help", BRAND_END),
|
||||
("q", "Quit", Color::Red),
|
||||
],
|
||||
};
|
||||
@@ -789,9 +855,9 @@ fn render_control_bar(f: &mut Frame, app: &App, area: Rect) {
|
||||
let context_widget = Paragraph::new(context_info)
|
||||
.block(Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Cyan))
|
||||
.border_style(Style::default().fg(BRAND_START))
|
||||
.title(" Context "))
|
||||
.style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD))
|
||||
.style(Style::default().fg(BRAND_END).add_modifier(Modifier::BOLD))
|
||||
.alignment(Alignment::Left);
|
||||
f.render_widget(context_widget, columns[0]);
|
||||
|
||||
@@ -827,7 +893,7 @@ fn render_control_bar(f: &mut Frame, app: &App, area: Rect) {
|
||||
let shortcuts_widget = Paragraph::new(shortcut_lines)
|
||||
.block(Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Cyan))
|
||||
.border_style(Style::default().fg(BRAND_START))
|
||||
.title(" Shortcuts "))
|
||||
.alignment(Alignment::Left);
|
||||
|
||||
@@ -844,7 +910,7 @@ fn render_header(f: &mut Frame, app: &App, area: Rect) {
|
||||
let title = format!("Hindsight Explorer - {}{}", app.view.title(), bank_info);
|
||||
|
||||
let header = Paragraph::new(title)
|
||||
.style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD))
|
||||
.style(Style::default().fg(BRAND_START).add_modifier(Modifier::BOLD))
|
||||
.alignment(Alignment::Center)
|
||||
.block(Block::default().borders(Borders::ALL));
|
||||
|
||||
@@ -859,11 +925,11 @@ fn render_footer(f: &mut Frame, app: &App, area: Rect) {
|
||||
Span::raw(&app.error_message),
|
||||
])
|
||||
} else if app.loading {
|
||||
Line::from(Span::styled(" Loading...", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)))
|
||||
Line::from(Span::styled(" Loading...", Style::default().fg(BRAND_END).add_modifier(Modifier::BOLD)))
|
||||
} else if !app.status_message.is_empty() {
|
||||
Line::from(vec![
|
||||
Span::raw(" "),
|
||||
Span::styled(&app.status_message, Style::default().fg(Color::Green)),
|
||||
Span::styled(&app.status_message, Style::default().fg(BRAND_MID)),
|
||||
])
|
||||
} else {
|
||||
Line::from("")
|
||||
@@ -928,7 +994,7 @@ fn render_memories(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
|
||||
let metadata = Paragraph::new(metadata_text)
|
||||
.block(Block::default().borders(Borders::ALL).title("Memory Metadata"))
|
||||
.style(Style::default().fg(Color::Cyan));
|
||||
.style(Style::default().fg(BRAND_START));
|
||||
|
||||
f.render_widget(metadata, chunks[0]);
|
||||
|
||||
@@ -946,7 +1012,7 @@ fn render_memories(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
let mut items = vec![
|
||||
// Header row
|
||||
ListItem::new(format!("{:<10} {:<18} {:<18} {}", "TYPE", "MENTIONED AT", "OCCURRED AT", "TEXT"))
|
||||
.style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD))
|
||||
.style(Style::default().fg(BRAND_START).add_modifier(Modifier::BOLD))
|
||||
];
|
||||
|
||||
// Data rows
|
||||
@@ -1002,7 +1068,7 @@ fn render_entities(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
|
||||
let metadata = Paragraph::new(metadata_text)
|
||||
.block(Block::default().borders(Borders::ALL).title("Entity Details (Esc to close)"))
|
||||
.style(Style::default().fg(Color::Cyan))
|
||||
.style(Style::default().fg(BRAND_START))
|
||||
.wrap(Wrap { trim: false });
|
||||
|
||||
f.render_widget(metadata, area);
|
||||
@@ -1011,7 +1077,7 @@ fn render_entities(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
let mut items = vec![
|
||||
// Header row
|
||||
ListItem::new(format!("{:<40} {:<15} {:<10}", "NAME", "TYPE", "MENTIONS"))
|
||||
.style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD))
|
||||
.style(Style::default().fg(BRAND_START).add_modifier(Modifier::BOLD))
|
||||
];
|
||||
|
||||
// Data rows
|
||||
@@ -1071,7 +1137,7 @@ fn render_documents(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
|
||||
let metadata = Paragraph::new(metadata_text)
|
||||
.block(Block::default().borders(Borders::ALL).title("Document Metadata"))
|
||||
.style(Style::default().fg(Color::Cyan));
|
||||
.style(Style::default().fg(BRAND_START));
|
||||
|
||||
f.render_widget(metadata, chunks[0]);
|
||||
|
||||
@@ -1091,7 +1157,7 @@ fn render_documents(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
let mut items = vec![
|
||||
// Header row
|
||||
ListItem::new(format!("{:<40} {:<20} {}", "ID", "TYPE", "CREATED"))
|
||||
.style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD))
|
||||
.style(Style::default().fg(BRAND_START).add_modifier(Modifier::BOLD))
|
||||
];
|
||||
|
||||
// Data rows
|
||||
@@ -1137,7 +1203,7 @@ fn render_query(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
|
||||
// Query input
|
||||
let query_style = if app.input_mode == InputMode::Query {
|
||||
Style::default().fg(Color::Yellow)
|
||||
Style::default().fg(BRAND_END)
|
||||
} else {
|
||||
Style::default()
|
||||
};
|
||||
@@ -1154,6 +1220,38 @@ fn render_query(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
|
||||
f.render_widget(query, chunks[0]);
|
||||
|
||||
// Show loading indicator if loading
|
||||
if app.loading {
|
||||
let loading_text = match app.query_mode {
|
||||
QueryMode::Recall => "Searching memories...",
|
||||
QueryMode::Reflect => "Reflecting on memories...",
|
||||
};
|
||||
|
||||
// Create animated dots based on time
|
||||
let dots = ".".repeat(((std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() / 500) % 4) as usize);
|
||||
|
||||
let loading_lines = vec![
|
||||
Line::from(""),
|
||||
Line::from(""),
|
||||
Line::from(vec![
|
||||
Span::styled(" ", Style::default()),
|
||||
Span::styled(format!("{}{}", loading_text, dots), Style::default().fg(BRAND_MID).add_modifier(Modifier::BOLD)),
|
||||
]),
|
||||
Line::from(""),
|
||||
Line::from(Span::styled(" Please wait while we process your query...", Style::default().fg(Color::DarkGray))),
|
||||
];
|
||||
|
||||
let loading_widget = Paragraph::new(loading_lines)
|
||||
.block(Block::default().borders(Borders::ALL).title(format!("{} in progress", mode_label)))
|
||||
.alignment(Alignment::Left);
|
||||
|
||||
f.render_widget(loading_widget, chunks[1]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Results or Response based on mode
|
||||
match app.query_mode {
|
||||
QueryMode::Recall => {
|
||||
@@ -1180,7 +1278,7 @@ fn render_query(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
|
||||
let metadata = Paragraph::new(metadata_text)
|
||||
.block(Block::default().borders(Borders::ALL).title("Recall Result Metadata"))
|
||||
.style(Style::default().fg(Color::Cyan));
|
||||
.style(Style::default().fg(BRAND_START));
|
||||
|
||||
f.render_widget(metadata, recall_chunks[0]);
|
||||
|
||||
@@ -1196,7 +1294,7 @@ fn render_query(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
let mut items = vec![
|
||||
// Header row
|
||||
ListItem::new(format!("{:<10} {:<18} {:<18} {}", "TYPE", "OCCURRED START", "OCCURRED END", "TEXT"))
|
||||
.style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD))
|
||||
.style(Style::default().fg(BRAND_START).add_modifier(Modifier::BOLD))
|
||||
];
|
||||
|
||||
// Data rows
|
||||
@@ -1248,17 +1346,17 @@ fn render_query(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
|
||||
fn render_help(f: &mut Frame, area: Rect) {
|
||||
let help_text = vec![
|
||||
Line::from(Span::styled("Hindsight Explorer - Keyboard Shortcuts", Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD))),
|
||||
Line::from(Span::styled("Hindsight Explorer - Keyboard Shortcuts", Style::default().fg(BRAND_START).add_modifier(Modifier::BOLD))),
|
||||
Line::from(""),
|
||||
Line::from(vec![
|
||||
Span::styled("Navigation Flow", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)),
|
||||
Span::styled("Navigation Flow", Style::default().fg(BRAND_END).add_modifier(Modifier::BOLD)),
|
||||
]),
|
||||
Line::from(" 1. Start by selecting a bank (Enter)"),
|
||||
Line::from(" 2. View memories, entities, or documents for that bank"),
|
||||
Line::from(" 3. Press / from any view to query (recall/reflect)"),
|
||||
Line::from(""),
|
||||
Line::from(vec![
|
||||
Span::styled("Basic Navigation", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)),
|
||||
Span::styled("Basic Navigation", Style::default().fg(BRAND_END).add_modifier(Modifier::BOLD)),
|
||||
]),
|
||||
Line::from(" ↑/↓, j/k - Navigate up/down in lists"),
|
||||
Line::from(" ←/→, h/l - Scroll text left/right in tables"),
|
||||
@@ -1266,7 +1364,7 @@ fn render_help(f: &mut Frame, area: Rect) {
|
||||
Line::from(" Esc - Go back / close detail view"),
|
||||
Line::from(""),
|
||||
Line::from(vec![
|
||||
Span::styled("Query View", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)),
|
||||
Span::styled("Query View", Style::default().fg(BRAND_END).add_modifier(Modifier::BOLD)),
|
||||
]),
|
||||
Line::from(" / - Start or edit query (from any non-bank view)"),
|
||||
Line::from(" m - Toggle mode (Recall ↔ Reflect)"),
|
||||
@@ -1275,7 +1373,7 @@ fn render_help(f: &mut Frame, area: Rect) {
|
||||
Line::from(" Enter - Execute query"),
|
||||
Line::from(""),
|
||||
Line::from(vec![
|
||||
Span::styled("General", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)),
|
||||
Span::styled("General", Style::default().fg(BRAND_END).add_modifier(Modifier::BOLD)),
|
||||
]),
|
||||
Line::from(" R - Refresh current view"),
|
||||
Line::from(" ? - Toggle this help screen"),
|
||||
@@ -1399,7 +1497,7 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> Result<()> {
|
||||
match key.code {
|
||||
KeyCode::Enter => {
|
||||
if matches!(app.view, View::Query(_)) {
|
||||
app.execute_query()?;
|
||||
app.execute_query();
|
||||
}
|
||||
}
|
||||
KeyCode::Esc => {
|
||||
@@ -1422,6 +1520,9 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
// Check for query results from background thread
|
||||
app.check_query_result();
|
||||
|
||||
// Auto-refresh check
|
||||
app.do_auto_refresh()?;
|
||||
}
|
||||
|
||||
@@ -63,8 +63,8 @@ pub fn recall(
|
||||
|
||||
let response = client.recall(agent_id, &request, verbose);
|
||||
|
||||
if let Some(sp) = spinner {
|
||||
sp.finish_and_clear();
|
||||
if let Some(mut sp) = spinner {
|
||||
sp.finish();
|
||||
}
|
||||
|
||||
match response {
|
||||
@@ -104,8 +104,8 @@ pub fn reflect(
|
||||
|
||||
let response = client.reflect(agent_id, &request, verbose);
|
||||
|
||||
if let Some(sp) = spinner {
|
||||
sp.finish_and_clear();
|
||||
if let Some(mut sp) = spinner {
|
||||
sp.finish();
|
||||
}
|
||||
|
||||
match response {
|
||||
@@ -154,8 +154,8 @@ pub fn retain(
|
||||
|
||||
let response = client.retain(agent_id, &request, r#async, verbose);
|
||||
|
||||
if let Some(sp) = spinner {
|
||||
sp.finish_and_clear();
|
||||
if let Some(mut sp) = spinner {
|
||||
sp.finish();
|
||||
}
|
||||
|
||||
match response {
|
||||
@@ -274,8 +274,8 @@ pub fn retain_files(
|
||||
|
||||
let response = client.retain(agent_id, &request, r#async, verbose);
|
||||
|
||||
if let Some(sp) = spinner {
|
||||
sp.finish_and_clear();
|
||||
if let Some(mut sp) = spinner {
|
||||
sp.finish();
|
||||
}
|
||||
|
||||
match response {
|
||||
@@ -312,8 +312,8 @@ pub fn delete(
|
||||
|
||||
let response = client.delete_memory(agent_id, unit_id, verbose);
|
||||
|
||||
if let Some(sp) = spinner {
|
||||
sp.finish_and_clear();
|
||||
if let Some(mut sp) = spinner {
|
||||
sp.finish();
|
||||
}
|
||||
|
||||
match response {
|
||||
@@ -345,12 +345,12 @@ pub fn clear(
|
||||
if !yes && output_format == OutputFormat::Pretty {
|
||||
let message = if let Some(ft) = &fact_type {
|
||||
format!(
|
||||
"Are you sure you want to clear all '{}' memories for agent '{}'? This cannot be undone.",
|
||||
"Are you sure you want to clear all '{}' memories for bank '{}'? This cannot be undone.",
|
||||
ft, agent_id
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"Are you sure you want to clear ALL memories for agent '{}'? This cannot be undone.",
|
||||
"Are you sure you want to clear ALL memories for bank '{}'? This cannot be undone.",
|
||||
agent_id
|
||||
)
|
||||
};
|
||||
@@ -377,8 +377,8 @@ pub fn clear(
|
||||
|
||||
let response = client.clear_memories(agent_id, fact_type.as_deref(), verbose);
|
||||
|
||||
if let Some(sp) = spinner {
|
||||
sp.finish_and_clear();
|
||||
if let Some(mut sp) = spinner {
|
||||
sp.finish();
|
||||
}
|
||||
|
||||
match response {
|
||||
|
||||
@@ -17,8 +17,8 @@ pub fn list(
|
||||
|
||||
let response = client.list_operations(agent_id, verbose);
|
||||
|
||||
if let Some(sp) = spinner {
|
||||
sp.finish_and_clear();
|
||||
if let Some(mut sp) = spinner {
|
||||
sp.finish();
|
||||
}
|
||||
|
||||
match response {
|
||||
@@ -62,8 +62,8 @@ pub fn cancel(
|
||||
|
||||
let response = client.cancel_operation(agent_id, operation_id, verbose);
|
||||
|
||||
if let Some(sp) = spinner {
|
||||
sp.finish_and_clear();
|
||||
if let Some(mut sp) = spinner {
|
||||
sp.finish();
|
||||
}
|
||||
|
||||
match response {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
[38;2;9;127;184m▄[0m[48;2;8;130;178m[38;2;5;133;186m▄[0m [48;2;10;143;160m[38;2;10;143;165m▄[0m[38;2;7;140;156m▄[0m
|
||||
[38;2;8;125;192m▄[0m [38;2;3;132;191m▀[0m[38;2;2;133;192m▄[0m [38;2;3;132;180m▄[0m[38;2;1;137;184m▄[0m[38;2;3;133;174m▄[0m [38;2;3;142;176m▄[0m[38;2;4;142;169m▀[0m [38;2;10;144;164m▄[0m
|
||||
[38;2;6;121;195m▀[0m[38;2;5;128;203m▀[0m[48;2;5;124;195m[38;2;3;125;200m▄[0m[38;2;2;126;196m▄[0m[48;2;3;128;188m[38;2;1;131;196m▄[0m[48;2;0;152;219m[38;2;2;131;191m▄[0m[38;2;1;141;196m▀[0m[38;2;1;135;183m▀[0m[38;2;1;148;198m▀[0m[48;2;1;156;202m[38;2;2;135;180m▄[0m[48;2;4;134;169m[38;2;1;137;177m▄[0m[38;2;3;138;173m▄[0m[48;2;6;137;165m[38;2;2;140;170m▄[0m[38;2;7;144;169m▀[0m[38;2;7;139;158m▀[0m
|
||||
[48;2;2;128;202m[38;2;2;124;201m▄[0m[48;2;1;130;201m[38;2;0;135;212m▄[0m[38;2;2;128;196m▄[0m [48;2;2;142;204m[38;2;7;138;199m▄[0m [38;2;1;135;186m▄[0m[48;2;1;142;186m[38;2;2;144;194m▄[0m[48;2;3;138;176m[38;2;2;134;176m▄[0m
|
||||
[48;2;8;118;200m[38;2;8;121;209m▄[0m[38;2;3;121;203m▀[0m [38;2;3;122;192m▀[0m[38;2;1;138;216m▀[0m[48;2;0;138;210m[38;2;3;128;198m▄[0m[48;2;0;126;188m[38;2;2;131;198m▄[0m[48;2;0;142;205m[38;2;3;132;193m▄[0m[38;2;1;140;196m▀[0m [38;2;4;134;175m▀[0m[48;2;13;135;167m[38;2;8;136;174m▄[0m
|
||||
@@ -34,6 +34,7 @@ impl From<Format> for OutputFormat {
|
||||
#[command(name = "hindsight")]
|
||||
#[command(about = "Hindsight CLI - Semantic memory system", long_about = None)]
|
||||
#[command(version)]
|
||||
#[command(before_help = get_before_help())]
|
||||
#[command(after_help = get_after_help())]
|
||||
struct Cli {
|
||||
/// Output format (pretty, json, yaml)
|
||||
@@ -60,6 +61,10 @@ fn get_after_help() -> String {
|
||||
)
|
||||
}
|
||||
|
||||
fn get_before_help() -> &'static str {
|
||||
ui::get_logo()
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Manage banks (list, profile, stats)
|
||||
@@ -100,8 +105,8 @@ enum BankCommands {
|
||||
/// List all banks
|
||||
List,
|
||||
|
||||
/// Get bank profile (disposition + background)
|
||||
Profile {
|
||||
/// Get bank disposition and background
|
||||
Disposition {
|
||||
/// Bank ID
|
||||
bank_id: String,
|
||||
},
|
||||
@@ -133,6 +138,16 @@ enum BankCommands {
|
||||
#[arg(long)]
|
||||
no_update_disposition: bool,
|
||||
},
|
||||
|
||||
/// Delete a bank and all its data
|
||||
Delete {
|
||||
/// Bank ID
|
||||
bank_id: String,
|
||||
|
||||
/// Skip confirmation prompt
|
||||
#[arg(short = 'y', long)]
|
||||
yes: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
@@ -378,12 +393,15 @@ fn run() -> Result<()> {
|
||||
Commands::Explore => commands::explore::run(&client),
|
||||
Commands::Bank(bank_cmd) => match bank_cmd {
|
||||
BankCommands::List => commands::bank::list(&client, verbose, output_format),
|
||||
BankCommands::Profile { bank_id } => commands::bank::profile(&client, &bank_id, verbose, output_format),
|
||||
BankCommands::Disposition { bank_id } => commands::bank::disposition(&client, &bank_id, verbose, output_format),
|
||||
BankCommands::Stats { bank_id } => commands::bank::stats(&client, &bank_id, verbose, output_format),
|
||||
BankCommands::Name { bank_id, name } => commands::bank::update_name(&client, &bank_id, &name, verbose, output_format),
|
||||
BankCommands::Background { bank_id, content, no_update_disposition } => {
|
||||
commands::bank::update_background(&client, &bank_id, &content, no_update_disposition, verbose, output_format)
|
||||
}
|
||||
BankCommands::Delete { bank_id, yes } => {
|
||||
commands::bank::delete(&client, &bank_id, yes, verbose, output_format)
|
||||
}
|
||||
},
|
||||
|
||||
Commands::Memory(memory_cmd) => match memory_cmd {
|
||||
|
||||
+189
-81
@@ -4,80 +4,132 @@ use hindsight_client::types::ChunkData;
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use std::io::{self, Write};
|
||||
|
||||
/// The logo as ANSI-colored text, generated by test-logo.py
|
||||
const LOGO: &str = include_str!("logo.ansi");
|
||||
|
||||
// Gradient colors: #0074d9 -> #009296
|
||||
const GRADIENT_START: (u8, u8, u8) = (0, 116, 217); // #0074d9
|
||||
const GRADIENT_END: (u8, u8, u8) = (0, 146, 150); // #009296
|
||||
|
||||
/// Interpolate between two RGB colors
|
||||
fn interpolate_color(start: (u8, u8, u8), end: (u8, u8, u8), t: f32) -> (u8, u8, u8) {
|
||||
(
|
||||
(start.0 as f32 + (end.0 as f32 - start.0 as f32) * t) as u8,
|
||||
(start.1 as f32 + (end.1 as f32 - start.1 as f32) * t) as u8,
|
||||
(start.2 as f32 + (end.2 as f32 - start.2 as f32) * t) as u8,
|
||||
)
|
||||
}
|
||||
|
||||
/// Color text using gradient position (0.0 = start, 1.0 = end)
|
||||
pub fn gradient(text: &str, t: f32) -> String {
|
||||
let (r, g, b) = interpolate_color(GRADIENT_START, GRADIENT_END, t);
|
||||
format!("\x1b[38;2;{};{};{}m{}\x1b[0m", r, g, b, text)
|
||||
}
|
||||
|
||||
/// Color text with gradient start color (#0074d9)
|
||||
pub fn gradient_start(text: &str) -> String {
|
||||
gradient(text, 0.0)
|
||||
}
|
||||
|
||||
/// Color text with gradient end color (#009296)
|
||||
pub fn gradient_end(text: &str) -> String {
|
||||
gradient(text, 1.0)
|
||||
}
|
||||
|
||||
/// Color text with gradient middle color
|
||||
pub fn gradient_mid(text: &str) -> String {
|
||||
gradient(text, 0.5)
|
||||
}
|
||||
|
||||
/// Apply gradient across entire text string
|
||||
pub fn gradient_text(text: &str) -> String {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let len = chars.len();
|
||||
if len == 0 {
|
||||
return String::new();
|
||||
}
|
||||
let mut result = String::new();
|
||||
for (i, ch) in chars.iter().enumerate() {
|
||||
if *ch == ' ' {
|
||||
result.push(' ');
|
||||
} else {
|
||||
let t = i as f32 / (len - 1).max(1) as f32;
|
||||
let (r, g, b) = interpolate_color(GRADIENT_START, GRADIENT_END, t);
|
||||
result.push_str(&format!("\x1b[38;2;{};{};{}m{}", r, g, b, ch));
|
||||
}
|
||||
}
|
||||
result.push_str("\x1b[0m");
|
||||
result
|
||||
}
|
||||
|
||||
/// Dim/gray text
|
||||
pub fn dim(text: &str) -> String {
|
||||
format!("\x1b[38;2;128;128;128m{}\x1b[0m", text)
|
||||
}
|
||||
|
||||
pub fn get_logo() -> &'static str {
|
||||
LOGO
|
||||
}
|
||||
|
||||
pub fn print_section_header(title: &str) {
|
||||
println!();
|
||||
println!("{}", format!("━━━ {} ━━━", title).bright_yellow().bold());
|
||||
println!("{}", gradient_text(&format!("━━━ {} ━━━", title)));
|
||||
println!();
|
||||
}
|
||||
|
||||
pub fn print_fact(fact: &RecallResult, show_activation: bool) {
|
||||
pub fn print_fact(fact: &RecallResult, _show_activation: bool) {
|
||||
let fact_type = fact.type_.as_deref().unwrap_or("unknown");
|
||||
|
||||
let type_color = match fact_type {
|
||||
"world" => "cyan",
|
||||
"agent" => "magenta",
|
||||
"opinion" => "yellow",
|
||||
_ => "white",
|
||||
// Use gradient positions for different fact types
|
||||
let type_t = match fact_type {
|
||||
"world" => 0.0,
|
||||
"agent" => 0.5,
|
||||
"opinion" => 1.0,
|
||||
_ => 0.5,
|
||||
};
|
||||
|
||||
let prefix = match fact_type {
|
||||
"world" => "🌍",
|
||||
"agent" => "🤖",
|
||||
"opinion" => "💭",
|
||||
_ => "📝",
|
||||
};
|
||||
|
||||
print!("{} ", prefix);
|
||||
print!("{}", format!("[{}]", fact_type.to_uppercase()).color(type_color).bold());
|
||||
|
||||
// Note: activation field not available in generated SearchResult
|
||||
// The API doesn't return it in the current schema
|
||||
if show_activation {
|
||||
// Placeholder for when activation is added to the API schema
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", gradient(&format!("[{}]", fact_type.to_uppercase()), type_t));
|
||||
println!(" {}", fact.text);
|
||||
|
||||
// Show context if available
|
||||
if let Some(context) = &fact.context {
|
||||
println!(" {}: {}", "Context".bright_black(), context.bright_black());
|
||||
println!(" {} {}", dim("context:"), dim(context));
|
||||
}
|
||||
|
||||
// Show temporal information
|
||||
if let Some(occurred_start) = &fact.occurred_start {
|
||||
if let Some(occurred_end) = &fact.occurred_end {
|
||||
println!(" {}: {} - {}", "Date".bright_black(), occurred_start.bright_black(), occurred_end.bright_black());
|
||||
println!(" {} {} - {}", dim("date:"), dim(occurred_start), dim(occurred_end));
|
||||
} else {
|
||||
println!(" {}: {}", "Date".bright_black(), occurred_start.bright_black());
|
||||
println!(" {} {}", dim("date:"), dim(occurred_start));
|
||||
}
|
||||
}
|
||||
|
||||
// Show document ID if available
|
||||
if let Some(document_id) = &fact.document_id {
|
||||
println!(" {}: {}", "Document".bright_black(), document_id.bright_black());
|
||||
println!(" {} {}", dim("document:"), dim(document_id));
|
||||
}
|
||||
|
||||
println!();
|
||||
}
|
||||
|
||||
pub fn print_chunk(chunk: &ChunkData) {
|
||||
println!(" {}", "─── Source Chunk ───".bright_blue());
|
||||
println!(" {}", gradient_mid("─── Source Chunk ───"));
|
||||
|
||||
// Split text into lines and indent each line
|
||||
for line in chunk.text.lines() {
|
||||
println!(" {}", line.bright_white());
|
||||
println!(" {}", line);
|
||||
}
|
||||
|
||||
if chunk.truncated {
|
||||
println!(" {}", "[Truncated due to token limit]".bright_yellow());
|
||||
println!(" {}", gradient_end("[Truncated due to token limit]"));
|
||||
}
|
||||
|
||||
println!(" {}: {} | {}: {}",
|
||||
"Chunk ID".bright_black(),
|
||||
chunk.id.bright_black(),
|
||||
"Index".bright_black(),
|
||||
chunk.chunk_index.to_string().bright_black()
|
||||
println!(" {} {} | {} {}",
|
||||
dim("Chunk ID:"),
|
||||
dim(&chunk.id),
|
||||
dim("Index:"),
|
||||
dim(&chunk.chunk_index.to_string())
|
||||
);
|
||||
|
||||
println!();
|
||||
@@ -88,10 +140,10 @@ pub fn print_search_results(response: &RecallResponse, show_trace: bool, show_ch
|
||||
print_section_header(&format!("Search Results ({})", results.len()));
|
||||
|
||||
if results.is_empty() {
|
||||
println!("{}", " No results found.".bright_black());
|
||||
println!(" {}", dim("No results found."));
|
||||
} else {
|
||||
for (i, fact) in results.iter().enumerate() {
|
||||
println!("{}", format!(" Result #{}", i + 1).bright_black());
|
||||
println!(" {}", dim(&format!("Result #{}", i + 1)));
|
||||
print_fact(fact, true);
|
||||
|
||||
// Show chunk if available and requested
|
||||
@@ -115,56 +167,120 @@ pub fn print_search_results(response: &RecallResponse, show_trace: bool, show_ch
|
||||
}
|
||||
|
||||
pub fn print_think_response(response: &ReflectResponse) {
|
||||
println!();
|
||||
println!("{}", response.text.bright_white());
|
||||
print_section_header("Reflection");
|
||||
|
||||
println!("{}", response.text);
|
||||
println!();
|
||||
|
||||
if !response.based_on.is_empty() {
|
||||
println!("{}", format!("Based on {} memory units", response.based_on.len()).bright_black());
|
||||
println!("{}", dim(&format!("Based on {} memory units", response.based_on.len())));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn print_trace_info(trace: &serde_json::Map<String, serde_json::Value>) {
|
||||
print_section_header("Trace Information");
|
||||
print_section_header("Trace");
|
||||
|
||||
if let Some(time) = trace.get("total_time").and_then(|v| v.as_f64()) {
|
||||
println!(" ⏱️ Total time: {}", format!("{:.2}ms", time).bright_green());
|
||||
println!(" {} {}", dim("total time:"), gradient_start(&format!("{:.2}ms", time)));
|
||||
}
|
||||
|
||||
if let Some(count) = trace.get("activation_count").and_then(|v| v.as_i64()) {
|
||||
println!(" 📊 Activation count: {}", count.to_string().bright_green());
|
||||
println!(" {} {}", dim("activation count:"), gradient_end(&count.to_string()));
|
||||
}
|
||||
|
||||
println!();
|
||||
}
|
||||
|
||||
pub fn print_success(message: &str) {
|
||||
println!("{} {}", "✓".bright_green().bold(), message.bright_white());
|
||||
println!("{}", gradient_start(message));
|
||||
}
|
||||
|
||||
pub fn print_error(message: &str) {
|
||||
eprintln!("{} {}", "✗".bright_red().bold(), message.bright_red());
|
||||
eprintln!("{} {}", "error:".bright_red().bold(), message.bright_red());
|
||||
}
|
||||
|
||||
pub fn print_warning(message: &str) {
|
||||
println!("{} {}", "⚠".bright_yellow().bold(), message.bright_yellow());
|
||||
println!("{} {}", gradient_end("warning:"), message);
|
||||
}
|
||||
|
||||
pub fn print_info(message: &str) {
|
||||
println!("{} {}", "ℹ".bright_blue().bold(), message.bright_white());
|
||||
println!("{}", gradient_start(message));
|
||||
}
|
||||
|
||||
pub fn create_spinner(message: &str) -> ProgressBar {
|
||||
let pb = ProgressBar::new_spinner();
|
||||
pb.set_style(
|
||||
ProgressStyle::default_spinner()
|
||||
.template("{spinner:.cyan} {msg}")
|
||||
.unwrap()
|
||||
.tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]),
|
||||
);
|
||||
pb.set_message(message.to_string());
|
||||
pb.enable_steady_tick(std::time::Duration::from_millis(80));
|
||||
pb
|
||||
/// Animated gradient spinner that shows text with moving gradient colors
|
||||
pub struct GradientSpinner {
|
||||
message: String,
|
||||
running: std::sync::Arc<std::sync::atomic::AtomicBool>,
|
||||
handle: Option<std::thread::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl GradientSpinner {
|
||||
pub fn new(message: &str) -> Self {
|
||||
let message = message.to_string();
|
||||
let running = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true));
|
||||
|
||||
let msg_clone = message.clone();
|
||||
let running_clone = running.clone();
|
||||
|
||||
let handle = std::thread::spawn(move || {
|
||||
let chars: Vec<char> = msg_clone.chars().collect();
|
||||
let len = chars.len();
|
||||
let num_frames = 30;
|
||||
let mut current_frame = 0usize;
|
||||
|
||||
while running_clone.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
current_frame = (current_frame + 1) % num_frames;
|
||||
let offset = current_frame as f32 / num_frames as f32;
|
||||
|
||||
// Build the gradient string
|
||||
let mut result = String::from("\r");
|
||||
for (i, ch) in chars.iter().enumerate() {
|
||||
if *ch == ' ' {
|
||||
result.push(' ');
|
||||
} else {
|
||||
let base_t = if len > 1 { i as f32 / (len - 1) as f32 } else { 0.0 };
|
||||
let t = (base_t + offset) % 1.0;
|
||||
let (r, g, b) = interpolate_color(GRADIENT_START, GRADIENT_END, t);
|
||||
result.push_str(&format!("\x1b[38;2;{};{};{}m{}", r, g, b, ch));
|
||||
}
|
||||
}
|
||||
result.push_str("\x1b[0m");
|
||||
|
||||
print!("{}", result);
|
||||
let _ = io::stdout().flush();
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(80));
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
message,
|
||||
running,
|
||||
handle: Some(handle),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finish(&mut self) {
|
||||
self.running.store(false, std::sync::atomic::Ordering::Relaxed);
|
||||
if let Some(handle) = self.handle.take() {
|
||||
let _ = handle.join();
|
||||
}
|
||||
// Clear the line
|
||||
print!("\r{}\r", " ".repeat(self.message.len() + 10));
|
||||
let _ = io::stdout().flush();
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for GradientSpinner {
|
||||
fn drop(&mut self) {
|
||||
if self.running.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
self.finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_spinner(message: &str) -> GradientSpinner {
|
||||
GradientSpinner::new(message)
|
||||
}
|
||||
|
||||
pub fn create_progress_bar(total: u64, message: &str) -> ProgressBar {
|
||||
@@ -180,7 +296,7 @@ pub fn create_progress_bar(total: u64, message: &str) -> ProgressBar {
|
||||
}
|
||||
|
||||
pub fn prompt_confirmation(message: &str) -> io::Result<bool> {
|
||||
print!("{} {} [y/N]: ", "?".bright_blue().bold(), message);
|
||||
print!("{} [y/N]: ", gradient_start(message));
|
||||
io::stdout().flush()?;
|
||||
|
||||
let mut input = String::new();
|
||||
@@ -189,16 +305,16 @@ pub fn prompt_confirmation(message: &str) -> io::Result<bool> {
|
||||
Ok(input.trim().eq_ignore_ascii_case("y") || input.trim().eq_ignore_ascii_case("yes"))
|
||||
}
|
||||
|
||||
pub fn print_profile(profile: &BankProfileResponse) {
|
||||
print_section_header(&format!("Bank Profile: {}", profile.bank_id));
|
||||
pub fn print_disposition(profile: &BankProfileResponse) {
|
||||
print_section_header(&format!("Disposition: {}", profile.bank_id));
|
||||
|
||||
// Print name
|
||||
println!("{} {}", "Name:".bright_cyan().bold(), profile.name.bright_white());
|
||||
println!("{} {}", dim("Name:"), gradient_start(&profile.name));
|
||||
println!();
|
||||
|
||||
// Print background if available
|
||||
if !profile.background.is_empty() {
|
||||
println!("{}", "Background:".bright_yellow());
|
||||
println!("{}", gradient_mid("Background:"));
|
||||
for line in profile.background.lines() {
|
||||
println!("{}", line);
|
||||
}
|
||||
@@ -206,38 +322,30 @@ pub fn print_profile(profile: &BankProfileResponse) {
|
||||
}
|
||||
|
||||
// Print disposition traits
|
||||
println!("{}", "─── Disposition Traits ───".bright_yellow());
|
||||
println!("{}", gradient_text("─── Disposition Traits ───"));
|
||||
println!();
|
||||
|
||||
// New 3-trait disposition system (values 1-5)
|
||||
let traits: [(_, i64, _, _, _); 3] = [
|
||||
("Skepticism", profile.disposition.skepticism.get() as i64, "🔍", "cyan", "1=trusting, 5=skeptical"),
|
||||
("Literalism", profile.disposition.literalism.get() as i64, "📋", "yellow", "1=flexible, 5=literal"),
|
||||
("Empathy", profile.disposition.empathy.get() as i64, "💚", "green", "1=detached, 5=empathetic"),
|
||||
let traits: [(_, i64, f32, _); 3] = [
|
||||
("Skepticism", profile.disposition.skepticism.get() as i64, 0.0, "1=trusting, 5=skeptical"),
|
||||
("Literalism", profile.disposition.literalism.get() as i64, 0.5, "1=flexible, 5=literal"),
|
||||
("Empathy", profile.disposition.empathy.get() as i64, 1.0, "1=detached, 5=empathetic"),
|
||||
];
|
||||
|
||||
for (name, value, emoji, color, desc) in &traits {
|
||||
for (name, value, t, desc) in &traits {
|
||||
// Scale 1-5 to bar visualization (each point = 8 chars, total 40)
|
||||
let bar_length = 40;
|
||||
let filled = ((*value - 1) * 10) as usize; // 1->0, 2->10, 3->20, 4->30, 5->40
|
||||
let empty = bar_length - filled;
|
||||
|
||||
let bar = format!("{}{}", "█".repeat(filled), "░".repeat(empty));
|
||||
let colored_bar = match *color {
|
||||
"green" => bar.bright_green(),
|
||||
"yellow" => bar.bright_yellow(),
|
||||
"cyan" => bar.bright_cyan(),
|
||||
"magenta" => bar.bright_magenta(),
|
||||
_ => bar.bright_white(),
|
||||
};
|
||||
|
||||
println!(" {} {:<12} [{}] {}/5",
|
||||
emoji,
|
||||
println!(" {:<12} [{}] {}/5",
|
||||
name,
|
||||
colored_bar,
|
||||
gradient(&bar, *t),
|
||||
value
|
||||
);
|
||||
println!(" {}", desc.bright_black());
|
||||
println!(" {}", dim(desc));
|
||||
}
|
||||
|
||||
println!();
|
||||
|
||||
@@ -15,8 +15,8 @@ Example:
|
||||
print(result.success)
|
||||
|
||||
# Search memories
|
||||
results = client.recall(bank_id="alice", query="What does Alice like?")
|
||||
for r in results:
|
||||
response = client.recall(bank_id="alice", query="What does Alice like?")
|
||||
for r in response.results:
|
||||
print(r.text)
|
||||
|
||||
# Generate contextual answer
|
||||
@@ -29,14 +29,59 @@ from .hindsight_client import Hindsight
|
||||
|
||||
# Re-export response types for convenient access
|
||||
from hindsight_client_api.models.retain_response import RetainResponse
|
||||
from hindsight_client_api.models.recall_response import RecallResponse
|
||||
from hindsight_client_api.models.recall_result import RecallResult
|
||||
from hindsight_client_api.models.recall_response import RecallResponse as _RecallResponse
|
||||
from hindsight_client_api.models.recall_result import RecallResult as _RecallResult
|
||||
from hindsight_client_api.models.reflect_response import ReflectResponse
|
||||
from hindsight_client_api.models.reflect_fact import ReflectFact
|
||||
from hindsight_client_api.models.list_memory_units_response import ListMemoryUnitsResponse
|
||||
from hindsight_client_api.models.bank_profile_response import BankProfileResponse
|
||||
from hindsight_client_api.models.disposition_traits import DispositionTraits
|
||||
|
||||
|
||||
# Add cleaner __repr__ and __iter__ for REPL usability
|
||||
def _recall_result_repr(self):
|
||||
text_preview = self.text[:80] + "..." if len(self.text) > 80 else self.text
|
||||
return f"RecallResult(id='{self.id[:8]}...', type='{self.type}', text='{text_preview}')"
|
||||
|
||||
|
||||
def _recall_response_repr(self):
|
||||
count = len(self.results) if self.results else 0
|
||||
extras = []
|
||||
if self.trace:
|
||||
extras.append("trace=True")
|
||||
if self.entities:
|
||||
extras.append(f"entities={len(self.entities)}")
|
||||
if self.chunks:
|
||||
extras.append(f"chunks={len(self.chunks)}")
|
||||
extras_str = ", " + ", ".join(extras) if extras else ""
|
||||
return f"RecallResponse({count} results{extras_str})"
|
||||
|
||||
|
||||
def _recall_response_iter(self):
|
||||
"""Iterate directly over results for convenience."""
|
||||
return iter(self.results or [])
|
||||
|
||||
|
||||
def _recall_response_len(self):
|
||||
"""Return number of results."""
|
||||
return len(self.results) if self.results else 0
|
||||
|
||||
|
||||
def _recall_response_getitem(self, index):
|
||||
"""Access results by index."""
|
||||
return self.results[index]
|
||||
|
||||
|
||||
_RecallResult.__repr__ = _recall_result_repr
|
||||
_RecallResponse.__repr__ = _recall_response_repr
|
||||
_RecallResponse.__iter__ = _recall_response_iter
|
||||
_RecallResponse.__len__ = _recall_response_len
|
||||
_RecallResponse.__getitem__ = _recall_response_getitem
|
||||
|
||||
# Re-export with patched repr
|
||||
RecallResult = _RecallResult
|
||||
RecallResponse = _RecallResponse
|
||||
|
||||
__all__ = [
|
||||
"Hindsight",
|
||||
# Response types
|
||||
|
||||
@@ -50,7 +50,9 @@ class Hindsight:
|
||||
client.retain(bank_id="alice", content="Alice loves AI")
|
||||
|
||||
# Recall memories
|
||||
results = client.recall(bank_id="alice", query="What does Alice like?")
|
||||
response = client.recall(bank_id="alice", query="What does Alice like?")
|
||||
for r in response.results:
|
||||
print(r.text)
|
||||
|
||||
# Generate contextual answer
|
||||
answer = client.reflect(bank_id="alice", query="What are my interests?")
|
||||
@@ -125,8 +127,8 @@ class Hindsight:
|
||||
|
||||
Args:
|
||||
bank_id: The memory bank ID
|
||||
items: List of memory items with 'content' and optional 'timestamp', 'context', 'metadata'
|
||||
document_id: Optional document ID for grouping memories
|
||||
items: List of memory items with 'content' and optional 'timestamp', 'context', 'metadata', 'document_id'
|
||||
document_id: Optional document ID for grouping memories (applied to items that don't have their own)
|
||||
retain_async: If True, process asynchronously in background (default: False)
|
||||
|
||||
Returns:
|
||||
@@ -138,13 +140,14 @@ class Hindsight:
|
||||
timestamp=item.get("timestamp"),
|
||||
context=item.get("context"),
|
||||
metadata=item.get("metadata"),
|
||||
# Use item's document_id if provided, otherwise fall back to batch-level document_id
|
||||
document_id=item.get("document_id") or document_id,
|
||||
)
|
||||
for item in items
|
||||
]
|
||||
|
||||
request_obj = retain_request.RetainRequest(
|
||||
items=memory_items,
|
||||
document_id=document_id,
|
||||
async_=retain_async,
|
||||
)
|
||||
|
||||
@@ -157,7 +160,13 @@ class Hindsight:
|
||||
types: Optional[List[str]] = None,
|
||||
max_tokens: int = 4096,
|
||||
budget: str = "mid",
|
||||
) -> List[RecallResult]:
|
||||
trace: bool = False,
|
||||
query_timestamp: Optional[str] = None,
|
||||
include_entities: bool = False,
|
||||
max_entity_tokens: int = 500,
|
||||
include_chunks: bool = False,
|
||||
max_chunk_tokens: int = 8192,
|
||||
) -> RecallResponse:
|
||||
"""
|
||||
Recall memories using semantic similarity.
|
||||
|
||||
@@ -167,20 +176,34 @@ class Hindsight:
|
||||
types: Optional list of fact types to filter (world, experience, opinion, observation)
|
||||
max_tokens: Maximum tokens in results (default: 4096)
|
||||
budget: Budget level for recall - "low", "mid", or "high" (default: "mid")
|
||||
trace: Enable trace output (default: False)
|
||||
query_timestamp: Optional ISO format date string (e.g., '2023-05-30T23:40:00')
|
||||
include_entities: Include entity observations in results (default: False)
|
||||
max_entity_tokens: Maximum tokens for entity observations (default: 500)
|
||||
include_chunks: Include raw text chunks in results (default: False)
|
||||
max_chunk_tokens: Maximum tokens for chunks (default: 8192)
|
||||
|
||||
Returns:
|
||||
List of RecallResult objects
|
||||
RecallResponse with results, optional entities, optional chunks, and optional trace
|
||||
"""
|
||||
from hindsight_client_api.models import include_options, entity_include_options, chunk_include_options
|
||||
|
||||
include_opts = include_options.IncludeOptions(
|
||||
entities=entity_include_options.EntityIncludeOptions(max_tokens=max_entity_tokens) if include_entities else None,
|
||||
chunks=chunk_include_options.ChunkIncludeOptions(max_tokens=max_chunk_tokens) if include_chunks else None,
|
||||
)
|
||||
|
||||
request_obj = recall_request.RecallRequest(
|
||||
query=query,
|
||||
types=types,
|
||||
budget=budget,
|
||||
max_tokens=max_tokens,
|
||||
trace=False,
|
||||
trace=trace,
|
||||
query_timestamp=query_timestamp,
|
||||
include=include_opts,
|
||||
)
|
||||
|
||||
response = _run_async(self._api.recall_memories(bank_id, request_obj))
|
||||
return response.results if hasattr(response, 'results') else []
|
||||
return _run_async(self._api.recall_memories(bank_id, request_obj))
|
||||
|
||||
def reflect(
|
||||
self,
|
||||
@@ -209,55 +232,6 @@ class Hindsight:
|
||||
|
||||
return _run_async(self._api.reflect(bank_id, request_obj))
|
||||
|
||||
# Full-featured methods (expose more options)
|
||||
|
||||
def recall_memories(
|
||||
self,
|
||||
bank_id: str,
|
||||
query: str,
|
||||
types: Optional[List[str]] = None,
|
||||
budget: str = "mid",
|
||||
max_tokens: int = 4096,
|
||||
trace: bool = False,
|
||||
query_timestamp: Optional[str] = None,
|
||||
include_entities: bool = True,
|
||||
max_entity_tokens: int = 500,
|
||||
) -> RecallResponse:
|
||||
"""
|
||||
Recall memories with all options (full-featured).
|
||||
|
||||
Args:
|
||||
bank_id: The memory bank ID
|
||||
query: Search query
|
||||
types: Optional list of fact types to filter (world, experience, opinion, observation)
|
||||
budget: Budget level - "low", "mid", or "high"
|
||||
max_tokens: Maximum tokens in results
|
||||
trace: Enable trace output
|
||||
query_timestamp: Optional ISO format date string (e.g., '2023-05-30T23:40:00')
|
||||
include_entities: Include entity observations in results (default: True)
|
||||
max_entity_tokens: Maximum tokens for entity observations (default: 500)
|
||||
|
||||
Returns:
|
||||
RecallResponse with results, optional entities, and optional trace
|
||||
"""
|
||||
from hindsight_client_api.models import include_options, entity_include_options
|
||||
|
||||
include_opts = include_options.IncludeOptions(
|
||||
entities=entity_include_options.EntityIncludeOptions(max_tokens=max_entity_tokens) if include_entities else None
|
||||
)
|
||||
|
||||
request_obj = recall_request.RecallRequest(
|
||||
query=query,
|
||||
types=types,
|
||||
budget=budget,
|
||||
max_tokens=max_tokens,
|
||||
trace=trace,
|
||||
query_timestamp=query_timestamp,
|
||||
include=include_opts,
|
||||
)
|
||||
|
||||
return _run_async(self._api.recall_memories(bank_id, request_obj))
|
||||
|
||||
def list_memories(
|
||||
self,
|
||||
bank_id: str,
|
||||
@@ -311,8 +285,8 @@ class Hindsight:
|
||||
|
||||
Args:
|
||||
bank_id: The memory bank ID
|
||||
items: List of memory items with 'content' and optional 'timestamp', 'context', 'metadata'
|
||||
document_id: Optional document ID for grouping memories
|
||||
items: List of memory items with 'content' and optional 'timestamp', 'context', 'metadata', 'document_id'
|
||||
document_id: Optional document ID for grouping memories (applied to items that don't have their own)
|
||||
retain_async: If True, process asynchronously in background (default: False)
|
||||
|
||||
Returns:
|
||||
@@ -324,13 +298,14 @@ class Hindsight:
|
||||
timestamp=item.get("timestamp"),
|
||||
context=item.get("context"),
|
||||
metadata=item.get("metadata"),
|
||||
# Use item's document_id if provided, otherwise fall back to batch-level document_id
|
||||
document_id=item.get("document_id") or document_id,
|
||||
)
|
||||
for item in items
|
||||
]
|
||||
|
||||
request_obj = retain_request.RetainRequest(
|
||||
items=memory_items,
|
||||
document_id=document_id,
|
||||
async_=retain_async,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "hindsight-client"
|
||||
version = "0.1.0"
|
||||
version = "0.1.4"
|
||||
description = "Python client for Hindsight - Semantic memory system with personality-driven thinking"
|
||||
authors = [
|
||||
{name = "Hindsight Team"}
|
||||
|
||||
@@ -92,32 +92,33 @@ class TestRecall:
|
||||
|
||||
def test_recall_basic(self, client, bank_id):
|
||||
"""Test basic memory search."""
|
||||
results = client.recall(
|
||||
response = client.recall(
|
||||
bank_id=bank_id,
|
||||
query="What does Alice like?",
|
||||
)
|
||||
|
||||
assert results is not None
|
||||
assert len(results) > 0
|
||||
assert response is not None
|
||||
assert response.results is not None
|
||||
assert len(response.results) > 0
|
||||
|
||||
# Check that at least one result contains relevant information
|
||||
result_texts = [r.text for r in results]
|
||||
result_texts = [r.text for r in response.results]
|
||||
assert any("Alice" in text or "Python" in text or "programming" in text for text in result_texts)
|
||||
|
||||
def test_recall_with_max_tokens(self, client, bank_id):
|
||||
"""Test search with token limit."""
|
||||
results = client.recall(
|
||||
response = client.recall(
|
||||
bank_id=bank_id,
|
||||
query="outdoor activities",
|
||||
max_tokens=1024,
|
||||
)
|
||||
|
||||
assert results is not None
|
||||
assert isinstance(results, list)
|
||||
assert response is not None
|
||||
assert response.results is not None
|
||||
|
||||
def test_recall_memories_full_featured(self, client, bank_id):
|
||||
"""Test recall_memories with all features."""
|
||||
response = client.recall_memories(
|
||||
def test_recall_full_featured(self, client, bank_id):
|
||||
"""Test recall with all features."""
|
||||
response = client.recall(
|
||||
bank_id=bank_id,
|
||||
query="What are people's hobbies?",
|
||||
types=["world"],
|
||||
@@ -238,7 +239,7 @@ class TestEndToEndWorkflow:
|
||||
bank_id=workflow_bank_id,
|
||||
query="What programming technologies do I use?",
|
||||
)
|
||||
assert len(search_results) > 0
|
||||
assert len(search_results.results) > 0
|
||||
|
||||
# 4. Generate contextual answer
|
||||
reflect_response = client.reflect(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vectorize-io/hindsight-client",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.4",
|
||||
"description": "TypeScript client for Hindsight - Semantic memory system with personality-driven thinking",
|
||||
"main": "./dist/src/index.js",
|
||||
"types": "./dist/src/index.d.ts",
|
||||
|
||||
@@ -128,7 +128,17 @@ export class HindsightClient {
|
||||
async recall(
|
||||
bankId: string,
|
||||
query: string,
|
||||
options?: { types?: string[]; maxTokens?: number; budget?: Budget; trace?: boolean }
|
||||
options?: {
|
||||
types?: string[];
|
||||
maxTokens?: number;
|
||||
budget?: Budget;
|
||||
trace?: boolean;
|
||||
queryTimestamp?: string;
|
||||
includeEntities?: boolean;
|
||||
maxEntityTokens?: number;
|
||||
includeChunks?: boolean;
|
||||
maxChunkTokens?: number;
|
||||
}
|
||||
): Promise<RecallResponse> {
|
||||
const response = await sdk.recallMemories({
|
||||
client: this.client,
|
||||
@@ -139,6 +149,11 @@ export class HindsightClient {
|
||||
max_tokens: options?.maxTokens,
|
||||
budget: options?.budget || 'mid',
|
||||
trace: options?.trace,
|
||||
query_timestamp: options?.queryTimestamp,
|
||||
include: {
|
||||
entities: options?.includeEntities ? { max_tokens: options?.maxEntityTokens ?? 500 } : undefined,
|
||||
chunks: options?.includeChunks ? { max_tokens: options?.maxChunkTokens ?? 8192 } : undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+83
-107
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "hindsight-control-plane",
|
||||
"version": "0.0.21",
|
||||
"version": "0.1.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "hindsight-control-plane",
|
||||
"version": "0.0.21",
|
||||
"version": "0.1.2",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
@@ -15,8 +15,11 @@
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-radio-group": "^1.3.8",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-slider": "^1.3.6",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@tailwindcss/postcss": "^4.1.17",
|
||||
"@types/cytoscape": "^3.21.9",
|
||||
"@types/node": "^24.10.0",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
@@ -33,19 +36,19 @@
|
||||
"postcss": "^8.5.6",
|
||||
"react": "^19.2.0",
|
||||
"react-chrono": "^2.9.1",
|
||||
"react-cytoscape": "^1.0.6",
|
||||
"react-dom": "^19.2.0",
|
||||
"react18-json-view": "^0.2.9",
|
||||
"recharts": "^3.5.1",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"three": "^0.182.0",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
},
|
||||
"../hindsight-clients/typescript": {
|
||||
"name": "@vectorize-io/hindsight-client",
|
||||
"version": "0.0.21",
|
||||
"version": "0.1.2",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@hey-api/openapi-ts": "^0.88.0",
|
||||
@@ -5045,6 +5048,39 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slider": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz",
|
||||
"integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/number": "1.1.1",
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-collection": "1.1.7",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-direction": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1",
|
||||
"@radix-ui/react-use-previous": "1.1.1",
|
||||
"@radix-ui/react-use-size": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.4",
|
||||
"license": "MIT",
|
||||
@@ -5061,6 +5097,35 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-switch": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz",
|
||||
"integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||
"@radix-ui/react-use-previous": "1.1.1",
|
||||
"@radix-ui/react-use-size": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.1",
|
||||
"license": "MIT",
|
||||
@@ -5324,6 +5389,12 @@
|
||||
"tailwindcss": "4.1.17"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cytoscape": {
|
||||
"version": "3.21.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/cytoscape/-/cytoscape-3.21.9.tgz",
|
||||
"integrity": "sha512-JyrG4tllI6jvuISPjHK9j2Xv/LTbnLekLke5otGStjFluIyA9JjgnvgZrSBsp8cEDpiTjwgZUZwpPv8TSBcoLw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-array": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
|
||||
@@ -6219,31 +6290,13 @@
|
||||
},
|
||||
"node_modules/cytoscape": {
|
||||
"version": "3.33.1",
|
||||
"resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz",
|
||||
"integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/cytoscape-cola": {
|
||||
"version": "2.5.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"webcola": "^3.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"cytoscape": "^3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cytoscape-dagre": {
|
||||
"version": "2.5.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dagre": "^0.8.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"cytoscape": "^3.2.22"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-array": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||
@@ -6265,18 +6318,6 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-dispatch": {
|
||||
"version": "1.0.6",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/d3-drag": {
|
||||
"version": "1.2.5",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"d3-dispatch": "1",
|
||||
"d3-selection": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-ease": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||
@@ -6307,10 +6348,6 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-path": {
|
||||
"version": "1.0.9",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/d3-scale": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
||||
@@ -6327,17 +6364,6 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-selection": {
|
||||
"version": "1.4.2",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/d3-shape": {
|
||||
"version": "1.3.7",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"d3-path": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-time": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
|
||||
@@ -6362,18 +6388,6 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-timer": {
|
||||
"version": "1.0.10",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/dagre": {
|
||||
"version": "0.8.5",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graphlib": "^2.1.8",
|
||||
"lodash": "^4.17.15"
|
||||
}
|
||||
},
|
||||
"node_modules/damerau-levenshtein": {
|
||||
"version": "1.0.8",
|
||||
"license": "BSD-2-Clause"
|
||||
@@ -7215,17 +7229,6 @@
|
||||
"url": "https://github.com/sponsors/rawify"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"license": "MIT",
|
||||
@@ -7389,13 +7392,6 @@
|
||||
"version": "1.4.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/graphlib": {
|
||||
"version": "2.1.8",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.15"
|
||||
}
|
||||
},
|
||||
"node_modules/has-bigints": {
|
||||
"version": "1.1.0",
|
||||
"license": "MIT",
|
||||
@@ -8044,10 +8040,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.merge": {
|
||||
"version": "4.6.2",
|
||||
"license": "MIT"
|
||||
@@ -8550,18 +8542,6 @@
|
||||
"styled-components": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-cytoscape": {
|
||||
"version": "1.0.6",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cytoscape": "^3.2.5",
|
||||
"cytoscape-cola": "^2.0.0",
|
||||
"cytoscape-dagre": "^2.1.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "19.2.0",
|
||||
"license": "MIT",
|
||||
@@ -9315,6 +9295,12 @@
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/three": {
|
||||
"version": "0.182.0",
|
||||
"resolved": "https://registry.npmjs.org/three/-/three-0.182.0.tgz",
|
||||
"integrity": "sha512-GbHabT+Irv+ihI1/f5kIIsZ+Ef9Sl5A1Y7imvS5RQjWgtTPfPnZ43JmlYI7NtCRDK9zir20lQpfg8/9Yd02OvQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tiny-invariant": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
||||
@@ -9713,16 +9699,6 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/webcola": {
|
||||
"version": "3.4.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"d3-dispatch": "^1.0.3",
|
||||
"d3-drag": "^1.0.4",
|
||||
"d3-shape": "^1.3.5",
|
||||
"d3-timer": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"license": "ISC",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hindsight-control-plane",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.4",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
@@ -19,8 +19,11 @@
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-radio-group": "^1.3.8",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-slider": "^1.3.6",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@tailwindcss/postcss": "^4.1.17",
|
||||
"@types/cytoscape": "^3.21.9",
|
||||
"@types/node": "^24.10.0",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
@@ -37,13 +40,13 @@
|
||||
"postcss": "^8.5.6",
|
||||
"react": "^19.2.0",
|
||||
"react-chrono": "^2.9.1",
|
||||
"react-cytoscape": "^1.0.6",
|
||||
"react-dom": "^19.2.0",
|
||||
"react18-json-view": "^0.2.9",
|
||||
"recharts": "^3.5.1",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"three": "^0.182.0",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
@@ -1,3 +1,4 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Space+Grotesk:wght@500;600;700&display=swap');
|
||||
@import "tailwindcss";
|
||||
:root {
|
||||
--background: oklch(0.9911 0 0);
|
||||
@@ -6,8 +7,9 @@
|
||||
--card-foreground: oklch(0.2046 0 0);
|
||||
--popover: oklch(0.9911 0 0);
|
||||
--popover-foreground: oklch(0.4386 0 0);
|
||||
--primary: oklch(0.8348 0.1302 160.9080);
|
||||
--primary-foreground: oklch(0.2626 0.0147 166.4589);
|
||||
--primary: oklch(0.55 0.19 250);
|
||||
--primary-foreground: oklch(0.98 0.01 250);
|
||||
--primary-gradient: linear-gradient(135deg, #0074d9 0%, #009296 100%);
|
||||
--secondary: oklch(0.9940 0 0);
|
||||
--secondary-foreground: oklch(0.2046 0 0);
|
||||
--muted: oklch(0.9461 0 0);
|
||||
@@ -18,23 +20,23 @@
|
||||
--destructive-foreground: oklch(0.9934 0.0032 17.2118);
|
||||
--border: oklch(0.9037 0 0);
|
||||
--input: oklch(0.9731 0 0);
|
||||
--ring: oklch(0.8348 0.1302 160.9080);
|
||||
--chart-1: oklch(0.8348 0.1302 160.9080);
|
||||
--ring: oklch(0.55 0.19 250);
|
||||
--chart-1: oklch(0.55 0.19 250);
|
||||
--chart-2: oklch(0.6231 0.1880 259.8145);
|
||||
--chart-3: oklch(0.6056 0.2189 292.7172);
|
||||
--chart-4: oklch(0.7686 0.1647 70.0804);
|
||||
--chart-5: oklch(0.6959 0.1491 162.4796);
|
||||
--sidebar: oklch(0.9911 0 0);
|
||||
--sidebar-foreground: oklch(0.5452 0 0);
|
||||
--sidebar-primary: oklch(0.8348 0.1302 160.9080);
|
||||
--sidebar-primary-foreground: oklch(0.2626 0.0147 166.4589);
|
||||
--sidebar-primary: oklch(0.55 0.19 250);
|
||||
--sidebar-primary-foreground: oklch(0.98 0.01 250);
|
||||
--sidebar-accent: oklch(0.9461 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.2435 0 0);
|
||||
--sidebar-border: oklch(0.9037 0 0);
|
||||
--sidebar-ring: oklch(0.8348 0.1302 160.9080);
|
||||
--font-sans: 'Avenir Book', 'Avenir', 'Nunito Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
--font-mono: monospace;
|
||||
--sidebar-ring: oklch(0.55 0.19 250);
|
||||
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
--font-heading: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
--font-mono: 'JetBrains Mono', monospace;
|
||||
--radius: 0.5rem;
|
||||
--shadow-x: 0px;
|
||||
--shadow-y: 1px;
|
||||
@@ -61,8 +63,9 @@
|
||||
--card-foreground: oklch(0.9288 0.0126 255.5078);
|
||||
--popover: oklch(0.2603 0 0);
|
||||
--popover-foreground: oklch(0.7348 0 0);
|
||||
--primary: oklch(0.4365 0.1044 156.7556);
|
||||
--primary-foreground: oklch(0.9213 0.0135 167.1556);
|
||||
--primary: oklch(0.60 0.17 250);
|
||||
--primary-foreground: oklch(0.98 0.01 250);
|
||||
--primary-gradient: linear-gradient(135deg, #0074d9 0%, #009296 100%);
|
||||
--secondary: oklch(0.2603 0 0);
|
||||
--secondary-foreground: oklch(0.9851 0 0);
|
||||
--muted: oklch(0.2393 0 0);
|
||||
@@ -73,38 +76,20 @@
|
||||
--destructive-foreground: oklch(0.9368 0.0045 34.3092);
|
||||
--border: oklch(0.2809 0 0);
|
||||
--input: oklch(0.2603 0 0);
|
||||
--ring: oklch(0.8003 0.1821 151.7110);
|
||||
--chart-1: oklch(0.8003 0.1821 151.7110);
|
||||
--ring: oklch(0.60 0.17 250);
|
||||
--chart-1: oklch(0.60 0.17 250);
|
||||
--chart-2: oklch(0.7137 0.1434 254.6240);
|
||||
--chart-3: oklch(0.7090 0.1592 293.5412);
|
||||
--chart-4: oklch(0.8369 0.1644 84.4286);
|
||||
--chart-5: oklch(0.7845 0.1325 181.9120);
|
||||
--sidebar: oklch(0.1822 0 0);
|
||||
--sidebar-foreground: oklch(0.6301 0 0);
|
||||
--sidebar-primary: oklch(0.4365 0.1044 156.7556);
|
||||
--sidebar-primary-foreground: oklch(0.9213 0.0135 167.1556);
|
||||
--sidebar-primary: oklch(0.60 0.17 250);
|
||||
--sidebar-primary-foreground: oklch(0.98 0.01 250);
|
||||
--sidebar-accent: oklch(0.3132 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.9851 0 0);
|
||||
--sidebar-border: oklch(0.2809 0 0);
|
||||
--sidebar-ring: oklch(0.8003 0.1821 151.7110);
|
||||
--font-sans: 'Avenir Book', 'Avenir', 'Nunito Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
--font-mono: monospace;
|
||||
--radius: 0.5rem;
|
||||
--shadow-x: 0px;
|
||||
--shadow-y: 1px;
|
||||
--shadow-blur: 3px;
|
||||
--shadow-spread: 0px;
|
||||
--shadow-opacity: 0.17;
|
||||
--shadow-color: #000000;
|
||||
--shadow-2xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.09);
|
||||
--shadow-xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.09);
|
||||
--shadow-sm: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17);
|
||||
--shadow: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17);
|
||||
--shadow-md: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 2px 4px -1px hsl(0 0% 0% / 0.17);
|
||||
--shadow-lg: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 4px 6px -1px hsl(0 0% 0% / 0.17);
|
||||
--shadow-xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 8px 10px -1px hsl(0 0% 0% / 0.17);
|
||||
--shadow-2xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.43);
|
||||
--sidebar-ring: oklch(0.60 0.17 250);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
@@ -143,7 +128,7 @@
|
||||
|
||||
--font-sans: var(--font-sans);
|
||||
--font-mono: var(--font-mono);
|
||||
--font-serif: var(--font-serif);
|
||||
--font-heading: var(--font-heading);
|
||||
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
@@ -170,4 +155,29 @@
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
letter-spacing: var(--tracking-normal);
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: var(--font-heading);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
code, pre {
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
/* Gradient utilities */
|
||||
.bg-primary-gradient {
|
||||
background: var(--primary-gradient);
|
||||
}
|
||||
|
||||
.border-primary-gradient {
|
||||
border-image: var(--primary-gradient) 1;
|
||||
}
|
||||
|
||||
.text-primary-gradient {
|
||||
background: var(--primary-gradient);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
import type { Metadata } from "next";
|
||||
import "./globals.css";
|
||||
import { BankProvider } from "@/lib/bank-context";
|
||||
import { ThemeProvider } from "@/lib/theme-context";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Hindsight Control Plane",
|
||||
description: "Control plane for the temporal semantic memory system",
|
||||
icons: {
|
||||
icon: "/favicon.png",
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
@@ -13,11 +17,13 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body>
|
||||
<BankProvider>
|
||||
{children}
|
||||
</BankProvider>
|
||||
<ThemeProvider>
|
||||
<BankProvider>
|
||||
{children}
|
||||
</BankProvider>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -27,7 +27,9 @@ import {
|
||||
DialogFooter,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Check, ChevronsUpDown, Plus, FileText } from 'lucide-react';
|
||||
import { Check, ChevronsUpDown, Plus, FileText, Moon, Sun, Github } from 'lucide-react';
|
||||
import { useTheme } from '@/lib/theme-context';
|
||||
import Image from 'next/image';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { cn } from '@/lib/utils';
|
||||
@@ -36,6 +38,7 @@ function BankSelectorInner() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { currentBank, setCurrentBank, banks, loadBanks } = useBank();
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [createDialogOpen, setCreateDialogOpen] = React.useState(false);
|
||||
const [newBankId, setNewBankId] = React.useState('');
|
||||
@@ -119,26 +122,34 @@ function BankSelectorInner() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-card text-card-foreground px-5 py-3 border-b-4 border-primary">
|
||||
<div className="flex items-center gap-2.5 text-sm">
|
||||
<span className="font-medium">Memory Bank:</span>
|
||||
<div className="bg-card text-card-foreground px-5 py-3 border-b-4 border-primary-gradient">
|
||||
<div className="flex items-center gap-4 text-sm">
|
||||
{/* Logo */}
|
||||
<Image src="/logo.png" alt="Hindsight" width={40} height={40} className="h-10 w-auto" unoptimized />
|
||||
|
||||
{/* Separator */}
|
||||
<div className="h-8 w-px bg-border" />
|
||||
|
||||
{/* Memory Bank Selector */}
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className="w-[300px] justify-between font-bold border-2 border-primary hover:bg-accent"
|
||||
className="w-[250px] justify-between font-bold border-2 border-primary hover:bg-accent"
|
||||
>
|
||||
{currentBank || "Select a memory bank..."}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[300px] p-0">
|
||||
<PopoverContent className="w-[250px] p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search memory banks..." />
|
||||
{sortedBanks.length > 0 && (
|
||||
<CommandInput placeholder="Search memory banks..." />
|
||||
)}
|
||||
<CommandList>
|
||||
<CommandEmpty>No memory bank found.</CommandEmpty>
|
||||
<CommandEmpty>No memory banks yet.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{sortedBanks.map((bank) => (
|
||||
<CommandItem
|
||||
@@ -165,34 +176,73 @@ function BankSelectorInner() {
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
{/* Footer: Create new bank */}
|
||||
<div className="border-t border-border p-1">
|
||||
<button
|
||||
className="w-full flex items-center gap-2 px-2 py-2 text-sm rounded-md hover:bg-accent transition-colors text-muted-foreground hover:text-foreground"
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
setCreateDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
<span>Create new bank</span>
|
||||
</button>
|
||||
</div>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-9 border-2 border-primary hover:bg-accent gap-1.5"
|
||||
onClick={() => setCreateDialogOpen(true)}
|
||||
title="Create new memory bank"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
<span>New Bank</span>
|
||||
</Button>
|
||||
{/* Separator */}
|
||||
<div className="h-8 w-px bg-border" />
|
||||
|
||||
{/* Add Document Button */}
|
||||
{currentBank && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-9 border-2 border-secondary hover:bg-secondary/20 gap-1.5"
|
||||
className="h-9 gap-1.5"
|
||||
onClick={() => setDocDialogOpen(true)}
|
||||
title="Add document to current bank"
|
||||
>
|
||||
<FileText className="h-4 w-4" />
|
||||
<span>New Document</span>
|
||||
<Plus className="h-4 w-4" />
|
||||
<span>Add Document</span>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Spacer */}
|
||||
<div className="flex-1" />
|
||||
|
||||
{/* GitHub Link */}
|
||||
<a
|
||||
href="https://github.com/vectorize-io/hindsight"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-accent transition-colors text-muted-foreground hover:text-foreground"
|
||||
title="View on GitHub"
|
||||
>
|
||||
<Github className="h-5 w-5" />
|
||||
<span className="text-sm font-medium">GitHub</span>
|
||||
</a>
|
||||
|
||||
{/* Separator */}
|
||||
<div className="h-8 w-px bg-border" />
|
||||
|
||||
{/* Dark Mode Toggle */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={toggleTheme}
|
||||
className="h-9 w-9"
|
||||
title={theme === 'light' ? 'Switch to dark mode' : 'Switch to light mode'}
|
||||
>
|
||||
{theme === 'light' ? (
|
||||
<Moon className="h-5 w-5" />
|
||||
) : (
|
||||
<Sun className="h-5 w-5" />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
@@ -333,17 +383,32 @@ function BankSelectorInner() {
|
||||
export function BankSelector() {
|
||||
return (
|
||||
<Suspense fallback={
|
||||
<div className="bg-card text-card-foreground px-5 py-3 border-b-4 border-primary">
|
||||
<div className="flex items-center gap-2.5 text-sm">
|
||||
<span className="font-medium">Memory Bank:</span>
|
||||
<div className="bg-card text-card-foreground px-5 py-3 border-b-4 border-primary-gradient">
|
||||
<div className="flex items-center gap-4 text-sm">
|
||||
<Image src="/logo.png" alt="Hindsight" width={40} height={40} className="h-10 w-auto" unoptimized />
|
||||
<div className="h-8 w-px bg-border" />
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-[300px] justify-between font-bold border-2 border-primary"
|
||||
className="w-[250px] justify-between font-bold border-2 border-primary"
|
||||
disabled
|
||||
>
|
||||
Loading...
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
<div className="flex-1" />
|
||||
<a
|
||||
href="https://github.com/vectorize-io/hindsight"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-accent transition-colors text-muted-foreground"
|
||||
>
|
||||
<Github className="h-5 w-5" />
|
||||
<span className="text-sm font-medium">GitHub</span>
|
||||
</a>
|
||||
<div className="h-8 w-px bg-border" />
|
||||
<Button variant="ghost" size="icon" className="h-9 w-9" disabled>
|
||||
<Moon className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}>
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
|
||||
import { client } from '@/lib/api';
|
||||
import { useBank } from '@/lib/bank-context';
|
||||
import cytoscape from 'cytoscape';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Copy, Check, Calendar, ZoomIn, ZoomOut, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';
|
||||
import { Copy, Check, Calendar, ZoomIn, ZoomOut, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Settings2, Eye, EyeOff } from 'lucide-react';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Slider } from '@/components/ui/slider';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { MemoryDetailPanel } from './memory-detail-panel';
|
||||
import { Graph2D, convertHindsightGraphData, GraphNode } from './graph-2d';
|
||||
|
||||
type FactType = 'world' | 'experience' | 'opinion';
|
||||
type ViewMode = 'graph' | 'table' | 'timeline';
|
||||
@@ -23,16 +25,41 @@ export function DataView({ factType }: DataViewProps) {
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('graph');
|
||||
const [data, setData] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [nodeLimit, setNodeLimit] = useState(50);
|
||||
const [layout, setLayout] = useState('circle');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [copiedId, setCopiedId] = useState<string | null>(null);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [selectedGraphNode, setSelectedGraphNode] = useState<any>(null);
|
||||
const [selectedTableMemory, setSelectedTableMemory] = useState<any>(null);
|
||||
const itemsPerPage = 100;
|
||||
const cyRef = useRef<any>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Graph controls state
|
||||
const [showLabels, setShowLabels] = useState(true);
|
||||
const [maxNodes, setMaxNodes] = useState<number | undefined>(50);
|
||||
const [showControlPanel, setShowControlPanel] = useState(true);
|
||||
const [visibleLinkTypes, setVisibleLinkTypes] = useState<Set<string>>(new Set(['semantic', 'temporal', 'entity', 'causal']));
|
||||
|
||||
const toggleLinkType = (type: string) => {
|
||||
setVisibleLinkTypes(prev => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(type)) {
|
||||
next.delete(type);
|
||||
} else {
|
||||
next.add(type);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
// Esc key handler to deselect graph node
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && selectedGraphNode) {
|
||||
setSelectedGraphNode(null);
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [selectedGraphNode]);
|
||||
|
||||
const copyToClipboard = async (text: string) => {
|
||||
try {
|
||||
@@ -68,118 +95,98 @@ export function DataView({ factType }: DataViewProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const renderGraph = () => {
|
||||
if (!data || !containerRef.current || !data.nodes || !data.edges) return;
|
||||
// Filter table rows based on search query (text only)
|
||||
const filteredTableRows = useMemo(() => {
|
||||
if (!data?.table_rows) return [];
|
||||
if (!searchQuery) return data.table_rows;
|
||||
|
||||
if (cyRef.current) {
|
||||
cyRef.current.destroy();
|
||||
}
|
||||
|
||||
const limitedNodes = (data.nodes || []).slice(0, nodeLimit);
|
||||
const nodeIds = new Set(limitedNodes.map((n: any) => n.data.id));
|
||||
const limitedEdges = (data.edges || []).filter((e: any) =>
|
||||
nodeIds.has(e.data.source) && nodeIds.has(e.data.target)
|
||||
const query = searchQuery.toLowerCase();
|
||||
return data.table_rows.filter((row: any) =>
|
||||
row.text?.toLowerCase().includes(query)
|
||||
);
|
||||
}, [data, searchQuery]);
|
||||
|
||||
const layouts: any = {
|
||||
circle: {
|
||||
name: 'circle',
|
||||
animate: false,
|
||||
radius: 300,
|
||||
spacingFactor: 1.5,
|
||||
},
|
||||
grid: {
|
||||
name: 'grid',
|
||||
animate: false,
|
||||
rows: Math.ceil(Math.sqrt(limitedNodes.length)),
|
||||
cols: Math.ceil(Math.sqrt(limitedNodes.length)),
|
||||
spacingFactor: 2,
|
||||
},
|
||||
cose: {
|
||||
name: 'cose',
|
||||
animate: false,
|
||||
nodeRepulsion: 15000,
|
||||
idealEdgeLength: 150,
|
||||
edgeElasticity: 100,
|
||||
nestingFactor: 1.2,
|
||||
gravity: 1,
|
||||
numIter: 1000,
|
||||
initialTemp: 200,
|
||||
coolingFactor: 0.95,
|
||||
minTemp: 1.0,
|
||||
},
|
||||
};
|
||||
// Get filtered node IDs for graph filtering
|
||||
const filteredNodeIds = useMemo(() => {
|
||||
return new Set(filteredTableRows.map((row: any) => row.id));
|
||||
}, [filteredTableRows]);
|
||||
|
||||
cyRef.current = cytoscape({
|
||||
container: containerRef.current,
|
||||
elements: [
|
||||
...limitedNodes.map((n: any) => ({ data: n.data })),
|
||||
...limitedEdges.map((e: any) => ({ data: e.data })),
|
||||
],
|
||||
style: [
|
||||
{
|
||||
selector: 'node',
|
||||
style: {
|
||||
'background-color': 'data(color)' as any,
|
||||
label: 'data(label)' as any,
|
||||
'text-valign': 'center',
|
||||
'text-halign': 'center',
|
||||
'font-size': '10px',
|
||||
'font-weight': 'bold',
|
||||
'text-wrap': 'wrap',
|
||||
'text-max-width': '100px',
|
||||
width: 40,
|
||||
height: 40,
|
||||
'border-width': 2,
|
||||
'border-color': '#333',
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: 'edge',
|
||||
style: {
|
||||
width: 1,
|
||||
'line-color': 'data(color)' as any,
|
||||
'line-style': 'data(lineStyle)' as any,
|
||||
'target-arrow-shape': 'triangle',
|
||||
'target-arrow-color': 'data(color)' as any,
|
||||
'curve-style': 'bezier',
|
||||
opacity: 0.6,
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: 'node:selected',
|
||||
style: {
|
||||
'border-width': 4,
|
||||
'border-color': '#000',
|
||||
},
|
||||
},
|
||||
] as any,
|
||||
layout: layouts[layout] || layouts.circle,
|
||||
});
|
||||
|
||||
// Add click handler for nodes
|
||||
cyRef.current.on('tap', 'node', (evt: any) => {
|
||||
const nodeId = evt.target.id();
|
||||
// Find the corresponding table row data
|
||||
const nodeData = data.table_rows?.find((row: any) => row.id === nodeId);
|
||||
if (nodeData) {
|
||||
setSelectedGraphNode(nodeData);
|
||||
}
|
||||
});
|
||||
|
||||
// Click on background to deselect
|
||||
cyRef.current.on('tap', (evt: any) => {
|
||||
if (evt.target === cyRef.current) {
|
||||
setSelectedGraphNode(null);
|
||||
}
|
||||
});
|
||||
// Helper to get normalized link type
|
||||
const getLinkTypeCategory = (type: string | undefined): string => {
|
||||
if (!type) return 'semantic';
|
||||
if (type === 'semantic' || type === 'temporal' || type === 'entity') return type;
|
||||
if (['causes', 'caused_by', 'enables', 'prevents'].includes(type)) return 'causal';
|
||||
return 'semantic';
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (viewMode === 'graph' && data) {
|
||||
renderGraph();
|
||||
// Convert data for Graph2D with filtering
|
||||
const graph2DData = useMemo(() => {
|
||||
if (!data) return { nodes: [], links: [] };
|
||||
const fullData = convertHindsightGraphData(data);
|
||||
|
||||
let nodes = fullData.nodes;
|
||||
let links = fullData.links;
|
||||
|
||||
// Filter nodes based on search query
|
||||
if (searchQuery) {
|
||||
const filteredNodes = fullData.nodes.filter(node => filteredNodeIds.has(node.id));
|
||||
const filteredNodeIdSet = new Set(filteredNodes.map(n => n.id));
|
||||
nodes = filteredNodes;
|
||||
links = fullData.links.filter(link =>
|
||||
filteredNodeIdSet.has(link.source) && filteredNodeIdSet.has(link.target)
|
||||
);
|
||||
}
|
||||
}, [viewMode, data, nodeLimit, layout]);
|
||||
|
||||
// Filter links based on visible link types
|
||||
links = links.filter(link => {
|
||||
const category = getLinkTypeCategory(link.type);
|
||||
return visibleLinkTypes.has(category);
|
||||
});
|
||||
|
||||
return { nodes, links };
|
||||
}, [data, searchQuery, filteredNodeIds, visibleLinkTypes]);
|
||||
|
||||
// Calculate link stats for display
|
||||
const linkStats = useMemo(() => {
|
||||
let semantic = 0, temporal = 0, entity = 0, causal = 0, total = 0;
|
||||
const otherTypes: Record<string, number> = {};
|
||||
graph2DData.links.forEach(l => {
|
||||
total++;
|
||||
const type = l.type || 'unknown';
|
||||
if (type === 'semantic') semantic++;
|
||||
else if (type === 'temporal') temporal++;
|
||||
else if (type === 'entity') entity++;
|
||||
else if (type === 'causes' || type === 'caused_by' || type === 'enables' || type === 'prevents') causal++;
|
||||
else {
|
||||
otherTypes[type] = (otherTypes[type] || 0) + 1;
|
||||
}
|
||||
});
|
||||
console.log('Graph link stats:', { semantic, temporal, entity, causal, total });
|
||||
if (Object.keys(otherTypes).length > 0) {
|
||||
console.log('Other link types:', otherTypes);
|
||||
}
|
||||
return { semantic, temporal, entity, causal, total, otherTypes };
|
||||
}, [graph2DData]);
|
||||
|
||||
// Handle node click in graph - show in panel
|
||||
const handleGraphNodeClick = useCallback((node: GraphNode) => {
|
||||
const nodeData = data?.table_rows?.find((row: any) => row.id === node.id);
|
||||
if (nodeData) {
|
||||
setSelectedGraphNode(nodeData);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
// Memoized color functions to prevent graph re-initialization
|
||||
// Uses brand colors: primary blue (#0074d9), teal (#009296), amber for entity, purple for causal
|
||||
const nodeColorFn = useCallback((node: GraphNode) => node.color || '#0074d9', []);
|
||||
const linkColorFn = useCallback((link: any) => {
|
||||
if (link.type === 'temporal') return '#009296'; // Brand teal
|
||||
if (link.type === 'entity') return '#f59e0b'; // Amber
|
||||
if (link.type === 'causes' || link.type === 'caused_by' || link.type === 'enables' || link.type === 'prevents') {
|
||||
return '#8b5cf6'; // Purple for causal
|
||||
}
|
||||
return '#0074d9'; // Brand primary blue for semantic
|
||||
}, []);
|
||||
|
||||
// Reset to first page when search query changes
|
||||
useEffect(() => {
|
||||
@@ -204,9 +211,20 @@ export function DataView({ factType }: DataViewProps) {
|
||||
</div>
|
||||
) : data ? (
|
||||
<>
|
||||
{/* Always visible filter */}
|
||||
<div className="mb-4">
|
||||
<Input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Filter memories by text..."
|
||||
className="max-w-md"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{data.total_units} total memories
|
||||
{searchQuery ? `${filteredTableRows.length} of ${data.total_units} memories` : `${data.total_units} total memories`}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 bg-muted rounded-lg p-1">
|
||||
<button
|
||||
@@ -243,134 +261,214 @@ export function DataView({ factType }: DataViewProps) {
|
||||
</div>
|
||||
|
||||
{viewMode === 'graph' && (
|
||||
<div className="flex gap-4">
|
||||
<div className={`relative transition-all ${selectedGraphNode ? 'w-2/3' : 'w-full'}`}>
|
||||
<div className="p-4 bg-card border-b-2 border-primary flex gap-4 items-center flex-wrap">
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="font-semibold text-card-foreground">Limit nodes:</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={nodeLimit}
|
||||
onChange={(e) => setNodeLimit(parseInt(e.target.value))}
|
||||
min="10"
|
||||
max="1000"
|
||||
step="10"
|
||||
className="w-20"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="font-semibold text-card-foreground">Layout:</label>
|
||||
<Select value={layout} onValueChange={setLayout}>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="circle">Circle (fast)</SelectItem>
|
||||
<SelectItem value="grid">Grid (fast)</SelectItem>
|
||||
<SelectItem value="cose">Force-directed (slow)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground ml-auto">
|
||||
Click on a node to view details
|
||||
</div>
|
||||
</div>
|
||||
<div ref={containerRef} className="w-full h-[800px] bg-background" />
|
||||
<div className="absolute top-20 left-5 bg-card p-4 border-2 border-primary rounded-lg shadow-lg max-w-[250px]">
|
||||
<h3 className="font-bold mb-2 border-b-2 border-primary pb-1 text-card-foreground">Legend</h3>
|
||||
<h4 className="font-bold mt-2 mb-1 text-sm text-card-foreground">Link Types:</h4>
|
||||
<div className="flex items-center my-2">
|
||||
<div className="w-8 h-0.5 mr-2.5 bg-cyan-500 border-t border-dashed border-cyan-500" />
|
||||
<span className="text-sm"><strong>Temporal</strong></span>
|
||||
</div>
|
||||
<div className="flex items-center my-2">
|
||||
<div className="w-8 h-0.5 mr-2.5 bg-pink-500" />
|
||||
<span className="text-sm"><strong>Semantic</strong></span>
|
||||
</div>
|
||||
<div className="flex items-center my-2">
|
||||
<div className="w-8 h-0.5 mr-2.5 bg-yellow-500" />
|
||||
<span className="text-sm"><strong>Entity</strong></span>
|
||||
</div>
|
||||
<h4 className="font-bold mt-2 mb-1 text-sm">Nodes:</h4>
|
||||
<div className="flex items-center my-2">
|
||||
<div className="w-5 h-5 mr-2.5 bg-gray-300 border border-gray-500 rounded" />
|
||||
<span className="text-sm">No entities</span>
|
||||
</div>
|
||||
<div className="flex items-center my-2">
|
||||
<div className="w-5 h-5 mr-2.5 bg-blue-300 border border-gray-500 rounded" />
|
||||
<span className="text-sm">1 entity</span>
|
||||
</div>
|
||||
<div className="flex items-center my-2">
|
||||
<div className="w-5 h-5 mr-2.5 bg-blue-500 border border-gray-500 rounded" />
|
||||
<span className="text-sm">2+ entities</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-0">
|
||||
{/* Graph */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<Graph2D
|
||||
data={graph2DData}
|
||||
height={700}
|
||||
showLabels={showLabels}
|
||||
onNodeClick={handleGraphNodeClick}
|
||||
maxNodes={maxNodes}
|
||||
nodeColorFn={nodeColorFn}
|
||||
linkColorFn={linkColorFn}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Memory Detail Panel for Graph View - Fixed on Right */}
|
||||
{selectedGraphNode && (
|
||||
<div className="fixed right-0 top-0 h-screen w-[420px] bg-card border-l-2 border-primary shadow-2xl z-50 overflow-y-auto animate-in slide-in-from-right duration-300 ease-out">
|
||||
<MemoryDetailPanel
|
||||
memory={selectedGraphNode}
|
||||
onClose={() => setSelectedGraphNode(null)}
|
||||
inPanel
|
||||
/>
|
||||
{/* Right Toggle Button */}
|
||||
<button
|
||||
onClick={() => setShowControlPanel(!showControlPanel)}
|
||||
className="flex-shrink-0 w-5 h-[700px] bg-transparent hover:bg-muted/50 flex items-center justify-center transition-colors"
|
||||
title={showControlPanel ? 'Hide panel' : 'Show panel'}
|
||||
>
|
||||
{showControlPanel ? (
|
||||
<ChevronRight className="w-3 h-3 text-muted-foreground/60" />
|
||||
) : (
|
||||
<ChevronLeft className="w-3 h-3 text-muted-foreground/60" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Right Panel - Legend/Controls OR Memory Details */}
|
||||
<div className={`${showControlPanel ? 'w-80' : 'w-0'} transition-all duration-300 overflow-hidden flex-shrink-0`}>
|
||||
<div className="w-80 h-[700px] bg-card border-l border-border overflow-y-auto">
|
||||
{selectedGraphNode ? (
|
||||
/* Memory Detail View */
|
||||
<MemoryDetailPanel
|
||||
memory={selectedGraphNode}
|
||||
onClose={() => setSelectedGraphNode(null)}
|
||||
inPanel
|
||||
/>
|
||||
) : (
|
||||
/* Legend & Controls View */
|
||||
<div className="p-4 space-y-5">
|
||||
{/* Legend & Stats */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold mb-3 text-foreground">Graph</h3>
|
||||
<div className="space-y-2">
|
||||
{/* Nodes */}
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: '#0074d9' }} />
|
||||
<span className="text-foreground">Nodes</span>
|
||||
</div>
|
||||
<span className="font-mono text-foreground">
|
||||
{Math.min(maxNodes ?? graph2DData.nodes.length, graph2DData.nodes.length)}/{graph2DData.nodes.length}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="text-xs font-medium text-muted-foreground mt-2 mb-1">Links ({linkStats.total}) <span className="text-muted-foreground/60">· click to filter</span></div>
|
||||
<button
|
||||
onClick={() => toggleLinkType('semantic')}
|
||||
className={`w-full flex items-center justify-between text-sm px-2 py-1 rounded transition-all ${
|
||||
visibleLinkTypes.has('semantic') ? 'hover:bg-muted' : 'opacity-40 hover:opacity-60'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-0.5 bg-[#0074d9]" />
|
||||
<span className="text-foreground">Semantic</span>
|
||||
</div>
|
||||
<span className={`font-mono ${linkStats.semantic === 0 ? 'text-destructive' : 'text-foreground'}`}>
|
||||
{linkStats.semantic}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => toggleLinkType('temporal')}
|
||||
className={`w-full flex items-center justify-between text-sm px-2 py-1 rounded transition-all ${
|
||||
visibleLinkTypes.has('temporal') ? 'hover:bg-muted' : 'opacity-40 hover:opacity-60'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-0.5 bg-[#009296]" />
|
||||
<span className="text-foreground">Temporal</span>
|
||||
</div>
|
||||
<span className={`font-mono ${linkStats.temporal === 0 ? 'text-destructive' : 'text-foreground'}`}>
|
||||
{linkStats.temporal}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => toggleLinkType('entity')}
|
||||
className={`w-full flex items-center justify-between text-sm px-2 py-1 rounded transition-all ${
|
||||
visibleLinkTypes.has('entity') ? 'hover:bg-muted' : 'opacity-40 hover:opacity-60'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-0.5 bg-[#f59e0b]" />
|
||||
<span className="text-foreground">Entity</span>
|
||||
</div>
|
||||
<span className="font-mono text-foreground">{linkStats.entity}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => toggleLinkType('causal')}
|
||||
className={`w-full flex items-center justify-between text-sm px-2 py-1 rounded transition-all ${
|
||||
visibleLinkTypes.has('causal') ? 'hover:bg-muted' : 'opacity-40 hover:opacity-60'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-0.5 bg-[#8b5cf6]" />
|
||||
<span className="text-foreground">Causal</span>
|
||||
</div>
|
||||
<span className={`font-mono ${linkStats.causal === 0 ? 'text-muted-foreground' : 'text-foreground'}`}>
|
||||
{linkStats.causal}
|
||||
</span>
|
||||
</button>
|
||||
{Object.entries(linkStats.otherTypes || {}).map(([type, count]) => (
|
||||
<div key={type} className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground capitalize ml-6">{type}</span>
|
||||
<span className="font-mono text-muted-foreground">{count as number}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border" />
|
||||
|
||||
{/* Controls Section */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold mb-3 text-foreground">Display</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="show-labels" className="text-sm text-foreground">Show labels</Label>
|
||||
<Switch
|
||||
id="show-labels"
|
||||
checked={showLabels}
|
||||
onCheckedChange={setShowLabels}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border" />
|
||||
|
||||
{/* Limits Section */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold mb-3 text-foreground">Performance</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Label className="text-sm text-foreground">Max nodes</Label>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{maxNodes ?? 'All'} / {graph2DData.nodes.length}
|
||||
</span>
|
||||
</div>
|
||||
<Slider
|
||||
value={[maxNodes ?? graph2DData.nodes.length]}
|
||||
min={10}
|
||||
max={Math.max(graph2DData.nodes.length, 10)}
|
||||
step={10}
|
||||
onValueChange={([v]) => setMaxNodes(v >= graph2DData.nodes.length ? undefined : v)}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
All links between visible nodes are shown.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border" />
|
||||
|
||||
{/* Hint */}
|
||||
<div className="text-xs text-muted-foreground/60 text-center pt-2">
|
||||
Click a node to see details
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{viewMode === 'table' && (
|
||||
<div>
|
||||
<div className="w-full">
|
||||
<div className="px-5 mb-4">
|
||||
<Input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Search memories (text, context, ID)..."
|
||||
className="max-w-2xl"
|
||||
/>
|
||||
</div>
|
||||
<div className="px-5 pb-5">
|
||||
{data.table_rows && data.table_rows.length > 0 ? (
|
||||
<div className="pb-4">
|
||||
{filteredTableRows.length > 0 ? (
|
||||
(() => {
|
||||
const filteredRows = data.table_rows.filter((row: any) => {
|
||||
if (!searchQuery) return true;
|
||||
const query = searchQuery.toLowerCase();
|
||||
return (
|
||||
row.text?.toLowerCase().includes(query) ||
|
||||
row.context?.toLowerCase().includes(query) ||
|
||||
row.id?.toLowerCase().includes(query)
|
||||
);
|
||||
});
|
||||
|
||||
const totalPages = Math.ceil(filteredRows.length / itemsPerPage);
|
||||
const totalPages = Math.ceil(filteredTableRows.length / itemsPerPage);
|
||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||
const endIndex = startIndex + itemsPerPage;
|
||||
const paginatedRows = filteredRows.slice(startIndex, endIndex);
|
||||
const paginatedRows = filteredTableRows.slice(startIndex, endIndex);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<Table>
|
||||
<Table className="table-fixed">
|
||||
<TableHeader>
|
||||
<TableRow className="bg-muted/50">
|
||||
<TableHead className="w-[80px]">ID</TableHead>
|
||||
<TableHead>Text</TableHead>
|
||||
<TableHead className="w-[150px]">Context</TableHead>
|
||||
<TableHead className="w-[100px]">Occurred</TableHead>
|
||||
<TableHead className="w-[100px]">Mentioned</TableHead>
|
||||
<TableHead className="w-[60px]">Actions</TableHead>
|
||||
<TableHead className="w-[45%]">Memory</TableHead>
|
||||
<TableHead className="w-[20%]">Entities</TableHead>
|
||||
<TableHead className="w-[15%]">Occurred</TableHead>
|
||||
<TableHead className="w-[15%]">Mentioned</TableHead>
|
||||
<TableHead className="w-[5%]"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{paginatedRows.map((row: any, idx: number) => {
|
||||
const occurredDisplay = row.occurred_start
|
||||
? new Date(row.occurred_start).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
|
||||
? new Date(row.occurred_start).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
|
||||
: null;
|
||||
const mentionedDisplay = row.mentioned_at
|
||||
? new Date(row.mentioned_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
|
||||
? new Date(row.mentioned_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
|
||||
: null;
|
||||
|
||||
return (
|
||||
@@ -381,46 +479,40 @@ export function DataView({ factType }: DataViewProps) {
|
||||
selectedTableMemory?.id === row.id ? 'bg-primary/10' : ''
|
||||
}`}
|
||||
>
|
||||
<TableCell className="font-mono text-xs text-muted-foreground" title={row.id}>
|
||||
{row.id?.substring(0, 8)}...
|
||||
<TableCell className="py-2">
|
||||
<div className="line-clamp-2 text-sm leading-snug">{row.text}</div>
|
||||
{row.context && (
|
||||
<div className="text-xs text-muted-foreground mt-0.5 truncate">{row.context}</div>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="line-clamp-2 text-sm">{row.text}</div>
|
||||
{row.entities && (
|
||||
<div className="flex gap-1 mt-1 flex-wrap">
|
||||
{row.entities.split(', ').slice(0, 3).map((entity: string, i: number) => (
|
||||
<span key={i} className="text-[10px] px-1.5 py-0.5 rounded bg-secondary text-secondary-foreground">
|
||||
<TableCell className="py-2">
|
||||
{row.entities ? (
|
||||
<div className="flex gap-1 flex-wrap">
|
||||
{row.entities.split(', ').slice(0, 2).map((entity: string, i: number) => (
|
||||
<span
|
||||
key={i}
|
||||
className="text-[10px] px-1.5 py-0.5 rounded-full bg-primary/10 text-primary font-medium"
|
||||
>
|
||||
{entity}
|
||||
</span>
|
||||
))}
|
||||
{row.entities.split(', ').length > 3 && (
|
||||
{row.entities.split(', ').length > 2 && (
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
+{row.entities.split(', ').length - 3}
|
||||
+{row.entities.split(', ').length - 2}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">-</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs text-muted-foreground truncate max-w-[150px]" title={row.context}>
|
||||
{row.context || '-'}
|
||||
<TableCell className="text-xs py-2">
|
||||
{occurredDisplay || <span className="text-muted-foreground">-</span>}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
{occurredDisplay ? (
|
||||
<span className="flex items-center gap-1">
|
||||
<Calendar className="h-3 w-3" />
|
||||
{occurredDisplay}
|
||||
</span>
|
||||
) : '-'}
|
||||
<TableCell className="text-xs py-2">
|
||||
{mentionedDisplay || <span className="text-muted-foreground">-</span>}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
{mentionedDisplay ? (
|
||||
<span className="flex items-center gap-1">
|
||||
<Calendar className="h-3 w-3" />
|
||||
{mentionedDisplay}
|
||||
</span>
|
||||
) : '-'}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TableCell className="py-2">
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
@@ -428,7 +520,7 @@ export function DataView({ factType }: DataViewProps) {
|
||||
}}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-7 w-7 p-0"
|
||||
className="h-6 w-6 p-0"
|
||||
title="Copy ID"
|
||||
>
|
||||
{copiedId === row.id ? (
|
||||
@@ -447,9 +539,9 @@ export function DataView({ factType }: DataViewProps) {
|
||||
|
||||
{/* Pagination Controls */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center justify-between mt-4 pt-4 border-t">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Showing {startIndex + 1} to {Math.min(endIndex, filteredRows.length)} of {filteredRows.length}
|
||||
<div className="flex items-center justify-between mt-3 pt-3 border-t">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{startIndex + 1}-{Math.min(endIndex, filteredTableRows.length)} of {filteredTableRows.length}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
@@ -457,20 +549,20 @@ export function DataView({ factType }: DataViewProps) {
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(1)}
|
||||
disabled={currentPage === 1}
|
||||
className="h-8 w-8 p-0"
|
||||
className="h-7 w-7 p-0"
|
||||
>
|
||||
<ChevronsLeft className="h-4 w-4" />
|
||||
<ChevronsLeft className="h-3 w-3" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
|
||||
disabled={currentPage === 1}
|
||||
className="h-8 w-8 p-0"
|
||||
className="h-7 w-7 p-0"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
<ChevronLeft className="h-3 w-3" />
|
||||
</Button>
|
||||
<span className="text-sm px-3">
|
||||
<span className="text-xs px-2">
|
||||
{currentPage} / {totalPages}
|
||||
</span>
|
||||
<Button
|
||||
@@ -478,18 +570,18 @@ export function DataView({ factType }: DataViewProps) {
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
|
||||
disabled={currentPage === totalPages}
|
||||
className="h-8 w-8 p-0"
|
||||
className="h-7 w-7 p-0"
|
||||
>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
<ChevronRight className="h-3 w-3" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(totalPages)}
|
||||
disabled={currentPage === totalPages}
|
||||
className="h-8 w-8 p-0"
|
||||
className="h-7 w-7 p-0"
|
||||
>
|
||||
<ChevronsRight className="h-4 w-4" />
|
||||
<ChevronsRight className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -499,7 +591,7 @@ export function DataView({ factType }: DataViewProps) {
|
||||
})()
|
||||
) : (
|
||||
<div className="text-center py-12 text-muted-foreground">
|
||||
{data.table_rows ? 'No memories match your search' : 'No memories found'}
|
||||
{data.table_rows?.length > 0 ? 'No memories match your filter' : 'No memories found'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -519,7 +611,7 @@ export function DataView({ factType }: DataViewProps) {
|
||||
)}
|
||||
|
||||
{viewMode === 'timeline' && (
|
||||
<TimelineView data={data} />
|
||||
<TimelineView data={data} filteredRows={filteredTableRows} />
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
@@ -537,17 +629,17 @@ export function DataView({ factType }: DataViewProps) {
|
||||
// Timeline View Component - Custom compact timeline with zoom and navigation
|
||||
type Granularity = 'year' | 'month' | 'week' | 'day';
|
||||
|
||||
function TimelineView({ data }: { data: any }) {
|
||||
function TimelineView({ data, filteredRows }: { data: any; filteredRows: any[] }) {
|
||||
const [selectedItem, setSelectedItem] = useState<any>(null);
|
||||
const [granularity, setGranularity] = useState<Granularity>('month');
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const timelineRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Filter and sort items that have occurred_start dates
|
||||
// Filter and sort items that have occurred_start dates (using filtered data)
|
||||
const { sortedItems, itemsWithoutDates } = useMemo(() => {
|
||||
if (!data?.table_rows) return { sortedItems: [], itemsWithoutDates: [] };
|
||||
if (!filteredRows || filteredRows.length === 0) return { sortedItems: [], itemsWithoutDates: [] };
|
||||
|
||||
const withDates = data.table_rows
|
||||
const withDates = filteredRows
|
||||
.filter((row: any) => row.occurred_start)
|
||||
.sort((a: any, b: any) => {
|
||||
const dateA = new Date(a.occurred_start).getTime();
|
||||
@@ -555,19 +647,10 @@ function TimelineView({ data }: { data: any }) {
|
||||
return dateA - dateB;
|
||||
});
|
||||
|
||||
const withoutDates = data.table_rows.filter((row: any) => !row.occurred_start);
|
||||
|
||||
// Debug logging
|
||||
console.log('Timeline data:', {
|
||||
total: data.table_rows.length,
|
||||
withDates: withDates.length,
|
||||
withoutDates: withoutDates.length,
|
||||
sampleWithDate: withDates[0],
|
||||
sampleWithoutDate: withoutDates[0]
|
||||
});
|
||||
const withoutDates = filteredRows.filter((row: any) => !row.occurred_start);
|
||||
|
||||
return { sortedItems: withDates, itemsWithoutDates: withoutDates };
|
||||
}, [data]);
|
||||
}, [filteredRows]);
|
||||
|
||||
// Group items by granularity
|
||||
const timelineGroups = useMemo(() => {
|
||||
@@ -697,9 +780,9 @@ function TimelineView({ data }: { data: any }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex gap-3 px-4">
|
||||
<div className="px-4">
|
||||
{/* Timeline */}
|
||||
<div className={`transition-all ${selectedItem ? 'w-2/3' : 'w-full'}`}>
|
||||
<div>
|
||||
{/* Controls */}
|
||||
<div className="flex items-center justify-between mb-3 gap-4">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
@@ -853,7 +936,7 @@ function TimelineView({ data }: { data: any }) {
|
||||
{item.entities && (
|
||||
<div className="flex gap-1 mt-1 flex-wrap">
|
||||
{item.entities.split(', ').slice(0, 3).map((entity: string, i: number) => (
|
||||
<span key={i} className="text-[9px] px-1 py-0.5 rounded bg-secondary text-secondary-foreground">
|
||||
<span key={i} className="text-[9px] px-1.5 py-0.5 rounded-full bg-primary/10 text-primary font-medium">
|
||||
{entity}
|
||||
</span>
|
||||
))}
|
||||
|
||||
@@ -0,0 +1,599 @@
|
||||
'use client';
|
||||
|
||||
import { useRef, useEffect, useState, useMemo } from 'react';
|
||||
import cytoscape, { Core, NodeSingular } from 'cytoscape';
|
||||
|
||||
// Hook to detect dark mode
|
||||
function useIsDarkMode() {
|
||||
const [isDark, setIsDark] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkDark = () => {
|
||||
setIsDark(document.documentElement.classList.contains('dark'));
|
||||
};
|
||||
|
||||
checkDark();
|
||||
|
||||
// Watch for theme changes
|
||||
const observer = new MutationObserver(checkDark);
|
||||
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return isDark;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Types & Interfaces
|
||||
// ============================================================================
|
||||
|
||||
export interface GraphNode {
|
||||
id: string;
|
||||
label?: string;
|
||||
color?: string;
|
||||
size?: number;
|
||||
group?: string;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface GraphLink {
|
||||
source: string;
|
||||
target: string;
|
||||
color?: string;
|
||||
width?: number;
|
||||
type?: string;
|
||||
entity?: string;
|
||||
weight?: number;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface GraphData {
|
||||
nodes: GraphNode[];
|
||||
links: GraphLink[];
|
||||
}
|
||||
|
||||
export interface Graph2DProps {
|
||||
data: GraphData;
|
||||
height?: number;
|
||||
showLabels?: boolean;
|
||||
onNodeClick?: (node: GraphNode) => void;
|
||||
onNodeHover?: (node: GraphNode | null) => void;
|
||||
nodeColorFn?: (node: GraphNode) => string;
|
||||
nodeSizeFn?: (node: GraphNode) => number;
|
||||
linkColorFn?: (link: GraphLink) => string;
|
||||
linkWidthFn?: (link: GraphLink) => number;
|
||||
maxNodes?: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Default Values
|
||||
// ============================================================================
|
||||
|
||||
// Brand colors
|
||||
const BRAND_PRIMARY = '#0074d9';
|
||||
const BRAND_TEAL = '#009296';
|
||||
const LINK_SEMANTIC = '#0074d9'; // Primary blue for semantic
|
||||
const LINK_TEMPORAL = '#009296'; // Teal for temporal
|
||||
const LINK_ENTITY = '#f59e0b'; // Amber for entity
|
||||
|
||||
const DEFAULT_NODE_COLOR = BRAND_PRIMARY;
|
||||
const DEFAULT_LINK_COLOR = LINK_SEMANTIC;
|
||||
const DEFAULT_NODE_SIZE = 20;
|
||||
const DEFAULT_LINK_WIDTH = 1;
|
||||
|
||||
// ============================================================================
|
||||
// Component
|
||||
// ============================================================================
|
||||
|
||||
export function Graph2D({
|
||||
data,
|
||||
height = 600,
|
||||
showLabels = true,
|
||||
onNodeClick,
|
||||
onNodeHover,
|
||||
nodeColorFn,
|
||||
nodeSizeFn,
|
||||
linkColorFn,
|
||||
linkWidthFn,
|
||||
maxNodes,
|
||||
}: Graph2DProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const cyRef = useRef<Core | null>(null);
|
||||
const [hoveredNode, setHoveredNode] = useState<GraphNode | null>(null);
|
||||
const [hoveredLink, setHoveredLink] = useState<GraphLink | null>(null);
|
||||
const [linkTooltipPos, setLinkTooltipPos] = useState<{ x: number; y: number } | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
// Use refs to store callbacks and data to prevent re-renders from resetting the graph
|
||||
const onNodeClickRef = useRef(onNodeClick);
|
||||
const onNodeHoverRef = useRef(onNodeHover);
|
||||
const fullDataRef = useRef(data);
|
||||
const nodeColorFnRef = useRef(nodeColorFn);
|
||||
const linkColorFnRef = useRef(linkColorFn);
|
||||
onNodeClickRef.current = onNodeClick;
|
||||
onNodeHoverRef.current = onNodeHover;
|
||||
fullDataRef.current = data;
|
||||
nodeColorFnRef.current = nodeColorFn;
|
||||
linkColorFnRef.current = linkColorFn;
|
||||
|
||||
// Transform and limit data - only limit nodes, show ALL links between visible nodes
|
||||
const graphData = useMemo(() => {
|
||||
let nodes = [...data.nodes];
|
||||
|
||||
// Limit nodes if needed
|
||||
if (maxNodes && nodes.length > maxNodes) {
|
||||
nodes = nodes.slice(0, maxNodes);
|
||||
}
|
||||
|
||||
// Show ALL links between visible nodes (no random link limiting)
|
||||
const nodeIds = new Set(nodes.map(n => n.id));
|
||||
const links = data.links.filter(l => nodeIds.has(l.source) && nodeIds.has(l.target));
|
||||
|
||||
return { nodes, links };
|
||||
}, [data, maxNodes]);
|
||||
|
||||
// Convert to Cytoscape format
|
||||
const cyElements = useMemo(() => {
|
||||
const nodes = graphData.nodes.map(node => ({
|
||||
data: {
|
||||
id: node.id,
|
||||
label: node.label || node.id.substring(0, 8),
|
||||
color: nodeColorFn ? nodeColorFn(node) : (node.color || DEFAULT_NODE_COLOR),
|
||||
size: nodeSizeFn ? nodeSizeFn(node) : (node.size || DEFAULT_NODE_SIZE),
|
||||
originalNode: node,
|
||||
},
|
||||
}));
|
||||
|
||||
const edges = graphData.links.map((link, idx) => ({
|
||||
data: {
|
||||
id: `edge-${idx}`,
|
||||
source: link.source,
|
||||
target: link.target,
|
||||
color: linkColorFn ? linkColorFn(link) : (link.color || DEFAULT_LINK_COLOR),
|
||||
width: linkWidthFn ? linkWidthFn(link) : (link.width || DEFAULT_LINK_WIDTH),
|
||||
type: link.type,
|
||||
entity: link.entity,
|
||||
weight: link.weight,
|
||||
originalLink: link,
|
||||
},
|
||||
}));
|
||||
|
||||
return [...nodes, ...edges];
|
||||
}, [graphData, nodeColorFn, nodeSizeFn, linkColorFn, linkWidthFn]);
|
||||
|
||||
// Initialize Cytoscape
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
// Handle empty data case
|
||||
if (cyElements.length === 0) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
// Theme-aware colors
|
||||
const textColor = isDarkMode ? '#ffffff' : '#1f2937';
|
||||
const textBgColor = isDarkMode ? 'rgba(0,0,0,0.8)' : 'rgba(255,255,255,0.9)';
|
||||
const borderColor = isDarkMode ? '#ffffff' : '#374151';
|
||||
|
||||
const cy = cytoscape({
|
||||
container: containerRef.current,
|
||||
elements: cyElements,
|
||||
style: [
|
||||
{
|
||||
selector: 'node',
|
||||
style: {
|
||||
'background-fill': 'radial-gradient',
|
||||
'background-gradient-stop-colors': ['#0074d9', '#005bb5'],
|
||||
'background-gradient-stop-positions': ['0%', '100%'],
|
||||
'width': 'data(size)',
|
||||
'height': 'data(size)',
|
||||
'label': showLabels ? 'data(label)' : '',
|
||||
'color': textColor,
|
||||
'text-valign': 'bottom',
|
||||
'text-halign': 'center',
|
||||
'font-size': '8px',
|
||||
'font-weight': 500,
|
||||
'text-margin-y': 3,
|
||||
'text-wrap': 'wrap',
|
||||
'text-max-width': '80px',
|
||||
'text-background-color': textBgColor,
|
||||
'text-background-opacity': 0.9,
|
||||
'text-background-padding': '2px',
|
||||
'text-background-shape': 'roundrectangle',
|
||||
'border-width': 0,
|
||||
'z-index': 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: 'node:selected',
|
||||
style: {
|
||||
'border-width': 3,
|
||||
'border-color': '#0074d9',
|
||||
'border-opacity': 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: 'node:active',
|
||||
style: {
|
||||
'overlay-opacity': 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: 'edge',
|
||||
style: {
|
||||
'width': 'data(width)',
|
||||
'line-color': 'data(color)',
|
||||
'target-arrow-color': 'data(color)',
|
||||
'curve-style': 'bezier',
|
||||
'opacity': isDarkMode ? 0.5 : 0.6,
|
||||
'z-index': 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: 'edge:selected',
|
||||
style: {
|
||||
'opacity': 1,
|
||||
'width': 3,
|
||||
},
|
||||
},
|
||||
// Dimmed state for non-selected elements
|
||||
{
|
||||
selector: '.dimmed',
|
||||
style: {
|
||||
'opacity': 0.15,
|
||||
},
|
||||
},
|
||||
// Highlighted state for selected node and neighbors
|
||||
{
|
||||
selector: 'node.highlighted',
|
||||
style: {
|
||||
'opacity': 1,
|
||||
'border-width': 3,
|
||||
'border-color': '#0074d9',
|
||||
'border-opacity': 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: 'edge.highlighted',
|
||||
style: {
|
||||
'opacity': 0.9,
|
||||
'width': 2,
|
||||
},
|
||||
},
|
||||
],
|
||||
layout: {
|
||||
name: 'cose',
|
||||
animate: false,
|
||||
randomize: true,
|
||||
nodeRepulsion: () => 100000,
|
||||
idealEdgeLength: () => 300,
|
||||
edgeElasticity: () => 20,
|
||||
nestingFactor: 0.1,
|
||||
gravity: 0.01,
|
||||
numIter: 2500,
|
||||
coolingFactor: 0.95,
|
||||
minTemp: 1.0,
|
||||
nodeOverlap: 20,
|
||||
nodeDimensionsIncludeLabels: true,
|
||||
padding: 50,
|
||||
} as any,
|
||||
minZoom: 0.1,
|
||||
maxZoom: 5,
|
||||
wheelSensitivity: 0.3,
|
||||
});
|
||||
|
||||
cyRef.current = cy;
|
||||
|
||||
// Event handlers
|
||||
cy.on('tap', 'node', (evt) => {
|
||||
const node = evt.target as NodeSingular;
|
||||
const originalNode = node.data('originalNode') as GraphNode;
|
||||
if (onNodeClickRef.current && originalNode) {
|
||||
onNodeClickRef.current(originalNode);
|
||||
}
|
||||
|
||||
// Find ALL connected nodes from full data (not just visible ones)
|
||||
const fullData = fullDataRef.current;
|
||||
const clickedNodeId = originalNode.id;
|
||||
|
||||
// Find all links connected to this node from full data
|
||||
const connectedLinks = fullData.links.filter(
|
||||
l => l.source === clickedNodeId || l.target === clickedNodeId
|
||||
);
|
||||
|
||||
// Find all connected node IDs
|
||||
const connectedNodeIds = new Set<string>();
|
||||
connectedLinks.forEach(l => {
|
||||
connectedNodeIds.add(l.source);
|
||||
connectedNodeIds.add(l.target);
|
||||
});
|
||||
|
||||
// Add any missing nodes to the graph
|
||||
const existingNodeIds = new Set(cy.nodes().map(n => n.id()));
|
||||
const nodesToAdd: any[] = [];
|
||||
const edgesToAdd: any[] = [];
|
||||
|
||||
connectedNodeIds.forEach(nodeId => {
|
||||
if (!existingNodeIds.has(nodeId)) {
|
||||
const nodeData = fullData.nodes.find(n => n.id === nodeId);
|
||||
if (nodeData) {
|
||||
nodesToAdd.push({
|
||||
group: 'nodes',
|
||||
data: {
|
||||
id: nodeData.id,
|
||||
label: nodeData.label || nodeData.id.substring(0, 8),
|
||||
color: nodeColorFnRef.current ? nodeColorFnRef.current(nodeData) : (nodeData.color || DEFAULT_NODE_COLOR),
|
||||
size: nodeData.size || DEFAULT_NODE_SIZE,
|
||||
originalNode: nodeData,
|
||||
isTemporary: true, // Mark as temporarily added
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add missing edges
|
||||
const existingEdgeIds = new Set(cy.edges().map(e => `${e.data('source')}-${e.data('target')}`));
|
||||
connectedLinks.forEach((link, idx) => {
|
||||
const edgeKey = `${link.source}-${link.target}`;
|
||||
const reverseKey = `${link.target}-${link.source}`;
|
||||
if (!existingEdgeIds.has(edgeKey) && !existingEdgeIds.has(reverseKey)) {
|
||||
edgesToAdd.push({
|
||||
group: 'edges',
|
||||
data: {
|
||||
id: `temp-edge-${idx}-${Date.now()}`,
|
||||
source: link.source,
|
||||
target: link.target,
|
||||
color: linkColorFnRef.current ? linkColorFnRef.current(link) : (link.color || DEFAULT_LINK_COLOR),
|
||||
width: link.width || DEFAULT_LINK_WIDTH,
|
||||
type: link.type,
|
||||
isTemporary: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Add new elements to graph
|
||||
if (nodesToAdd.length > 0 || edgesToAdd.length > 0) {
|
||||
cy.add([...nodesToAdd, ...edgesToAdd]);
|
||||
|
||||
// Position new nodes near the clicked node
|
||||
const clickedPos = node.position();
|
||||
cy.nodes('[?isTemporary]').forEach((n, i) => {
|
||||
const angle = (2 * Math.PI * i) / nodesToAdd.length;
|
||||
const radius = 150;
|
||||
n.position({
|
||||
x: clickedPos.x + radius * Math.cos(angle),
|
||||
y: clickedPos.y + radius * Math.sin(angle),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Get all connected elements (including newly added)
|
||||
const neighborhood = node.neighborhood().add(node);
|
||||
|
||||
// Dim all elements first
|
||||
cy.elements().addClass('dimmed');
|
||||
|
||||
// Highlight the neighborhood
|
||||
neighborhood.removeClass('dimmed');
|
||||
neighborhood.addClass('highlighted');
|
||||
|
||||
// Center on the neighborhood without changing positions
|
||||
cy.animate({
|
||||
fit: { eles: neighborhood, padding: 50 },
|
||||
}, { duration: 400 });
|
||||
});
|
||||
|
||||
// Click on background to reset
|
||||
cy.on('tap', (evt) => {
|
||||
if (evt.target === cy) {
|
||||
// Remove temporary nodes and edges
|
||||
cy.elements('[?isTemporary]').remove();
|
||||
|
||||
cy.elements().removeClass('dimmed highlighted');
|
||||
cy.animate({
|
||||
fit: { eles: cy.elements(), padding: 50 },
|
||||
}, { duration: 400 });
|
||||
}
|
||||
});
|
||||
|
||||
cy.on('mouseover', 'node', (evt) => {
|
||||
const node = evt.target as NodeSingular;
|
||||
const originalNode = node.data('originalNode') as GraphNode;
|
||||
setHoveredNode(originalNode);
|
||||
if (onNodeHoverRef.current && originalNode) {
|
||||
onNodeHoverRef.current(originalNode);
|
||||
}
|
||||
containerRef.current!.style.cursor = 'pointer';
|
||||
});
|
||||
|
||||
cy.on('mouseout', 'node', () => {
|
||||
setHoveredNode(null);
|
||||
if (onNodeHoverRef.current) {
|
||||
onNodeHoverRef.current(null);
|
||||
}
|
||||
containerRef.current!.style.cursor = 'default';
|
||||
});
|
||||
|
||||
// Edge hover handlers
|
||||
cy.on('mouseover', 'edge', (evt) => {
|
||||
const edge = evt.target;
|
||||
const originalLink = edge.data('originalLink') as GraphLink;
|
||||
if (originalLink) {
|
||||
setHoveredLink(originalLink);
|
||||
// Get position for tooltip
|
||||
const renderedPos = edge.renderedMidpoint();
|
||||
setLinkTooltipPos({ x: renderedPos.x, y: renderedPos.y });
|
||||
}
|
||||
containerRef.current!.style.cursor = 'pointer';
|
||||
});
|
||||
|
||||
cy.on('mouseout', 'edge', () => {
|
||||
setHoveredLink(null);
|
||||
setLinkTooltipPos(null);
|
||||
containerRef.current!.style.cursor = 'default';
|
||||
});
|
||||
|
||||
// Run layout
|
||||
cy.layout({
|
||||
name: 'cose',
|
||||
animate: false,
|
||||
randomize: true,
|
||||
nodeRepulsion: () => 100000,
|
||||
idealEdgeLength: () => 300,
|
||||
edgeElasticity: () => 20,
|
||||
nestingFactor: 0.1,
|
||||
gravity: 0.01,
|
||||
numIter: 2500,
|
||||
coolingFactor: 0.95,
|
||||
minTemp: 1.0,
|
||||
nodeOverlap: 20,
|
||||
nodeDimensionsIncludeLabels: true,
|
||||
padding: 50,
|
||||
} as any).run();
|
||||
|
||||
// Fit to viewport
|
||||
cy.fit(undefined, 50);
|
||||
setIsLoading(false);
|
||||
|
||||
return () => {
|
||||
cy.destroy();
|
||||
};
|
||||
}, [cyElements, showLabels, isDarkMode]);
|
||||
|
||||
// Handle resize
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (cyRef.current) {
|
||||
cyRef.current.resize();
|
||||
cyRef.current.fit(undefined, 50);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative w-full rounded-lg overflow-hidden border border-border" style={{ height }}>
|
||||
{/* Loading state */}
|
||||
{isLoading && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-background z-10">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4" />
|
||||
<p className="text-sm text-muted-foreground">Loading graph...</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Cytoscape container */}
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="w-full h-full"
|
||||
style={{
|
||||
background: isDarkMode
|
||||
? 'radial-gradient(circle at 1px 1px, rgba(255,255,255,0.08) 1px, transparent 0)'
|
||||
: 'radial-gradient(circle at 1px 1px, rgba(0,0,0,0.06) 1px, transparent 0)',
|
||||
backgroundSize: '20px 20px',
|
||||
backgroundColor: isDarkMode ? '#0f1419' : '#f8fafc',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Empty state */}
|
||||
{!isLoading && graphData.nodes.length === 0 && (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<p className="text-muted-foreground">No memories to display</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Link hover tooltip */}
|
||||
{hoveredLink && linkTooltipPos && (
|
||||
<div
|
||||
className="absolute z-30 pointer-events-none"
|
||||
style={{
|
||||
left: linkTooltipPos.x,
|
||||
top: linkTooltipPos.y,
|
||||
transform: 'translate(-50%, -100%) translateY(-8px)',
|
||||
}}
|
||||
>
|
||||
<div className={`px-3 py-2 rounded-lg shadow-lg text-sm ${
|
||||
isDarkMode ? 'bg-gray-800 text-white' : 'bg-white text-gray-900 border border-gray-200'
|
||||
}`}>
|
||||
<div className="font-medium capitalize mb-1">
|
||||
{(() => {
|
||||
const type = hoveredLink.type || 'semantic';
|
||||
if (['causes', 'caused_by', 'enables', 'prevents'].includes(type)) {
|
||||
return `Causal (${type.replace('_', ' ')})`;
|
||||
}
|
||||
return `${type} link`;
|
||||
})()}
|
||||
</div>
|
||||
{hoveredLink.entity && (
|
||||
<div className="text-xs opacity-80">
|
||||
Entity: <span className="font-medium">{hoveredLink.entity}</span>
|
||||
</div>
|
||||
)}
|
||||
{hoveredLink.weight !== undefined && (
|
||||
<div className="text-xs opacity-80">
|
||||
Weight: <span className="font-medium">{hoveredLink.weight.toFixed(3)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Controls hint */}
|
||||
<div className="absolute bottom-4 right-4 text-xs text-muted-foreground/60 z-20">
|
||||
Drag to pan • Scroll to zoom • Click node to focus
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Utility Functions
|
||||
// ============================================================================
|
||||
|
||||
export function convertHindsightGraphData(hindsightData: {
|
||||
nodes?: Array<{ data: { id: string; label?: string; color?: string } }>;
|
||||
edges?: Array<{ data: { source: string; target: string; color?: string; lineStyle?: string; linkType?: string; entityName?: string; weight?: number; similarity?: number } }>;
|
||||
table_rows?: Array<{ id: string; text: string; entities?: string; context?: string }>;
|
||||
}): GraphData {
|
||||
const nodes: GraphNode[] = (hindsightData.nodes || []).map(n => {
|
||||
const tableRow = hindsightData.table_rows?.find(r => r.id === n.data.id);
|
||||
// Use memory text as label, truncated to ~40 chars
|
||||
let label = n.data.label;
|
||||
if (!label && tableRow?.text) {
|
||||
label = tableRow.text.length > 40 ? tableRow.text.substring(0, 40) + '...' : tableRow.text;
|
||||
}
|
||||
if (!label) {
|
||||
label = n.data.id.substring(0, 8);
|
||||
}
|
||||
return {
|
||||
id: n.data.id,
|
||||
label,
|
||||
color: n.data.color,
|
||||
metadata: tableRow,
|
||||
};
|
||||
});
|
||||
|
||||
const links: GraphLink[] = (hindsightData.edges || []).map(e => ({
|
||||
source: e.data.source,
|
||||
target: e.data.target,
|
||||
color: e.data.color,
|
||||
// Use linkType directly from API, fallback to lineStyle check, default to semantic
|
||||
type: e.data.linkType || (e.data.lineStyle === 'dashed' ? 'temporal' : 'semantic'),
|
||||
entity: e.data.entityName, // API returns entityName
|
||||
weight: e.data.weight ?? e.data.similarity,
|
||||
}));
|
||||
|
||||
return { nodes, links };
|
||||
}
|
||||
@@ -64,13 +64,12 @@ export function MemoryDetailPanel({
|
||||
<p className="text-sm text-muted-foreground mt-1">Full memory content and metadata</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onClose}
|
||||
className="h-9 px-3 gap-2"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
Close
|
||||
<X className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,16 +15,16 @@ interface SidebarProps {
|
||||
|
||||
export function Sidebar({ currentTab, onTabChange }: SidebarProps) {
|
||||
const { currentBank } = useBank();
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
const [isCollapsed, setIsCollapsed] = useState(true);
|
||||
|
||||
if (!currentBank) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const navItems = [
|
||||
{ id: 'data' as NavItem, label: 'Memories', icon: Database },
|
||||
{ id: 'recall' as NavItem, label: 'Recall', icon: Search },
|
||||
{ id: 'reflect' as NavItem, label: 'Reflect', icon: Sparkles },
|
||||
{ id: 'data' as NavItem, label: 'Memories', icon: Database },
|
||||
{ id: 'documents' as NavItem, label: 'Documents', icon: FileText },
|
||||
{ id: 'entities' as NavItem, label: 'Entities', icon: Users },
|
||||
{ id: 'profile' as NavItem, label: 'Memory Bank', icon: Box },
|
||||
@@ -35,24 +35,7 @@ export function Sidebar({ currentTab, onTabChange }: SidebarProps) {
|
||||
'bg-card border-r border-border flex flex-col transition-all duration-300',
|
||||
isCollapsed ? 'w-16' : 'w-64'
|
||||
)}>
|
||||
<div className="p-4 border-b border-border flex items-center justify-between">
|
||||
{!isCollapsed && (
|
||||
<h2 className="text-lg font-semibold text-card-foreground">Hindsight</h2>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setIsCollapsed(!isCollapsed)}
|
||||
className="p-1 rounded-lg hover:bg-accent transition-colors ml-auto"
|
||||
title={isCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
||||
>
|
||||
{isCollapsed ? (
|
||||
<ChevronRight className="w-5 h-5" />
|
||||
) : (
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 p-3">
|
||||
<nav className="flex-1 p-3 pt-4">
|
||||
<ul className="space-y-1">
|
||||
{navItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
@@ -75,7 +58,7 @@ export function Sidebar({ currentTab, onTabChange }: SidebarProps) {
|
||||
className={cn(
|
||||
'w-full flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-all',
|
||||
isActive
|
||||
? 'bg-primary text-primary-foreground shadow-sm'
|
||||
? 'bg-primary-gradient text-white shadow-sm'
|
||||
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground',
|
||||
isCollapsed && 'justify-center px-0'
|
||||
)}
|
||||
@@ -89,6 +72,27 @@ export function Sidebar({ currentTab, onTabChange }: SidebarProps) {
|
||||
})}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
{/* Collapse/Expand button at bottom */}
|
||||
<div className="p-3 border-t border-border">
|
||||
<button
|
||||
onClick={() => setIsCollapsed(!isCollapsed)}
|
||||
className={cn(
|
||||
'w-full flex items-center gap-3 px-4 py-2 rounded-lg text-sm text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-colors',
|
||||
isCollapsed && 'justify-center px-0'
|
||||
)}
|
||||
title={isCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
||||
>
|
||||
{isCollapsed ? (
|
||||
<ChevronRight className="w-5 h-5" />
|
||||
) : (
|
||||
<>
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
<span>Collapse</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ const buttonVariants = cva(
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
default: "bg-primary-gradient text-white hover:opacity-90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SliderPrimitive from "@radix-ui/react-slider"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Slider = React.forwardRef<
|
||||
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SliderPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full touch-none select-none items-center",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
|
||||
<SliderPrimitive.Range className="absolute h-full bg-primary" />
|
||||
</SliderPrimitive.Track>
|
||||
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
|
||||
</SliderPrimitive.Root>
|
||||
))
|
||||
Slider.displayName = SliderPrimitive.Root.displayName
|
||||
|
||||
export { Slider }
|
||||
@@ -0,0 +1,29 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Switch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
))
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||
|
||||
export { Switch }
|
||||
@@ -0,0 +1,46 @@
|
||||
'use client';
|
||||
|
||||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
|
||||
type Theme = 'light' | 'dark';
|
||||
|
||||
interface ThemeContextType {
|
||||
theme: Theme;
|
||||
toggleTheme: () => void;
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||
|
||||
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||
const [theme, setTheme] = useState<Theme>('light');
|
||||
|
||||
useEffect(() => {
|
||||
// Check for saved preference or system preference
|
||||
const saved = localStorage.getItem('theme') as Theme | null;
|
||||
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const initialTheme = saved || (systemPrefersDark ? 'dark' : 'light');
|
||||
setTheme(initialTheme);
|
||||
document.documentElement.classList.toggle('dark', initialTheme === 'dark');
|
||||
}, []);
|
||||
|
||||
const toggleTheme = () => {
|
||||
const newTheme = theme === 'light' ? 'dark' : 'light';
|
||||
setTheme(newTheme);
|
||||
localStorage.setItem('theme', newTheme);
|
||||
document.documentElement.classList.toggle('dark', newTheme === 'dark');
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, toggleTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
const context = useContext(ThemeContext);
|
||||
if (!context) {
|
||||
throw new Error('useTheme must be used within a ThemeProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -317,7 +317,7 @@ If it's correct, set correct=true.
|
||||
response_format=JudgeResponse,
|
||||
scope="judge",
|
||||
temperature=0,
|
||||
max_tokens=4096
|
||||
max_completion_tokens=4096
|
||||
)
|
||||
|
||||
return judgement.correct, judgement.reasoning
|
||||
|
||||
@@ -398,7 +398,7 @@ Answer:
|
||||
],
|
||||
response_format=QuestionAnswer,
|
||||
scope="memory",
|
||||
max_tokens=32768,
|
||||
max_completion_tokens=32768,
|
||||
)
|
||||
reasoning_text = answer_obj.reasoning or ""
|
||||
if reasoning_text:
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "hindsight-dev"
|
||||
version = "0.1.0"
|
||||
version = "0.1.4"
|
||||
description = "Development utilities for Hindsight"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
|
||||
@@ -160,15 +160,14 @@ hindsight retain my-bank "Project deadline: April 15 (extended)" --document-id p
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## List Documents
|
||||
## Get Document
|
||||
|
||||
View all documents in a memory bank:
|
||||
Retrieve a document's original text and metadata. This is useful for expanding document context after a recall operation returns memories with document references.
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
# Using the low-level API
|
||||
from hindsight_client_api import ApiClient, Configuration
|
||||
from hindsight_client_api.api import DefaultApi
|
||||
|
||||
@@ -176,19 +175,16 @@ config = Configuration(host="http://localhost:8888")
|
||||
api_client = ApiClient(config)
|
||||
api = DefaultApi(api_client)
|
||||
|
||||
# List all documents
|
||||
response = api.list_documents(bank_id="my-bank")
|
||||
|
||||
for doc in response.items:
|
||||
print(f"{doc.id}: {doc.memory_unit_count} memories")
|
||||
print(f" Created: {doc.created_at}")
|
||||
|
||||
# With pagination
|
||||
response = api.list_documents(
|
||||
# Get document to expand context from recall results
|
||||
doc = api.get_document(
|
||||
bank_id="my-bank",
|
||||
limit=50,
|
||||
offset=0
|
||||
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}")
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
@@ -199,129 +195,28 @@ import { sdk, createClient, createConfig } from '@vectorize-io/hindsight-client'
|
||||
|
||||
const apiClient = createClient(createConfig({ baseUrl: 'http://localhost:8888' }));
|
||||
|
||||
// List all documents
|
||||
const response = await sdk.listDocuments({
|
||||
client: apiClient,
|
||||
path: { bank_id: 'my-bank' }
|
||||
});
|
||||
|
||||
for (const doc of response.data.items) {
|
||||
console.log(`${doc.id}: ${doc.memory_unit_count} memories`);
|
||||
console.log(` Created: ${doc.created_at}`);
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="cli" label="CLI">
|
||||
|
||||
```bash
|
||||
# List documents
|
||||
hindsight documents list my-bank
|
||||
|
||||
# With limit
|
||||
hindsight documents list my-bank --limit 50
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Get Document Details
|
||||
|
||||
Retrieve a specific document with its content:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
# Get document
|
||||
doc = 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[:200]}...")
|
||||
print(f"Memories: {doc.memory_unit_count}")
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
|
||||
```typescript
|
||||
// Get document
|
||||
const doc = await sdk.getDocument({
|
||||
// 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.data.id}`);
|
||||
console.log(`Original text: ${doc.data.original_text.substring(0, 200)}...`);
|
||||
console.log(`Memories: ${doc.data.memory_unit_count}`);
|
||||
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}`);
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="cli" label="CLI">
|
||||
|
||||
```bash
|
||||
# Get document
|
||||
hindsight documents get my-bank meeting-2024-03-15
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Delete Documents
|
||||
|
||||
Remove a document and all its memories:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
# Delete document (removes all associated memories)
|
||||
api.delete_document(
|
||||
bank_id="my-bank",
|
||||
document_id="old-meeting"
|
||||
)
|
||||
|
||||
# Bulk delete
|
||||
for doc_id in ["old-1", "old-2", "old-3"]:
|
||||
api.delete_document(bank_id="my-bank", document_id=doc_id)
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
|
||||
```typescript
|
||||
// Delete document
|
||||
await sdk.deleteDocument({
|
||||
client: apiClient,
|
||||
path: { bank_id: 'my-bank', document_id: 'old-meeting' }
|
||||
});
|
||||
|
||||
// Bulk delete
|
||||
for (const docId of ['old-1', 'old-2', 'old-3']) {
|
||||
await sdk.deleteDocument({
|
||||
client: apiClient,
|
||||
path: { bank_id: 'my-bank', document_id: docId }
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="cli" label="CLI">
|
||||
|
||||
```bash
|
||||
# Delete document
|
||||
hindsight documents delete my-bank old-meeting
|
||||
|
||||
# Confirm deletion
|
||||
hindsight documents delete my-bank old-meeting --confirm
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Document Response Format
|
||||
|
||||
```json
|
||||
@@ -329,76 +224,13 @@ hindsight documents delete my-bank old-meeting --confirm
|
||||
"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",
|
||||
"retain_params": {
|
||||
"context": "team meeting",
|
||||
"event_date": "2024-03-15"
|
||||
}
|
||||
"updated_at": "2024-03-15T14:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
### Meeting Notes
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
from datetime import date
|
||||
|
||||
# Store meeting notes with date-based IDs
|
||||
client.retain(
|
||||
bank_id="team-memory",
|
||||
content=meeting_transcript,
|
||||
document_id=f"meeting-{date.today()}"
|
||||
)
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Documentation
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
|
||||
# Store docs with version tracking
|
||||
docs_dir = Path("docs")
|
||||
version = "1.0"
|
||||
|
||||
for file in docs_dir.glob("*.md"):
|
||||
client.retain(
|
||||
bank_id="docs-memory",
|
||||
content=file.read_text(),
|
||||
document_id=f"docs-{file.stem}-v{version}"
|
||||
)
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Conversation History
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
# Store chat history with session IDs
|
||||
client.retain(
|
||||
bank_id="chat-memory",
|
||||
content=conversation,
|
||||
document_id=f"session-{session_id}"
|
||||
)
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [**Entities**](./entities) — Track people, places, and concepts
|
||||
|
||||
@@ -4,13 +4,10 @@ sidebar_position: 7
|
||||
|
||||
# Entities
|
||||
|
||||
Entities are the people, organizations, places, and concepts that Hindsight automatically tracks across your memory bank.
|
||||
Entities are the people, organizations, places, and concepts that Hindsight automatically extracts and tracks across your memory bank.
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
:::tip Prerequisites
|
||||
Make sure you've completed the [Quick Start](./quickstart) and understand [how retain works](./retain).
|
||||
:::info Automatic Feature
|
||||
You don't need to do anything to use entities—Hindsight extracts them automatically when you call `retain`. However, understanding how entities work is important because they power key features in [recall](./recall) and [reflect](./reflect).
|
||||
:::
|
||||
|
||||
## Why Entities Matter
|
||||
@@ -21,243 +18,95 @@ Entities improve recall quality in two ways:
|
||||
|
||||
2. **Observations** — Hindsight synthesizes high-level summaries about each entity from multiple facts. Including entity observations in recall provides richer context.
|
||||
|
||||
:::tip Include Entities in Recall
|
||||
Use `include_entities=True` in your recall calls to get entity observations alongside fact results. See [Recall](./recall) for details.
|
||||
:::
|
||||
## What Gets Extracted?
|
||||
|
||||
## What Are Entities?
|
||||
When you retain information, the LLM extracts named entities from each fact:
|
||||
|
||||
When you retain information, Hindsight automatically identifies and tracks entities:
|
||||
- **People** — Names like "Alice", "Dr. Smith", "CEO John"
|
||||
- **Organizations** — Companies, teams, institutions
|
||||
- **Places** — Cities, countries, specific locations
|
||||
- **Products/Objects** — Software, tools, significant items
|
||||
- **Concepts** — Abstract themes like "career growth", "friendship"
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
**Example:**
|
||||
|
||||
```python
|
||||
from hindsight_client import Hindsight
|
||||
|
||||
client = Hindsight(base_url="http://localhost:8888")
|
||||
|
||||
client.retain(
|
||||
bank_id="my-bank",
|
||||
content="Alice works at Google in Mountain View. She specializes in TensorFlow."
|
||||
)
|
||||
```
|
||||
Content: "Alice works at Google in Mountain View. She specializes in TensorFlow."
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
|
||||
```typescript
|
||||
import { HindsightClient } from '@vectorize-io/hindsight-client';
|
||||
|
||||
const client = new HindsightClient({ baseUrl: 'http://localhost:8888' });
|
||||
|
||||
await client.retain('my-bank', 'Alice works at Google in Mountain View. She specializes in TensorFlow.');
|
||||
Entities extracted:
|
||||
- Alice (person)
|
||||
- Google (organization)
|
||||
- Mountain View (location)
|
||||
- TensorFlow (product)
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
**Entities extracted:**
|
||||
- **Alice** (person)
|
||||
- **Google** (organization)
|
||||
- **Mountain View** (location)
|
||||
- **TensorFlow** (product)
|
||||
|
||||
## Entity Resolution
|
||||
|
||||
Multiple mentions are unified into a single entity:
|
||||
When the same entity is mentioned multiple times (possibly with different names), Hindsight resolves them to a single canonical entity using a scoring algorithm:
|
||||
|
||||
- "Alice" + "Alice Chen" + "Alice C." → one person
|
||||
- "Bob" + "Robert Chen" → one person (nickname)
|
||||
- Context-aware: "Apple (company)" vs "apple (fruit)"
|
||||
### Resolution Factors
|
||||
|
||||
## List Entities
|
||||
1. **Name similarity (50%)** — How closely the text matches existing entity names. Handles variations like "Alice" vs "Alice Chen" or partial matches.
|
||||
|
||||
Get all entities tracked in a memory bank:
|
||||
2. **Co-occurrence (30%)** — Entities that frequently appear together are more likely to be the same. If "Alice" always appears with "Google" and "TensorFlow", a new mention of "Alice" near those entities scores higher for matching.
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
3. **Temporal proximity (20%)** — Recent mentions are weighted more heavily. If an entity was seen in the last 7 days, new similar mentions are more likely to match.
|
||||
|
||||
```python
|
||||
# Using the low-level API
|
||||
from hindsight_client_api import ApiClient, Configuration
|
||||
from hindsight_client_api.api import DefaultApi
|
||||
### Resolution Threshold
|
||||
|
||||
config = Configuration(host="http://localhost:8888")
|
||||
api_client = ApiClient(config)
|
||||
api = DefaultApi(api_client)
|
||||
A match requires a combined score above **0.6** (60%). Below this threshold, Hindsight creates a new entity rather than risk merging distinct entities.
|
||||
|
||||
# List all entities
|
||||
response = api.list_entities(bank_id="my-bank")
|
||||
|
||||
for entity in response.items:
|
||||
print(f"{entity.canonical_name}: {entity.mention_count} mentions")
|
||||
|
||||
# List with pagination
|
||||
response = api.list_entities(
|
||||
bank_id="my-bank",
|
||||
limit=50,
|
||||
offset=0
|
||||
)
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
|
||||
```typescript
|
||||
import { sdk, createClient, createConfig } from '@vectorize-io/hindsight-client';
|
||||
|
||||
const apiClient = createClient(createConfig({ baseUrl: 'http://localhost:8888' }));
|
||||
|
||||
// List all entities
|
||||
const response = await sdk.listEntities({
|
||||
client: apiClient,
|
||||
path: { bank_id: 'my-bank' }
|
||||
});
|
||||
|
||||
for (const entity of response.data.items) {
|
||||
console.log(`${entity.canonical_name}: ${entity.mention_count} mentions`);
|
||||
}
|
||||
|
||||
// List with pagination
|
||||
const paginated = await sdk.listEntities({
|
||||
client: apiClient,
|
||||
path: { bank_id: 'my-bank' },
|
||||
query: { limit: 50, offset: 0 }
|
||||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="cli" label="CLI">
|
||||
|
||||
```bash
|
||||
# List all entities
|
||||
hindsight entities list my-bank
|
||||
|
||||
# With limit
|
||||
hindsight entities list my-bank --limit 50
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Get Entity Details
|
||||
|
||||
Retrieve detailed information about a specific entity:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
# Get entity details with observations
|
||||
entity = api.get_entity(
|
||||
bank_id="my-bank",
|
||||
entity_id="entity-uuid"
|
||||
)
|
||||
|
||||
print(f"Entity: {entity.canonical_name}")
|
||||
print(f"First seen: {entity.first_seen}")
|
||||
print(f"Mentions: {entity.mention_count}")
|
||||
|
||||
# Observations (synthesized summaries)
|
||||
for obs in entity.observations:
|
||||
print(f" - {obs.text}")
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
|
||||
```typescript
|
||||
// Get entity details
|
||||
const entity = await sdk.getEntity({
|
||||
client: apiClient,
|
||||
path: { bank_id: 'my-bank', entity_id: 'entity-uuid' }
|
||||
});
|
||||
|
||||
console.log(`Entity: ${entity.data.canonical_name}`);
|
||||
console.log(`First seen: ${entity.data.first_seen}`);
|
||||
console.log(`Mentions: ${entity.data.mention_count}`);
|
||||
|
||||
// Observations
|
||||
for (const obs of entity.data.observations) {
|
||||
console.log(` - ${obs.text}`);
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="cli" label="CLI">
|
||||
|
||||
```bash
|
||||
# Get entity details
|
||||
hindsight entities get my-bank entity-uuid
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
This means:
|
||||
- Exact name matches with recent co-occurring entities → strong match
|
||||
- Partial name matches without context → likely creates new entity
|
||||
- Same name in completely different contexts → may create separate entities
|
||||
|
||||
## Entity Observations
|
||||
|
||||
Observations are high-level summaries automatically synthesized from multiple facts:
|
||||
Observations are **derived state**—high-level summaries that Hindsight automatically synthesizes from the facts associated with an entity. They provide a condensed view of what the system knows about important entities.
|
||||
|
||||
**Facts about Alice:**
|
||||
**Example:**
|
||||
|
||||
Facts about Alice:
|
||||
- "Alice works at Google"
|
||||
- "Alice is a software engineer"
|
||||
- "Alice specializes in ML"
|
||||
- "Alice joined Google in 2020"
|
||||
- "Alice leads the search team"
|
||||
|
||||
**Observation created:**
|
||||
- "Alice is a software engineer at Google specializing in ML"
|
||||
Observation created:
|
||||
- "Alice is a software engineer at Google who joined in 2020, specializes in ML, and leads the search team"
|
||||
|
||||
Observations are generated in the background after retaining information.
|
||||
### How Observations Work
|
||||
|
||||
## Regenerate Observations
|
||||
Observations are **not generated for every entity**. When you retain new documents:
|
||||
|
||||
Force regeneration of entity observations:
|
||||
1. **Top entities selected** — Hindsight identifies the top 5 most-mentioned entities in the batch
|
||||
2. **Threshold check** — Only entities with at least 5 facts get observations
|
||||
3. **Regeneration** — Observations are regenerated using the entity's most recent 50 facts
|
||||
4. **Old observations replaced** — Previous observations are deleted and new ones created
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
This means:
|
||||
- Frequently mentioned entities get observations; rarely mentioned ones don't
|
||||
- Observations stay up-to-date as new information is retained
|
||||
- The system prioritizes entities that matter most to your memory bank
|
||||
|
||||
```python
|
||||
# Regenerate observations for an entity
|
||||
api.regenerate_entity_observations(
|
||||
bank_id="my-bank",
|
||||
entity_id="entity-uuid"
|
||||
)
|
||||
```
|
||||
### Observations vs Opinions
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
Observations are **objective summaries**—they synthesize facts without any bias or perspective. This is different from [opinions](./opinions), which are influenced by the memory bank's disposition.
|
||||
|
||||
```typescript
|
||||
// Regenerate observations
|
||||
await sdk.regenerateEntityObservations({
|
||||
client: apiClient,
|
||||
path: { bank_id: 'my-bank', entity_id: 'entity-uuid' }
|
||||
});
|
||||
```
|
||||
| | Observations | Opinions |
|
||||
|---|---|---|
|
||||
| **Purpose** | Summarize what's known about an entity | Express the bank's perspective on a topic |
|
||||
| **Disposition influence** | No | Yes |
|
||||
| **Scope** | Per-entity | Any topic |
|
||||
| **Generation** | Automatic (top entities) | On-demand via reflect |
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
### Using Observations
|
||||
|
||||
## Entity Response Format
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "entity-uuid",
|
||||
"canonical_name": "Alice Chen",
|
||||
"first_seen": "2024-01-15T10:30:00Z",
|
||||
"last_seen": "2024-03-20T14:22:00Z",
|
||||
"mention_count": 47,
|
||||
"observations": [
|
||||
{
|
||||
"text": "Alice is a software engineer at Google specializing in ML",
|
||||
"mentioned_at": "2024-03-20T15:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
Observations are included in recall results when you set `include_entities=True`. They provide quick context about key entities without retrieving all underlying facts.
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [**Memory Banks**](./memory-banks) — Configure bank disposition
|
||||
- [**Documents**](./documents) — Track document sources
|
||||
- [**Operations**](./operations) — Monitor background tasks
|
||||
- [**Recall**](./recall) — Use entities in memory retrieval
|
||||
- [**Reflect**](./reflect) — Get entity-aware responses
|
||||
|
||||
@@ -2,17 +2,26 @@
|
||||
sidebar_position: 6
|
||||
---
|
||||
|
||||
# Memory Bank
|
||||
# Memory Banks
|
||||
|
||||
Configure memory bank disposition, background, and behavior.
|
||||
Memory banks have characteristics:
|
||||
- Banks are completely isolated from each other.
|
||||
- You don't need to pre-create it, Hindsight will create it for you with default settings.
|
||||
- Banks have a profile that influences how they form opinions from memories. (optional)
|
||||
Memory banks are isolated containers that store all memory-related data for a specific context or use case.
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
## 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
|
||||
|
||||
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.
|
||||
|
||||
:::tip Prerequisites
|
||||
Make sure you've completed the [Quick Start](./quickstart) to install the client and start the server.
|
||||
:::
|
||||
@@ -32,9 +41,9 @@ client.create_bank(
|
||||
name="Research Assistant",
|
||||
background="I am a research assistant specializing in machine learning",
|
||||
disposition={
|
||||
"skepticism": 4, # Questions claims, wants evidence
|
||||
"literalism": 3, # Balanced interpretation
|
||||
"empathy": 3 # Balanced emotional consideration
|
||||
"skepticism": 4,
|
||||
"literalism": 3,
|
||||
"empathy": 3
|
||||
}
|
||||
)
|
||||
```
|
||||
@@ -75,51 +84,17 @@ hindsight bank disposition my-bank \
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Disposition Traits
|
||||
## Background and Disposition
|
||||
|
||||
Each trait is scored 1 to 5:
|
||||
Background and disposition are optional settings that influence how the bank forms opinions during [reflect](./reflect) operations.
|
||||
|
||||
| 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 |
|
||||
:::info
|
||||
Background and disposition only affect the `reflect` operation (opinion formation). They do not impact `retain`, `recall`, or other memory operations.
|
||||
:::
|
||||
|
||||
### How Traits Affect Behavior
|
||||
### Background
|
||||
|
||||
**Skepticism** influences how the bank evaluates claims:
|
||||
|
||||
```python
|
||||
# High skepticism (5)
|
||||
"What's the source for this? Have these results been replicated?"
|
||||
|
||||
# Low skepticism (1)
|
||||
"That sounds reasonable, let's proceed with that assumption."
|
||||
```
|
||||
|
||||
**Literalism** affects interpretation:
|
||||
|
||||
```python
|
||||
# High literalism (5)
|
||||
"The requirement says 'users' - that means all users, no exceptions."
|
||||
|
||||
# Low literalism (1)
|
||||
"When they say 'users', they probably mean active users in this context."
|
||||
```
|
||||
|
||||
**Empathy** shapes how emotional context is considered:
|
||||
|
||||
```python
|
||||
# High empathy (5)
|
||||
"I understand this is frustrating. Let's find a solution that works for you."
|
||||
|
||||
# Low empathy (1)
|
||||
"Here are the facts: Option A has 20% better performance than Option B."
|
||||
```
|
||||
|
||||
## Background
|
||||
|
||||
The background is a first-person narrative providing bank context:
|
||||
The background is a first-person narrative providing context for opinion formation:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
@@ -147,164 +122,12 @@ await client.createBank('financial-advisor', {
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Background influences:
|
||||
- How questions are interpreted
|
||||
- Perspective in responses
|
||||
- Opinion formation context
|
||||
### Disposition Traits
|
||||
|
||||
## Getting Bank Profile
|
||||
Disposition traits influence how opinions are formed during reflection. Each trait is scored 1 to 5:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
# Using the low-level API
|
||||
from hindsight_client_api import ApiClient, Configuration
|
||||
from hindsight_client_api.api import DefaultApi
|
||||
|
||||
config = Configuration(host="http://localhost:8888")
|
||||
api_client = ApiClient(config)
|
||||
api = DefaultApi(api_client)
|
||||
|
||||
profile = api.get_bank_profile("my-bank")
|
||||
|
||||
print(f"Name: {profile.name}")
|
||||
print(f"Background: {profile.background}")
|
||||
print(f"Disposition: {profile.disposition}")
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
|
||||
```typescript
|
||||
const profile = await client.getBankProfile('my-bank');
|
||||
|
||||
console.log(`Name: ${profile.name}`);
|
||||
console.log(`Background: ${profile.background}`);
|
||||
console.log(`Disposition:`, profile.disposition);
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="cli" label="CLI">
|
||||
|
||||
```bash
|
||||
hindsight bank profile my-bank
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Default Values
|
||||
|
||||
If not specified, banks use neutral defaults:
|
||||
|
||||
```python
|
||||
{
|
||||
"skepticism": 3,
|
||||
"literalism": 3,
|
||||
"empathy": 3,
|
||||
"background": ""
|
||||
}
|
||||
```
|
||||
|
||||
## Disposition Templates
|
||||
|
||||
Common disposition configurations:
|
||||
|
||||
| Use Case | Skepticism | Literalism | Empathy |
|
||||
|----------|------------|------------|---------|
|
||||
| **Customer Support** | 2 | 2 | 5 |
|
||||
| **Code Reviewer** | 4 | 5 | 2 |
|
||||
| **Legal Analyst** | 5 | 5 | 2 |
|
||||
| **Therapist/Coach** | 2 | 2 | 5 |
|
||||
| **Research Assistant** | 4 | 3 | 3 |
|
||||
| **Neutral (default)** | 3 | 3 | 3 |
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
# Customer support bank
|
||||
client.create_bank(
|
||||
bank_id="support",
|
||||
background="I am a friendly customer support agent",
|
||||
disposition={
|
||||
"skepticism": 2, # Trusting
|
||||
"literalism": 2, # Flexible interpretation
|
||||
"empathy": 5 # Very empathetic
|
||||
}
|
||||
)
|
||||
|
||||
# Code reviewer bank
|
||||
client.create_bank(
|
||||
bank_id="reviewer",
|
||||
background="I am a thorough code reviewer focused on quality",
|
||||
disposition={
|
||||
"skepticism": 4, # Questions assumptions
|
||||
"literalism": 5, # Exact interpretation
|
||||
"empathy": 2 # Direct, fact-focused
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
|
||||
```typescript
|
||||
// Customer support bank
|
||||
await client.createBank('support', {
|
||||
background: 'I am a friendly customer support agent',
|
||||
disposition: {
|
||||
skepticism: 2,
|
||||
literalism: 2,
|
||||
empathy: 5
|
||||
}
|
||||
});
|
||||
|
||||
// Code reviewer bank
|
||||
await client.createBank('reviewer', {
|
||||
background: 'I am a thorough code reviewer focused on quality',
|
||||
disposition: {
|
||||
skepticism: 4,
|
||||
literalism: 5,
|
||||
empathy: 2
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Bank Isolation
|
||||
|
||||
Each bank has:
|
||||
- **Separate memories** — banks don't share memories
|
||||
- **Own disposition** — traits are per-bank
|
||||
- **Independent opinions** — formed from their own experiences
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
# Store to bank A
|
||||
client.retain(bank_id="bank-a", content="Python is great")
|
||||
|
||||
# Bank B doesn't see it
|
||||
results = client.recall(bank_id="bank-b", query="Python")
|
||||
# Returns empty
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
|
||||
```typescript
|
||||
// Store to bank A
|
||||
await client.retain('bank-a', 'Python is great');
|
||||
|
||||
// Bank B doesn't see it
|
||||
const results = await client.recall('bank-b', 'Python');
|
||||
// Returns empty
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
| 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 |
|
||||
|
||||
@@ -4,290 +4,31 @@ sidebar_position: 9
|
||||
|
||||
# Operations
|
||||
|
||||
Monitor and manage long-running background tasks in Hindsight.
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
Background tasks that Hindsight executes asynchronously.
|
||||
|
||||
:::tip Prerequisites
|
||||
Make sure you've completed the [Quick Start](./quickstart) and understand [how retain works](./retain).
|
||||
:::
|
||||
|
||||
## What Are Operations?
|
||||
## How Operations Work
|
||||
|
||||
When you call `retain_batch` with `async=True`, Hindsight processes the content in the background and returns immediately with an operation ID. Operations let you track and manage these async retain tasks.
|
||||
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, async operations are executed in-process within the API service. This is managed automatically — you don't need to configure anything.
|
||||
By default, all background operations are executed in-process within the API service.
|
||||
|
||||
:::tip Scaling with Streaming
|
||||
For high-throughput workloads, you can extend the task backend to use a streaming platform like Kafka. This enables scale-out processing across multiple workers and handles backpressure on the API.
|
||||
:::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.
|
||||
:::
|
||||
|
||||
## Async Batch Retain
|
||||
|
||||
For large content batches, use async mode to avoid timeouts:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
from hindsight_client import Hindsight
|
||||
|
||||
client = Hindsight(base_url="http://localhost:8888")
|
||||
|
||||
client.retain_batch(
|
||||
bank_id="my-bank",
|
||||
items=[
|
||||
{"content": doc1_text},
|
||||
{"content": doc2_text},
|
||||
],
|
||||
retain_async=True
|
||||
)
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
|
||||
```typescript
|
||||
import { HindsightClient } from '@vectorize-io/hindsight-client';
|
||||
|
||||
const client = new HindsightClient({ baseUrl: 'http://localhost:8888' });
|
||||
|
||||
await client.retainBatch('my-bank', [
|
||||
{ content: doc1Text },
|
||||
{ content: doc2Text },
|
||||
], { async: true });
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="cli" label="CLI">
|
||||
|
||||
```bash
|
||||
hindsight retain my-bank --files docs/*.md --async
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## List Operations
|
||||
|
||||
View all operations for a memory bank:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
# Using the low-level API
|
||||
from hindsight_client_api import ApiClient, Configuration
|
||||
from hindsight_client_api.api import DefaultApi
|
||||
|
||||
config = Configuration(host="http://localhost:8888")
|
||||
api_client = ApiClient(config)
|
||||
api = DefaultApi(api_client)
|
||||
|
||||
# List all operations
|
||||
response = api.list_operations(bank_id="my-bank")
|
||||
|
||||
for op in response.items:
|
||||
print(f"{op.id}: {op.task_type} - {op.status}")
|
||||
print(f" Items: {op.items_count}")
|
||||
if op.error_message:
|
||||
print(f" Error: {op.error_message}")
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
|
||||
```typescript
|
||||
import { sdk, createClient, createConfig } from '@vectorize-io/hindsight-client';
|
||||
|
||||
const apiClient = createClient(createConfig({ baseUrl: 'http://localhost:8888' }));
|
||||
|
||||
// List all operations
|
||||
const response = await sdk.listOperations({
|
||||
client: apiClient,
|
||||
path: { bank_id: 'my-bank' }
|
||||
});
|
||||
|
||||
for (const op of response.data.items) {
|
||||
console.log(`${op.id}: ${op.task_type} - ${op.status}`);
|
||||
console.log(` Items: ${op.items_count}`);
|
||||
if (op.error_message) {
|
||||
console.log(` Error: ${op.error_message}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="cli" label="CLI">
|
||||
|
||||
```bash
|
||||
# List all operations
|
||||
hindsight operations list my-bank
|
||||
|
||||
# Filter by status
|
||||
hindsight operations list my-bank --status running
|
||||
|
||||
# Watch all running operations
|
||||
hindsight operations watch my-bank --all
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Cancel Operations
|
||||
|
||||
Stop a running or pending operation:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
# Cancel operation
|
||||
api.cancel_operation(
|
||||
bank_id="my-bank",
|
||||
operation_id="op-abc123"
|
||||
)
|
||||
|
||||
# Cancel all pending operations
|
||||
response = api.list_operations(bank_id="my-bank")
|
||||
for op in response.items:
|
||||
if op.status == "pending":
|
||||
api.cancel_operation(bank_id="my-bank", operation_id=op.id)
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
|
||||
```typescript
|
||||
// Cancel operation
|
||||
await sdk.cancelOperation({
|
||||
client: apiClient,
|
||||
path: { bank_id: 'my-bank', operation_id: 'op-abc123' }
|
||||
});
|
||||
|
||||
// Cancel all pending
|
||||
const ops = await sdk.listOperations({
|
||||
client: apiClient,
|
||||
path: { bank_id: 'my-bank' }
|
||||
});
|
||||
|
||||
for (const op of ops.data.items) {
|
||||
if (op.status === 'pending') {
|
||||
await sdk.cancelOperation({
|
||||
client: apiClient,
|
||||
path: { bank_id: 'my-bank', operation_id: op.id }
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="cli" label="CLI">
|
||||
|
||||
```bash
|
||||
# Cancel operation
|
||||
hindsight operations cancel my-bank op-abc123
|
||||
|
||||
# Cancel all pending
|
||||
hindsight operations cancel my-bank --all-pending
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Operation States
|
||||
|
||||
| State | Description |
|
||||
|-------|-------------|
|
||||
| **pending** | Queued, waiting to start |
|
||||
| **running** | Currently processing |
|
||||
| **completed** | Successfully finished |
|
||||
| **failed** | Encountered an error |
|
||||
| **cancelled** | Stopped by user |
|
||||
|
||||
## Monitoring Strategies
|
||||
|
||||
### Polling
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
import time
|
||||
|
||||
def wait_for_operations(api, bank_id, poll_interval=5):
|
||||
"""Wait for all pending/running operations to complete."""
|
||||
while True:
|
||||
response = api.list_operations(bank_id=bank_id)
|
||||
|
||||
pending_or_running = [
|
||||
op for op in response.items
|
||||
if op.status in ['pending', 'running']
|
||||
]
|
||||
|
||||
if not pending_or_running:
|
||||
print("All operations completed!")
|
||||
break
|
||||
|
||||
for op in pending_or_running:
|
||||
print(f" {op.id}: {op.status} ({op.items_count} items)")
|
||||
|
||||
time.sleep(poll_interval)
|
||||
|
||||
# Use it
|
||||
wait_for_operations(api, "my-bank")
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
|
||||
```typescript
|
||||
async function waitForOperations(apiClient: any, bankId: string, pollInterval = 5000) {
|
||||
while (true) {
|
||||
const response = await sdk.listOperations({
|
||||
client: apiClient,
|
||||
path: { bank_id: bankId }
|
||||
});
|
||||
|
||||
const pendingOrRunning = response.data.items.filter(
|
||||
(op: any) => ['pending', 'running'].includes(op.status)
|
||||
);
|
||||
|
||||
if (pendingOrRunning.length === 0) {
|
||||
console.log('All operations completed!');
|
||||
break;
|
||||
}
|
||||
|
||||
for (const op of pendingOrRunning) {
|
||||
console.log(` ${op.id}: ${op.status} (${op.items_count} items)`);
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
||||
}
|
||||
}
|
||||
|
||||
// Use it
|
||||
await waitForOperations(apiClient, 'my-bank');
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Performance Tips
|
||||
|
||||
**Use async for large batches:**
|
||||
- Sync: < 100 items or < 100KB
|
||||
- Async: > 100 items or > 100KB
|
||||
|
||||
**Monitor progress:**
|
||||
- Check `items_count` field
|
||||
- Poll every 5-10 seconds
|
||||
|
||||
**Handle failures:**
|
||||
- Check `error_message` field for details
|
||||
- Retry with exponential backoff
|
||||
- Break large batches into smaller chunks
|
||||
## Operation Types
|
||||
|
||||
| Operation | Trigger | Description |
|
||||
|-----------|---------|-------------|
|
||||
| **batch_retain** | `retain_batch` with `async=True` | Processes large content batches in the background |
|
||||
| **form_opinion** | After each `reflect` call | Extracts and stores new opinions formed during reflection |
|
||||
| **reinforce_opinion** | After `retain` | Updates opinion confidence based on new supporting evidence |
|
||||
| **access_count_update** | After `recall` | Tracks which memories are accessed for relevance scoring |
|
||||
| **regenerate_observations** | Bank profile update | Regenerates entity observations when disposition changes |
|
||||
|
||||
## Next Steps
|
||||
|
||||
|
||||
@@ -31,10 +31,10 @@ API available at http://localhost:8888
|
||||
|
||||
export OPENAI_API_KEY=sk-xxx
|
||||
|
||||
docker run -it -p 8888:8888 -p 9999:9999 \
|
||||
docker run --rm -it --pull always -p 8888:8888 -p 9999:9999 \
|
||||
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
|
||||
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
|
||||
ghcr.io/vectorize-io/hindsight
|
||||
ghcr.io/vectorize-io/hindsight:latest
|
||||
```
|
||||
|
||||
- **API**: http://localhost:8888
|
||||
@@ -100,7 +100,7 @@ await client.reflect('my-bank', 'Tell me about Alice');
|
||||
<TabItem value="cli" label="CLI">
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/vectorize-io/hindsight/refs/heads/main/hindsight-cli/install.sh | bash
|
||||
curl -fsSL https://hindsight.vectorize.io/get-cli | bash
|
||||
```
|
||||
|
||||
```bash
|
||||
|
||||
@@ -2,18 +2,22 @@
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
# Search Facts
|
||||
# Recall Memories
|
||||
|
||||
Retrieve memories using multi-strategy search.
|
||||
Retrieve memories using multi-strategy recall.
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
:::info How Recall Works
|
||||
Learn about the four retrieval strategies (semantic, keyword, graph, temporal) and RRF fusion in the [Recall Architecture](/developer/retrieval) guide.
|
||||
:::
|
||||
|
||||
:::tip Prerequisites
|
||||
Make sure you've completed the [Quick Start](./quickstart) to install the client and start the server.
|
||||
:::
|
||||
|
||||
## Basic Search
|
||||
## Basic Recall
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
@@ -23,7 +27,9 @@ from hindsight_client import Hindsight
|
||||
|
||||
client = Hindsight(base_url="http://localhost:8888")
|
||||
|
||||
client.recall(bank_id="my-bank", query="What does Alice do?")
|
||||
response = client.recall(bank_id="my-bank", query="What does Alice do?")
|
||||
for r in response.results:
|
||||
print(f"{r.text} (score: {r.weight:.2f})")
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
@@ -34,20 +40,23 @@ import { HindsightClient } from '@vectorize-io/hindsight-client';
|
||||
|
||||
const client = new HindsightClient({ baseUrl: 'http://localhost:8888' });
|
||||
|
||||
await client.recall('my-bank', 'What does Alice do?');
|
||||
const response = await client.recall('my-bank', 'What does Alice do?');
|
||||
for (const r of response.results) {
|
||||
console.log(`${r.text} (score: ${r.weight})`);
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="cli" label="CLI">
|
||||
|
||||
```bash
|
||||
hindsight memory search my-bank "What does Alice do?"
|
||||
hindsight recall my-bank "What does Alice do?"
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Search Parameters
|
||||
## Recall Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
@@ -55,43 +64,15 @@ hindsight memory search my-bank "What does Alice do?"
|
||||
| `types` | list | all | Filter: `world`, `experience`, `opinion` |
|
||||
| `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_entities` | bool | false | Include entity observations |
|
||||
| `max_entity_tokens` | int | 500 | Token budget for entity observations |
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
results = client.recall(
|
||||
bank_id="my-bank",
|
||||
query="What does Alice do?",
|
||||
types=["world", "experience"],
|
||||
budget="high",
|
||||
max_tokens=8000
|
||||
)
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
|
||||
```typescript
|
||||
const results = await client.recall('my-bank', 'What does Alice do?', {
|
||||
budget: 'high',
|
||||
maxTokens: 8000
|
||||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Full-Featured Search
|
||||
|
||||
For more control, use the full-featured recall method:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
# Full response with trace info
|
||||
response = client.recall_memories(
|
||||
response = client.recall(
|
||||
bank_id="my-bank",
|
||||
query="What does Alice do?",
|
||||
types=["world", "experience"],
|
||||
@@ -103,22 +84,20 @@ response = client.recall_memories(
|
||||
)
|
||||
|
||||
# Access results
|
||||
for r in response["results"]:
|
||||
print(f"{r['text']} (score: {r['weight']:.2f})")
|
||||
for r in response.results:
|
||||
print(f"{r.text} (score: {r.weight:.2f})")
|
||||
|
||||
# Access entity observations (if include_entities=True)
|
||||
if "entities" in response:
|
||||
for entity in response["entities"]:
|
||||
print(f"Entity: {entity['name']}")
|
||||
if response.entities:
|
||||
for entity in response.entities:
|
||||
print(f"Entity: {entity.name}")
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
|
||||
```typescript
|
||||
// Full response with trace info
|
||||
const response = await client.recallMemories('my-bank', {
|
||||
query: 'What does Alice do?',
|
||||
const response = await client.recall('my-bank', 'What does Alice do?', {
|
||||
types: ['world', 'experience'],
|
||||
budget: 'high',
|
||||
maxTokens: 8000,
|
||||
@@ -134,44 +113,9 @@ for (const r of response.results) {
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Temporal Queries
|
||||
|
||||
Hindsight automatically detects time expressions and activates temporal search:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
# These queries activate temporal-graph retrieval
|
||||
results = client.recall(bank_id="my-bank", query="What did Alice do last spring?")
|
||||
results = client.recall(bank_id="my-bank", query="What happened in June?")
|
||||
results = client.recall(bank_id="my-bank", query="Events from last year")
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="cli" label="CLI">
|
||||
|
||||
```bash
|
||||
hindsight memory search my-bank "What did Alice do last spring?"
|
||||
hindsight memory search my-bank "What happened between March and May?"
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Supported temporal expressions:
|
||||
|
||||
| Expression | Parsed As |
|
||||
|------------|-----------|
|
||||
| "last spring" | March 1 - May 31 (previous year) |
|
||||
| "in June" | June 1-30 (current/nearest year) |
|
||||
| "last year" | Jan 1 - Dec 31 (previous year) |
|
||||
| "last week" | 7 days ago - today |
|
||||
| "between March and May" | March 1 - May 31 |
|
||||
|
||||
## Filter by Fact Type
|
||||
|
||||
Search specific memory networks:
|
||||
Recall specific memory types:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
@@ -210,20 +154,23 @@ facts = client.recall(
|
||||
<TabItem value="cli" label="CLI">
|
||||
|
||||
```bash
|
||||
hindsight memory search my-bank "Python" --fact-type opinion
|
||||
hindsight memory search my-bank "Alice" --fact-type world,experience
|
||||
hindsight recall my-bank "Python" --fact-type opinion
|
||||
hindsight recall my-bank "Alice" --fact-type world,experience
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
:::info How Recall Works
|
||||
Learn about the four search strategies (semantic, keyword, graph, temporal) and RRF fusion in the [Recall Architecture](/developer/retrieval) guide.
|
||||
:::warning About Opinions
|
||||
Opinions are beliefs formed during [reflect](/developer/api/reflect) operations. Unlike world facts and experience, opinions are subjective interpretations and may not represent objective truth. Depending on your use case:
|
||||
- **Exclude opinions** (`types=["world", "experience"]`) when you need factual, verifiable information
|
||||
- **Include opinions** when you want the agent's perspective or formed beliefs
|
||||
- **Use opinions alone** (`types=["opinion"]`) only when specifically asking about the agent's views
|
||||
:::
|
||||
|
||||
## 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.
|
||||
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:
|
||||
|
||||
@@ -237,9 +184,9 @@ 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.
|
||||
|
||||
### Additional Context: Chunks and Entity Observations
|
||||
## Include Related Context
|
||||
|
||||
For the most relevant memories, you can optionally retrieve additional context—each with its own token budget:
|
||||
Beyond the core memory results, you can optionally retrieve additional context—each with its own token budget:
|
||||
|
||||
| Option | Parameter | Description |
|
||||
|--------|-----------|-------------|
|
||||
@@ -247,19 +194,16 @@ For the most relevant memories, you can optionally retrieve additional context
|
||||
| **Entity Observations** | `include_entities`, `max_entity_tokens` | Related observations about entities mentioned in results |
|
||||
|
||||
```python
|
||||
response = client.recall_memories(
|
||||
response = client.recall(
|
||||
bank_id="my-bank",
|
||||
query="What does Alice do?",
|
||||
max_tokens=4096, # Budget for memories
|
||||
include_chunks=True,
|
||||
max_chunk_tokens=2000, # Budget for raw chunks
|
||||
include_entities=True,
|
||||
max_entity_tokens=1000 # Budget for entity observations
|
||||
)
|
||||
|
||||
# Access the additional context
|
||||
chunks = response.get("chunks", {})
|
||||
entities = response.get("entities", [])
|
||||
entities = response.entities or []
|
||||
```
|
||||
|
||||
This gives your agent richer context while maintaining precise control over total token consumption.
|
||||
@@ -268,7 +212,7 @@ This gives your agent richer context while maintaining precise control over tota
|
||||
|
||||
The `budget` parameter controls graph traversal depth:
|
||||
|
||||
- **"low"**: Fast, shallow search — good for simple lookups
|
||||
- **"low"**: Fast, shallow retrieval — good for simple lookups
|
||||
- **"mid"**: Balanced — default for most queries
|
||||
- **"high"**: Deep exploration — finds indirect connections
|
||||
|
||||
|
||||
@@ -6,9 +6,21 @@ sidebar_position: 3
|
||||
|
||||
Generate disposition-aware responses using retrieved memories.
|
||||
|
||||
When you call **reflect**, Hindsight performs a multi-step reasoning process:
|
||||
1. **Recalls** relevant memories from the bank based on your query
|
||||
2. **Applies** the bank's disposition traits to shape the reasoning style
|
||||
3. **Generates** a contextual answer grounded in the retrieved facts
|
||||
4. **Forms opinions** in the background based on the reasoning (available in subsequent calls)
|
||||
|
||||
The response includes the generated answer along with the facts that were used, providing full transparency into how the answer was derived.
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
:::info How Reflect Works
|
||||
Learn about disposition-driven reasoning and opinion formation in the [Reflect Architecture](/developer/reflect) guide.
|
||||
:::
|
||||
|
||||
:::tip Prerequisites
|
||||
Make sure you've completed the [Quick Start](./quickstart) to install the client and start the server.
|
||||
:::
|
||||
@@ -80,34 +92,50 @@ const response = await client.reflect('my-bank', 'What do you think about remote
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
:::info How Reflect Works
|
||||
Learn about disposition-driven reasoning and opinion formation in the [Reflect Architecture](/developer/reflect) guide.
|
||||
:::
|
||||
## The Role of Context
|
||||
|
||||
## Opinion Formation
|
||||
The `context` parameter steers how the reflection is performed without impacting the memory recall. It provides situational information that helps shape the reasoning and response.
|
||||
|
||||
Reflect can form new opinions based on evidence:
|
||||
**How context is used:**
|
||||
- **Shapes reasoning**: Helps understand the situation when formulating an answer
|
||||
- **Disambiguates intent**: Clarifies what aspect of the query matters most
|
||||
- **Does not affect recall**: The same memories are retrieved regardless of context
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
# Context is passed to the LLM to help it understand the situation
|
||||
response = client.reflect(
|
||||
bank_id="my-bank",
|
||||
query="What do you think about Python vs JavaScript for data science?"
|
||||
query="What do you think about the proposal?",
|
||||
context="We're in a budget review meeting discussing Q4 spending"
|
||||
)
|
||||
```
|
||||
|
||||
# Response might include:
|
||||
# answer: "Based on what I know about data science workflows..."
|
||||
# new_opinions: [
|
||||
# {"text": "Python is better for data science", "id": "..."}
|
||||
# ]
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
|
||||
```typescript
|
||||
// Context helps the LLM understand the current situation
|
||||
const response = await client.reflect('my-bank', 'What do you think about the proposal?', {
|
||||
context: "We're in a budget review meeting discussing Q4 spending"
|
||||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
New opinions are automatically stored and influence future responses.
|
||||
## Opinion Formation
|
||||
|
||||
When reflect reasons about a question, it may form new **opinions** based on the evidence in the memory bank. These opinions are created in the background and become available in subsequent `reflect` and `recall` calls.
|
||||
|
||||
**Why opinions matter:**
|
||||
- **Consistent thinking**: Opinions ensure the memory bank maintains a coherent perspective over time
|
||||
- **Evolving viewpoints**: As more information is retained, opinions can be refined or updated
|
||||
- **Grounded reasoning**: Opinions are always derived from factual evidence in the memory bank
|
||||
|
||||
Opinions are stored as a special memory type and are automatically retrieved when relevant to future queries. This creates a natural evolution of the bank's perspective, similar to how humans form and refine their views based on accumulated experience.
|
||||
|
||||
## Disposition Influence
|
||||
|
||||
@@ -165,7 +193,7 @@ const response = await client.reflect('cautious-advisor', 'Should I invest in cr
|
||||
|
||||
## Using Sources
|
||||
|
||||
The `facts_used` field shows which memories informed the response:
|
||||
The `based_on` field shows which memories informed the response:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
@@ -173,10 +201,10 @@ The `facts_used` field shows which memories informed the response:
|
||||
```python
|
||||
response = client.reflect(bank_id="my-bank", query="Tell me about Alice")
|
||||
|
||||
print("Response:", response["answer"])
|
||||
print("Response:", response.text)
|
||||
print("\nBased on:")
|
||||
for fact in response.get("facts_used", []):
|
||||
print(f" - {fact['text']} (relevance: {fact['weight']:.2f})")
|
||||
for fact in response.based_on or []:
|
||||
print(f" - [{fact.type}] {fact.text}")
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
@@ -185,10 +213,10 @@ for fact in response.get("facts_used", []):
|
||||
```typescript
|
||||
const response = await client.reflect('my-bank', 'Tell me about Alice');
|
||||
|
||||
console.log('Response:', response.answer);
|
||||
console.log('Response:', response.text);
|
||||
console.log('\nBased on:');
|
||||
for (const fact of response.facts_used || []) {
|
||||
console.log(` - ${fact.text} (relevance: ${fact.weight.toFixed(2)})`);
|
||||
for (const fact of response.based_on || []) {
|
||||
console.log(` - [${fact.type}] ${fact.text}`);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -4,11 +4,17 @@ sidebar_position: 2
|
||||
|
||||
# Ingest Data
|
||||
|
||||
Store memories, conversations, and documents into Hindsight.
|
||||
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 Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
:::info How Retain Works
|
||||
Learn about fact extraction, entity resolution, and graph construction in the [Retain Architecture](/developer/retain) guide.
|
||||
:::
|
||||
|
||||
:::tip Prerequisites
|
||||
Make sure you've completed the [Quick Start](./quickstart) to install the client and start the server.
|
||||
:::
|
||||
@@ -50,9 +56,18 @@ hindsight memory put my-bank "Alice works at Google as a software engineer"
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## 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
|
||||
|
||||
Add context and event dates for better retrieval:
|
||||
Always provide context and event dates for optimal memory extraction:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
@@ -88,11 +103,11 @@ hindsight memory put my-bank "Alice got promoted" \
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
The `timestamp` enables temporal queries like "What happened last spring?"
|
||||
The `timestamp` defaults to the current time if not specified. Providing explicit timestamps enables temporal queries like "What happened last spring?"
|
||||
|
||||
## Batch Ingestion
|
||||
|
||||
Store multiple memories in a single request:
|
||||
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.
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
@@ -144,51 +159,40 @@ hindsight memory put-files my-bank report.pdf --document-id "q4-report"
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
:::info How Retain Works
|
||||
Learn about fact extraction, entity resolution, and graph construction in the [Retain Architecture](/developer/retain) guide.
|
||||
:::
|
||||
|
||||
## Async Ingestion
|
||||
|
||||
For large batches, use async ingestion:
|
||||
For large batches, use async ingestion to avoid blocking:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
# Start async ingestion
|
||||
# Start async ingestion (returns immediately)
|
||||
result = client.retain_batch(
|
||||
bank_id="my-bank",
|
||||
items=[...large batch...],
|
||||
document_id="large-doc",
|
||||
async_=True
|
||||
retain_async=True
|
||||
)
|
||||
|
||||
# Result contains operation_id for tracking
|
||||
print(result["operation_id"])
|
||||
# Check if it was processed asynchronously
|
||||
print(result.var_async) # True
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
|
||||
```typescript
|
||||
// Start async ingestion
|
||||
// Start async ingestion (returns immediately)
|
||||
const result = await client.retainBatch('my-bank', largeItems, {
|
||||
documentId: 'large-doc',
|
||||
async: true
|
||||
});
|
||||
|
||||
console.log(result.operation_id);
|
||||
console.log(result.async); // true
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Best Practices
|
||||
|
||||
| Do | Don't |
|
||||
|----|-------|
|
||||
| Include context for better retrieval | Store raw unstructured dumps |
|
||||
| Use document_id to group related content | Mix unrelated content in one batch |
|
||||
| Add timestamp for temporal queries | Omit dates if time matters |
|
||||
| Store conversations as they happen | Wait to batch everything |
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
---
|
||||
|
||||
# Think vs Search
|
||||
|
||||
When to use `search` vs `think`.
|
||||
|
||||
## Quick Comparison
|
||||
|
||||
| | Search | Think |
|
||||
|---|--------|-------|
|
||||
| **Returns** | Raw memory results | Generated response |
|
||||
| **Use case** | Retrieval, lookup | Q&A, reasoning |
|
||||
| **LLM calls** | 0 (retrieval only) | 1+ (generation) |
|
||||
| **Speed** | Fast (~100-200ms) | Slower (~500-2000ms) |
|
||||
| **Opinions** | Returns existing | Can form new ones |
|
||||
| **Disposition** | Not applied | Applied to response |
|
||||
|
||||
## When to Use Search
|
||||
|
||||
**Use Search when you need:**
|
||||
|
||||
- Raw facts for your own processing
|
||||
- Fast retrieval without generation
|
||||
- To populate context for another LLM
|
||||
- To check what's in memory
|
||||
- Debugging retrieval quality
|
||||
|
||||
```python
|
||||
# Get raw facts to inject into your own prompt
|
||||
results = client.search(agent_id="my-agent", query="Alice's preferences")
|
||||
|
||||
context = "\n".join([r["text"] for r in results])
|
||||
# Use context in your own LLM call
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
|
||||
```python
|
||||
# Lookup — just get the facts
|
||||
results = client.search(agent_id="my-agent", query="Alice's email address")
|
||||
|
||||
# Context building — feed into another system
|
||||
results = client.search(agent_id="my-agent", query="Recent project discussions")
|
||||
context = format_for_prompt(results)
|
||||
|
||||
# Verification — check what's stored
|
||||
results = client.search(agent_id="my-agent", query="What do I know about Bob?")
|
||||
```
|
||||
|
||||
## When to Use Think
|
||||
|
||||
**Use Think when you need:**
|
||||
|
||||
- A natural language response
|
||||
- Disposition-aware answers
|
||||
- Opinion formation
|
||||
- Reasoning over multiple facts
|
||||
- Source attribution
|
||||
|
||||
```python
|
||||
# Get a complete answer with disposition
|
||||
answer = client.think(agent_id="my-agent", query="What should I recommend to Alice?")
|
||||
print(answer["text"]) # Natural language response
|
||||
print(answer["based_on"]) # Sources used
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
|
||||
```python
|
||||
# Q&A — need a response, not just facts
|
||||
answer = client.think(agent_id="my-agent", query="What does Alice do for work?")
|
||||
|
||||
# Reasoning — synthesize multiple facts
|
||||
answer = client.think(agent_id="my-agent", query="How are Alice and Bob connected?")
|
||||
|
||||
# Opinion — agent forms a view
|
||||
answer = client.think(agent_id="my-agent", query="What do you think about Python?")
|
||||
|
||||
# Recommendation — disposition-influenced
|
||||
answer = client.think(agent_id="my-agent", query="What book should I read next?")
|
||||
```
|
||||
|
||||
## Performance Comparison
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
subgraph Search
|
||||
S1[Query] --> S2[4-way Retrieval]
|
||||
S2 --> S3[RRF + Rerank]
|
||||
S3 --> S4[Results]
|
||||
end
|
||||
|
||||
subgraph Think
|
||||
T1[Query] --> T2[4-way Retrieval]
|
||||
T2 --> T3[RRF + Rerank]
|
||||
T3 --> T4[Load Disposition]
|
||||
T4 --> T5[LLM Generation]
|
||||
T5 --> T6[Store Opinions]
|
||||
T6 --> T7[Response]
|
||||
end
|
||||
```
|
||||
|
||||
| Operation | Search | Think |
|
||||
|-----------|--------|-------|
|
||||
| Retrieval | ~100ms | ~100ms |
|
||||
| Reranking | ~35ms | ~35ms |
|
||||
| LLM Generation | — | ~500-1500ms |
|
||||
| Opinion Storage | — | ~50ms |
|
||||
| **Total** | **~135ms** | **~700-1700ms** |
|
||||
|
||||
## Hybrid Pattern
|
||||
|
||||
Use Search for context, Think for final response:
|
||||
|
||||
```python
|
||||
# First: fast search to check relevance
|
||||
results = client.search(agent_id="my-agent", query="Alice project status")
|
||||
|
||||
if len(results) > 0:
|
||||
# Only call Think if we have relevant memories
|
||||
answer = client.think(agent_id="my-agent", query="Summarize Alice's project status")
|
||||
else:
|
||||
answer = {"text": "I don't have information about Alice's projects."}
|
||||
```
|
||||
|
||||
## Decision Flowchart
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Need memory access] --> B{Need natural language response?}
|
||||
B -->|No| C[Use Search]
|
||||
B -->|Yes| D{Need disposition/opinions?}
|
||||
D -->|No| E{Building context for another LLM?}
|
||||
E -->|Yes| C
|
||||
E -->|No| F[Use Think]
|
||||
D -->|Yes| F
|
||||
```
|
||||
|
||||
## Cost Considerations
|
||||
|
||||
| Factor | Search | Think |
|
||||
|--------|--------|-------|
|
||||
| API calls | 1 | 1 |
|
||||
| LLM tokens | 0 | 500-2000 |
|
||||
| Latency | Low | Medium |
|
||||
| Cost | Low | Higher (LLM usage) |
|
||||
|
||||
If you're making many requests or building a high-throughput system, consider:
|
||||
- Use Search for bulk operations
|
||||
- Use Think for user-facing responses
|
||||
- Cache Think responses when appropriate
|
||||
@@ -24,7 +24,7 @@ Configure the LLM provider used for fact extraction, entity resolution, and reas
|
||||
|
||||
| Variable | Description | Default | Required |
|
||||
|----------|-------------|---------|----------|
|
||||
| `HINDSIGHT_API_LLM_PROVIDER` | LLM provider: `groq`, `openai`, `ollama` | `groq` | Yes |
|
||||
| `HINDSIGHT_API_LLM_PROVIDER` | LLM provider: `groq`, `openai`, `gemini`, `ollama` | `groq` | Yes |
|
||||
| `HINDSIGHT_API_LLM_API_KEY` | API key for LLM provider | - | Yes (except ollama) |
|
||||
| `HINDSIGHT_API_LLM_MODEL` | Model name | Provider-specific | No |
|
||||
| `HINDSIGHT_API_LLM_BASE_URL` | Custom LLM endpoint | Provider default | No |
|
||||
@@ -47,6 +47,14 @@ export HINDSIGHT_API_LLM_API_KEY=sk-xxxxxxxxxxxx
|
||||
export HINDSIGHT_API_LLM_MODEL=gpt-4o
|
||||
```
|
||||
|
||||
**Gemini**
|
||||
|
||||
```bash
|
||||
export HINDSIGHT_API_LLM_PROVIDER=gemini
|
||||
export HINDSIGHT_API_LLM_API_KEY=xxxxxxxxxxxx
|
||||
export HINDSIGHT_API_LLM_MODEL=gemini-2.0-flash
|
||||
```
|
||||
|
||||
**Ollama (Local, No API Key)**
|
||||
|
||||
```bash
|
||||
|
||||
@@ -37,11 +37,12 @@ See [Models](./models) for detailed comparison and configuration.
|
||||
Run everything in one container with embedded PostgreSQL:
|
||||
|
||||
```bash
|
||||
docker run -p 8888:8888 -p 9999:9999 \
|
||||
-e HINDSIGHT_API_LLM_PROVIDER=openai \
|
||||
-e HINDSIGHT_API_LLM_API_KEY=sk-xxxxxxxxxxxx \
|
||||
-e HINDSIGHT_API_LLM_MODEL=gpt-4o-mini \
|
||||
ghcr.io/vectorize-io/hindsight
|
||||
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
|
||||
@@ -54,27 +55,29 @@ docker run -p 8888:8888 -p 9999:9999 \
|
||||
**Best for**: Production deployments, auto-scaling, cloud environments
|
||||
|
||||
```bash
|
||||
# Add Hindsight Helm repository
|
||||
helm repo add hindsight https://vectorize-io.github.io/hindsight
|
||||
helm repo update
|
||||
|
||||
# Install with built-in PostgreSQL
|
||||
helm install hindsight hindsight/hindsight \
|
||||
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 hindsight/hindsight \
|
||||
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+
|
||||
- Helm 3.8+
|
||||
|
||||
See the [Helm chart documentation](https://github.com/vectorize-io/hindsight/tree/main/helm) for advanced configuration.
|
||||
|
||||
|
||||
@@ -8,37 +8,50 @@ Hindsight includes a built-in [Model Context Protocol (MCP)](https://modelcontex
|
||||
|
||||
## Access
|
||||
|
||||
The MCP server is **enabled by default** and mounted at `/mcp` on the API server:
|
||||
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
|
||||
http://localhost:8888/mcp/{bank_id}/
|
||||
```
|
||||
|
||||
To disable it, set the environment variable:
|
||||
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
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
### hindsight_put
|
||||
### retain
|
||||
|
||||
Store information to a user's memory bank.
|
||||
Store information to long-term memory.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `bank_id` | string | Yes | Unique identifier for the user (e.g., `user_12345`, `[email protected]`) |
|
||||
| `content` | string | Yes | The fact or memory to store |
|
||||
| `context` | string | Yes | Category for the memory (e.g., `personal_preferences`, `work_history`) |
|
||||
| `explanation` | string | No | Why this memory is being stored |
|
||||
| `context` | string | No | Category for the memory (default: `general`) |
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"name": "hindsight_put",
|
||||
"name": "retain",
|
||||
"arguments": {
|
||||
"bank_id": "user_12345",
|
||||
"content": "User prefers Python over JavaScript for backend development",
|
||||
"context": "programming_preferences"
|
||||
}
|
||||
@@ -53,23 +66,20 @@ Store information to a user's memory bank.
|
||||
|
||||
---
|
||||
|
||||
### hindsight_search
|
||||
### recall
|
||||
|
||||
Search a user's memory bank to provide personalized responses.
|
||||
Search memories to provide personalized responses.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `bank_id` | string | Yes | Unique identifier for the user |
|
||||
| `query` | string | Yes | Natural language search query |
|
||||
| `max_tokens` | integer | No | Maximum tokens for results (default: 4096) |
|
||||
| `explanation` | string | No | Why this search is being performed |
|
||||
| `max_results` | integer | No | Maximum results to return (default: 10) |
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"name": "hindsight_search",
|
||||
"name": "recall",
|
||||
"arguments": {
|
||||
"bank_id": "user_12345",
|
||||
"query": "What are the user's programming language preferences?"
|
||||
}
|
||||
}
|
||||
@@ -84,8 +94,7 @@ Search a user's memory bank to provide personalized responses.
|
||||
"text": "User prefers Python over JavaScript for backend development",
|
||||
"type": "world",
|
||||
"context": "programming_preferences",
|
||||
"event_date": null,
|
||||
"document_id": null
|
||||
"event_date": null
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -99,17 +108,22 @@ Search a user's memory bank to provide personalized responses.
|
||||
|
||||
---
|
||||
|
||||
## Per-User Isolation
|
||||
|
||||
Both tools require a `bank_id` that uniquely identifies the user. Memories are strictly isolated per bank — one user cannot access another user's memories.
|
||||
|
||||
**Best practices:**
|
||||
- Use consistent identifiers (user ID, email, session ID)
|
||||
- Don't share `bank_id` between different users
|
||||
- Only call these tools when you can identify the specific user
|
||||
|
||||
---
|
||||
|
||||
## Integration with AI Assistants
|
||||
|
||||
The MCP server can be used with any MCP-compatible AI assistant. For Claude Desktop integration using the CLI, see [MCP Server (CLI)](/sdks/mcp).
|
||||
The MCP server can be used with any MCP-compatible AI assistant.
|
||||
|
||||
### Claude Desktop Configuration
|
||||
|
||||
To connect Claude Desktop to a specific memory bank:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"hindsight-alice": {
|
||||
"url": "http://localhost:8888/mcp/alice/"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Each user can have their own MCP server configuration pointing to their personal memory bank.
|
||||
|
||||
@@ -8,45 +8,21 @@ curl http://localhost:8888/metrics
|
||||
|
||||
## Available Metrics
|
||||
|
||||
### Request Metrics
|
||||
### Operation Metrics
|
||||
|
||||
| Metric | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `hindsight_http_requests_total` | Counter | Total HTTP requests (labels: method, endpoint, status_code) |
|
||||
| `hindsight_http_request_duration_seconds` | Histogram | Request latency (labels: method, endpoint) |
|
||||
| Metric | Type | Labels | Description |
|
||||
|--------|------|--------|-------------|
|
||||
| `hindsight.operation.duration` | Histogram | operation, bank_id, budget, max_tokens, success | Duration of operations in seconds |
|
||||
| `hindsight.operation.total` | Counter | operation, bank_id, budget, max_tokens, success | Total number of operations executed |
|
||||
|
||||
### Memory Operations
|
||||
The `operation` label values are: `retain`, `recall`, `reflect`.
|
||||
|
||||
| Metric | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `hindsight_retain_duration_seconds` | Histogram | Retain operation latency |
|
||||
| `hindsight_retain_items_total` | Counter | Total items retained |
|
||||
| `hindsight_recall_duration_seconds` | Histogram | Recall operation latency |
|
||||
| `hindsight_recall_results_count` | Histogram | Number of results per recall |
|
||||
| `hindsight_reflect_duration_seconds` | Histogram | Reflect operation latency |
|
||||
### Token Metrics
|
||||
|
||||
### LLM Metrics
|
||||
|
||||
| Metric | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `hindsight_llm_requests_total` | Counter | LLM API requests (labels: provider, model, status) |
|
||||
| `hindsight_llm_request_duration_seconds` | Histogram | LLM request latency |
|
||||
| `hindsight_llm_tokens_total` | Counter | Tokens consumed (labels: provider, token_type) |
|
||||
|
||||
### Database Metrics
|
||||
|
||||
| Metric | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `hindsight_db_connections_active` | Gauge | Active database connections |
|
||||
| `hindsight_db_connections_idle` | Gauge | Idle connections in pool |
|
||||
| `hindsight_db_query_duration_seconds` | Histogram | Query latency (labels: query_type) |
|
||||
|
||||
### Memory Bank Metrics
|
||||
|
||||
| Metric | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `hindsight_bank_memory_units_total` | Gauge | Total memories per bank |
|
||||
| `hindsight_bank_entities_total` | Gauge | Total entities per bank |
|
||||
| Metric | Type | Labels | Description |
|
||||
|--------|------|--------|-------------|
|
||||
| `hindsight.tokens.input` | Counter | operation, bank_id, budget, max_tokens | Input tokens consumed |
|
||||
| `hindsight.tokens.output` | Counter | operation, bank_id, budget, max_tokens | Output tokens generated |
|
||||
|
||||
## Prometheus Configuration
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ Hindsight uses several machine learning models for different tasks.
|
||||
| Model Type | Purpose | Default | Configurable |
|
||||
|------------|---------|---------|--------------|
|
||||
| **Embedding** | Vector representations for semantic search | `BAAI/bge-small-en-v1.5` | Yes |
|
||||
| **Cross-Encoder** | Reranking search results | `ms-marco-MiniLM-L-6-v2` | Yes |
|
||||
| **Cross-Encoder** | Reranking search results | `cross-encoder/ms-marco-MiniLM-L-6-v2` | Yes |
|
||||
| **LLM** | Fact extraction, reasoning, generation | Provider-specific | Yes |
|
||||
|
||||
All local models (embedding, cross-encoder) are automatically downloaded from HuggingFace on first run.
|
||||
@@ -28,12 +28,20 @@ Converts text into dense vector representations for semantic similarity search.
|
||||
| `BAAI/bge-base-en-v1.5` | 768 | Higher accuracy, slower |
|
||||
| `sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2` | 384 | Multilingual (50+ languages) |
|
||||
|
||||
:::warning
|
||||
All embedding models must produce 384-dimensional vectors to match the database schema.
|
||||
:::
|
||||
|
||||
**Configuration:**
|
||||
|
||||
```bash
|
||||
export HINDSIGHT_API_EMBEDDING_MODEL=BAAI/bge-base-en-v1.5
|
||||
export HINDSIGHT_API_EMBEDDING_DEVICE=cuda # or mps for Apple Silicon
|
||||
export HINDSIGHT_API_EMBEDDING_BATCH_SIZE=64
|
||||
# Local provider (default)
|
||||
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=local
|
||||
export HINDSIGHT_API_EMBEDDINGS_LOCAL_MODEL=BAAI/bge-small-en-v1.5
|
||||
|
||||
# TEI provider (remote)
|
||||
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=tei
|
||||
export HINDSIGHT_API_EMBEDDINGS_TEI_URL=http://localhost:8080
|
||||
```
|
||||
|
||||
---
|
||||
@@ -48,16 +56,20 @@ Reranks initial search results to improve precision.
|
||||
|
||||
| Model | Use Case |
|
||||
|-------|----------|
|
||||
| `ms-marco-MiniLM-L-6-v2` | Default, fast |
|
||||
| `ms-marco-MiniLM-L-12-v2` | Higher accuracy |
|
||||
| `mmarco-mMiniLMv2-L12-H384-v1` | Multilingual |
|
||||
| `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 |
|
||||
|
||||
**Configuration:**
|
||||
|
||||
```bash
|
||||
export HINDSIGHT_API_RERANK_MODEL=cross-encoder/ms-marco-MiniLM-L-12-v2
|
||||
export HINDSIGHT_API_RERANK_TOP_K=50 # How many results to rerank
|
||||
export HINDSIGHT_API_RERANK_ENABLED=true # Set to false to disable
|
||||
# Local provider (default)
|
||||
export HINDSIGHT_API_RERANKER_PROVIDER=local
|
||||
export HINDSIGHT_API_RERANKER_LOCAL_MODEL=cross-encoder/ms-marco-MiniLM-L-6-v2
|
||||
|
||||
# TEI provider (remote)
|
||||
export HINDSIGHT_API_RERANKER_PROVIDER=tei
|
||||
export HINDSIGHT_API_RERANKER_TEI_URL=http://localhost:8081
|
||||
```
|
||||
|
||||
---
|
||||
@@ -66,14 +78,14 @@ export HINDSIGHT_API_RERANK_ENABLED=true # Set to false to disable
|
||||
|
||||
Used for fact extraction, entity resolution, opinion generation, and answer synthesis.
|
||||
|
||||
**Supported providers:** Groq, OpenAI, Ollama, Gemini
|
||||
**Supported providers:** Groq, OpenAI, Gemini, Ollama
|
||||
|
||||
| Provider | Recommended Model | Best For |
|
||||
|----------|------------------|----------|
|
||||
| **Groq** | `openai/gpt-oss-20b` | Fast inference, high throughput (recommended) |
|
||||
| **OpenAI** | `gpt-5-mini` | Good quality |
|
||||
| **Gemini** | `gemini-2.5-flash` | Good quality |
|
||||
| **Ollama** | `gpt-oss-20b` | Local deployment, privacy |
|
||||
| **Groq** | `openai/gpt-oss-20b` | Fast inference, high throughput (recommended) |
|
||||
| **OpenAI** | `gpt-4o` | Good quality |
|
||||
| **Gemini** | `gemini-2.0-flash` | Good quality, cost effective |
|
||||
| **Ollama** | `llama3.1` | Local deployment, privacy |
|
||||
|
||||
**Configuration:**
|
||||
|
||||
@@ -86,28 +98,17 @@ 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-5-mini
|
||||
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.5-flash
|
||||
export HINDSIGHT_API_LLM_MODEL=gemini-2.0-flash
|
||||
|
||||
# Ollama (local)
|
||||
export HINDSIGHT_API_LLM_PROVIDER=ollama
|
||||
export HINDSIGHT_API_LLM_BASE_URL=http://localhost:11434/v1
|
||||
export HINDSIGHT_API_LLM_MODEL=gpt-oss-20b
|
||||
export HINDSIGHT_API_LLM_MODEL=llama3.1
|
||||
```
|
||||
|
||||
**Note:** The LLM is the primary bottleneck for write operations. See [Performance](./performance) for optimization strategies.
|
||||
|
||||
---
|
||||
|
||||
## Model Comparison
|
||||
|
||||
| Provider | Model | Speed | Quality | Cost |
|
||||
|----------|-------|-------|---------|------|
|
||||
| Groq | gpt-oss-20b | Fast | Good | Free tier |
|
||||
| OpenAI | gpt-4o-mini | Medium | Good | $0.15 / $0.60 per 1M tokens |
|
||||
| OpenAI | gpt-4o | Slower | Best | $2.50 / $10.00 per 1M tokens |
|
||||
| Ollama | llama3.1 | Varies | Good | Free (local) |
|
||||
**Note:** The LLM is the primary bottleneck for retain operations. See [Performance](./performance) for optimization strategies.
|
||||
|
||||
@@ -6,6 +6,17 @@ sidebar_position: 4
|
||||
|
||||
When you call `reflect()`, Hindsight doesn't just retrieve facts — it **reasons** about them through the lens of the bank's unique disposition, forming new opinions and generating contextual responses.
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[Query] --> B[Recall Memories]
|
||||
B --> C[Load Disposition]
|
||||
C --> D[Reason]
|
||||
D --> E[Form Opinions]
|
||||
E --> F[Response]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Why Reflect?
|
||||
|
||||
Most AI systems can retrieve facts, but they can't **reason** about them in a consistent way. Every response is generated fresh without a stable perspective or evolving beliefs.
|
||||
@@ -41,16 +52,6 @@ With reflect:
|
||||
|
||||
---
|
||||
|
||||
## The Reflect Process
|
||||
|
||||
1. **Recall** relevant memories based on the query
|
||||
2. **Load** the bank's disposition traits and background
|
||||
3. **Reason** about the memories through the disposition lens
|
||||
4. **Form** new opinions with confidence scores
|
||||
5. **Return** response, sources, and any new beliefs
|
||||
|
||||
---
|
||||
|
||||
## 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 forms opinions during `reflect()`:
|
||||
|
||||
@@ -8,16 +8,12 @@ When you call `retain()`, Hindsight transforms conversations and documents into
|
||||
|
||||
## What Retain Does
|
||||
|
||||
```
|
||||
Your Content
|
||||
↓
|
||||
Extract Rich Facts
|
||||
↓
|
||||
Identify Entities
|
||||
↓
|
||||
Build Connections
|
||||
↓
|
||||
Searchable Memory Bank
|
||||
```mermaid
|
||||
graph LR
|
||||
A[Your Content] --> B[Extract Facts]
|
||||
B --> C[Identify Entities]
|
||||
C --> D[Build Connections]
|
||||
D --> E[Memory Bank]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -6,6 +6,24 @@ sidebar_position: 3
|
||||
|
||||
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:
|
||||
@@ -117,19 +135,6 @@ This gives your agent richer context while maintaining precise control over tota
|
||||
|
||||
---
|
||||
|
||||
## How Recall Works
|
||||
|
||||
When you call `recall(query, bank_id)`:
|
||||
|
||||
1. **Parse** → Detect temporal expressions, understand intent
|
||||
2. **Search** → Run 4 strategies in parallel
|
||||
3. **Fuse** → Combine results, prioritizing consensus
|
||||
4. **Rerank** → Neural reranking for final relevance
|
||||
5. **Filter** → Select top memories within token budget
|
||||
6. **Return** → Ranked, relevant memories
|
||||
|
||||
---
|
||||
|
||||
## Tuning Recall: Quality vs Latency
|
||||
|
||||
Different use cases require different trade-offs between **recall quality** and **response speed**. Two parameters control this:
|
||||
|
||||
@@ -9,7 +9,7 @@ The Hindsight CLI provides command-line access to memory operations and bank man
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/vectorize-io/hindsight/refs/heads/main/hindsight-cli/install.sh | bash
|
||||
curl -fsSL https://hindsight.vectorize.io/get-cli | bash
|
||||
```
|
||||
|
||||
## Configuration
|
||||
@@ -179,12 +179,32 @@ hindsight memory recall <bank_id> "query" -o yaml
|
||||
|
||||
## Interactive Explorer
|
||||
|
||||
Launch the TUI explorer for visual navigation:
|
||||
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 opinions
|
||||
- **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
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
# LangGraph
|
||||
|
||||
Hindsight provides a `BaseStore` implementation for LangGraph's memory system.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
cd hindsight-langmem && uv pip install -e .
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```python
|
||||
from hindsight_langmem import HindsightStore
|
||||
|
||||
# Create store
|
||||
store = HindsightStore(
|
||||
base_url="http://localhost:8888",
|
||||
default_agent_id="my-agent",
|
||||
)
|
||||
|
||||
# Store data
|
||||
store.put(
|
||||
namespace=("user", "preferences"),
|
||||
key="language",
|
||||
value={"language": "Python", "reason": "data science"}
|
||||
)
|
||||
|
||||
# Retrieve data
|
||||
item = store.get(namespace=("user", "preferences"), key="language")
|
||||
print(item.value) # {"language": "Python", "reason": "data science"}
|
||||
|
||||
# Search
|
||||
results = store.search(
|
||||
namespace_prefix=("user",),
|
||||
query="programming language",
|
||||
limit=10
|
||||
)
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
`HindsightStore` implements LangGraph's `BaseStore` interface:
|
||||
|
||||
- **Namespaces** map to Hindsight agent IDs (joined with `__`)
|
||||
- **Keys** map to document IDs
|
||||
- **Values** are stored as JSON in memory content
|
||||
|
||||
## BaseStore Interface
|
||||
|
||||
### put
|
||||
|
||||
Store an item:
|
||||
|
||||
```python
|
||||
store.put(
|
||||
namespace=("user", "session-123"),
|
||||
key="preferences",
|
||||
value={"theme": "dark", "language": "en"}
|
||||
)
|
||||
```
|
||||
|
||||
### get
|
||||
|
||||
Retrieve an item:
|
||||
|
||||
```python
|
||||
item = store.get(namespace=("user", "session-123"), key="preferences")
|
||||
if item:
|
||||
print(item.value) # {"theme": "dark", "language": "en"}
|
||||
print(item.created_at)
|
||||
print(item.updated_at)
|
||||
```
|
||||
|
||||
### search
|
||||
|
||||
Search within a namespace:
|
||||
|
||||
```python
|
||||
results = store.search(
|
||||
namespace_prefix=("user",),
|
||||
query="theme preferences",
|
||||
limit=10,
|
||||
offset=0
|
||||
)
|
||||
|
||||
for item in results:
|
||||
print(f"{item.key}: {item.value}")
|
||||
```
|
||||
|
||||
### delete
|
||||
|
||||
Delete an item:
|
||||
|
||||
```python
|
||||
store.delete(namespace=("user", "session-123"), key="preferences")
|
||||
```
|
||||
|
||||
## Async Support
|
||||
|
||||
All operations have async variants:
|
||||
|
||||
```python
|
||||
await store.aput(namespace, key, value)
|
||||
item = await store.aget(namespace, key)
|
||||
results = await store.asearch(namespace_prefix, query)
|
||||
await store.adelete(namespace, key)
|
||||
```
|
||||
|
||||
## With LangGraph
|
||||
|
||||
```python
|
||||
from langgraph.graph import StateGraph
|
||||
from hindsight_langmem import HindsightStore
|
||||
|
||||
store = HindsightStore(base_url="http://localhost:8888")
|
||||
|
||||
# Use store in your graph
|
||||
graph = StateGraph()
|
||||
# ... configure graph with store
|
||||
```
|
||||
|
||||
## Namespace Mapping
|
||||
|
||||
Namespaces are converted to Hindsight agent IDs:
|
||||
|
||||
| Namespace | bank ID |
|
||||
|-----------|----------|
|
||||
| `("user",)` | `user` |
|
||||
| `("user", "session")` | `user__session` |
|
||||
| `("app", "v1", "data")` | `app__v1__data` |
|
||||
| `()` | `default_agent_id` |
|
||||
|
||||
Memory banks are created automatically if they don't exist.
|
||||
@@ -1,89 +0,0 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
---
|
||||
|
||||
# MCP Server
|
||||
|
||||
Model Context Protocol server for AI assistants like Claude Desktop.
|
||||
|
||||
## Setup
|
||||
|
||||
The MCP server is included in the Hindsight API. When running the API with MCP enabled, it exposes MCP tools at `/mcp/{bank_id}/sse`.
|
||||
|
||||
### Claude Desktop Configuration
|
||||
|
||||
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"hindsight": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "mcp-remote", "http://localhost:8888/mcp/my-bank-id/sse"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Replace `my-bank-id` with your memory bank ID.
|
||||
|
||||
## Available Tools
|
||||
|
||||
### retain
|
||||
|
||||
Store a memory:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "retain",
|
||||
"arguments": {
|
||||
"content": "User prefers Python for data analysis",
|
||||
"context": "preferences"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `content` | string | yes | Memory content to store |
|
||||
| `context` | string | no | Category (default: 'general') |
|
||||
|
||||
### recall
|
||||
|
||||
Search memories:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "recall",
|
||||
"arguments": {
|
||||
"query": "What does the user do for work?"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `query` | string | yes | Natural language search query |
|
||||
| `max_results` | integer | no | Max results (default: 10) |
|
||||
|
||||
## Usage Example
|
||||
|
||||
Once configured, Claude can use Hindsight naturally:
|
||||
|
||||
**User**: "Remember that I prefer morning meetings"
|
||||
|
||||
**Claude**: *Uses retain*
|
||||
|
||||
> "I've noted that you prefer morning meetings."
|
||||
|
||||
---
|
||||
|
||||
**User**: "What do you know about my preferences?"
|
||||
|
||||
**Claude**: *Uses recall*
|
||||
|
||||
> "Based on our conversations, you prefer morning meetings and like Python for data analysis."
|
||||
@@ -15,21 +15,21 @@ npm install @vectorize-io/hindsight-client
|
||||
## Quick Start
|
||||
|
||||
```typescript
|
||||
import { HindsightClient } from '@vectorize-io/hindsight-client';
|
||||
const { HindsightClient } = require('@vectorize-io/hindsight-client');
|
||||
|
||||
const client = new HindsightClient({ baseUrl: 'http://localhost:8888' });
|
||||
|
||||
// Retain a memory
|
||||
await client.retain('my-agent', 'Alice works at Google');
|
||||
await client.retain('my-bank', 'Alice works at Google');
|
||||
|
||||
// Recall memories
|
||||
const response = await client.recall('my-agent', 'What does Alice do?');
|
||||
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-agent', 'Tell me about Alice');
|
||||
const answer = await client.reflect('my-bank', 'Tell me about Alice');
|
||||
console.log(answer.text);
|
||||
```
|
||||
|
||||
@@ -49,10 +49,10 @@ const client = new HindsightClient({
|
||||
|
||||
```typescript
|
||||
// Simple
|
||||
await client.retain('my-agent', 'Alice works at Google');
|
||||
await client.retain('my-bank', 'Alice works at Google');
|
||||
|
||||
// With options
|
||||
await client.retain('my-agent', 'Alice got promoted', {
|
||||
await client.retain('my-bank', 'Alice got promoted', {
|
||||
timestamp: new Date('2024-01-15'),
|
||||
context: 'career update',
|
||||
metadata: { source: 'slack' },
|
||||
@@ -63,11 +63,10 @@ await client.retain('my-agent', 'Alice got promoted', {
|
||||
### Retain Batch
|
||||
|
||||
```typescript
|
||||
await client.retainBatch('my-agent', [
|
||||
await client.retainBatch('my-bank', [
|
||||
{ content: 'Alice works at Google', context: 'career' },
|
||||
{ content: 'Bob is a data scientist', context: 'career' },
|
||||
], {
|
||||
documentId: 'conversation_001',
|
||||
async: false,
|
||||
});
|
||||
```
|
||||
@@ -76,31 +75,29 @@ await client.retainBatch('my-agent', [
|
||||
|
||||
```typescript
|
||||
// Simple - returns RecallResponse
|
||||
const response = await client.recall('my-agent', 'What does Alice do?');
|
||||
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-agent', 'What does Alice do?', {
|
||||
const response = await client.recall('my-bank', 'What does Alice do?', {
|
||||
types: ['world', 'opinion'], // Filter by fact type
|
||||
maxTokens: 4096,
|
||||
budget: 'high', // 'low', 'mid', or 'high'
|
||||
trace: true,
|
||||
});
|
||||
```
|
||||
|
||||
### Reflect (Generate Response)
|
||||
|
||||
```typescript
|
||||
const answer = await client.reflect('my-agent', 'What should I know about Alice?', {
|
||||
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
|
||||
console.log(answer.based_on); // Memories used
|
||||
```
|
||||
|
||||
## Bank Management
|
||||
@@ -108,7 +105,7 @@ console.log(answer.based_on); // Memories used
|
||||
### Create Bank
|
||||
|
||||
```typescript
|
||||
await client.createBank('my-agent', {
|
||||
await client.createBank('my-bank', {
|
||||
name: 'Assistant',
|
||||
background: 'I am a helpful AI assistant',
|
||||
disposition: {
|
||||
@@ -119,117 +116,14 @@ await client.createBank('my-agent', {
|
||||
});
|
||||
```
|
||||
|
||||
### Get Bank Profile
|
||||
|
||||
```typescript
|
||||
const profile = await client.getBankProfile('my-agent');
|
||||
console.log(profile.disposition);
|
||||
console.log(profile.background);
|
||||
```
|
||||
|
||||
### List Memories
|
||||
|
||||
```typescript
|
||||
const response = await client.listMemories('my-agent', {
|
||||
const response = await client.listMemories('my-bank', {
|
||||
type: 'world', // Optional filter
|
||||
q: 'Alice', // Optional text search
|
||||
limit: 100,
|
||||
offset: 0,
|
||||
});
|
||||
|
||||
for (const memory of response.memories) {
|
||||
console.log(`${memory.id}: ${memory.text}`);
|
||||
}
|
||||
```
|
||||
|
||||
## TypeScript Types
|
||||
|
||||
The client exports all types for full TypeScript support:
|
||||
|
||||
```typescript
|
||||
import type {
|
||||
RetainResponse,
|
||||
RecallResponse,
|
||||
RecallResult,
|
||||
ReflectResponse,
|
||||
BankProfileResponse,
|
||||
Budget,
|
||||
} from '@vectorize-io/hindsight-client';
|
||||
|
||||
// Budget is a union type: 'low' | 'mid' | 'high'
|
||||
const budget: Budget = 'mid';
|
||||
```
|
||||
|
||||
## Advanced: Low-Level SDK
|
||||
|
||||
For advanced use cases, access the auto-generated SDK directly:
|
||||
|
||||
```typescript
|
||||
import { sdk, createClient, createConfig } from '@vectorize-io/hindsight-client';
|
||||
|
||||
const client = createClient(createConfig({ baseUrl: 'http://localhost:8888' }));
|
||||
|
||||
// Use sdk functions directly
|
||||
const response = await sdk.recallMemories({
|
||||
client,
|
||||
path: { bank_id: 'my-agent' },
|
||||
body: {
|
||||
query: 'What does Alice do?',
|
||||
budget: 'mid',
|
||||
max_tokens: 4096,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```typescript
|
||||
import { HindsightClient } from '@vectorize-io/hindsight-client';
|
||||
|
||||
const client = new HindsightClient({ baseUrl: 'http://localhost:8888' });
|
||||
|
||||
try {
|
||||
const response = await client.recall('unknown-agent', 'test');
|
||||
} catch (error) {
|
||||
console.error('Error:', error.message);
|
||||
}
|
||||
```
|
||||
|
||||
## Example: Full Workflow
|
||||
|
||||
```typescript
|
||||
import { HindsightClient } from '@vectorize-io/hindsight-client';
|
||||
|
||||
async function main() {
|
||||
const client = new HindsightClient({ baseUrl: 'http://localhost:8888' });
|
||||
|
||||
// Create a bank with disposition
|
||||
await client.createBank('demo', {
|
||||
name: 'Demo Agent',
|
||||
background: 'A helpful assistant for demos',
|
||||
disposition: {
|
||||
skepticism: 2, // Trusting
|
||||
literalism: 3, // Balanced
|
||||
empathy: 4, // Empathetic
|
||||
},
|
||||
});
|
||||
|
||||
// Store some memories
|
||||
await client.retain('demo', 'Alice works at Google');
|
||||
await client.retain('demo', 'Bob is a data scientist at Google');
|
||||
await client.retain('demo', 'Alice and Bob collaborate on ML projects');
|
||||
|
||||
// Search for memories
|
||||
const searchResults = await client.recall('demo', 'Who works at Google?');
|
||||
console.log('Search results:');
|
||||
for (const r of searchResults.results) {
|
||||
console.log(` - ${r.text}`);
|
||||
}
|
||||
|
||||
// Generate a response
|
||||
const answer = await client.reflect('demo', 'What do you know about the team?');
|
||||
console.log('\nReflection:', answer.text);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
console.log(response)
|
||||
```
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
# OpenAI
|
||||
|
||||
Drop-in replacement for the OpenAI Python client with automatic memory integration.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
cd hindsight-openai && uv pip install -e .
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```python
|
||||
from hindsight_openai import configure, OpenAI
|
||||
|
||||
# Configure once
|
||||
configure(
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
agent_id="my-agent",
|
||||
)
|
||||
|
||||
# Use OpenAI client normally
|
||||
client = OpenAI(api_key="sk-...")
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model="gpt-4",
|
||||
messages=[{"role": "user", "content": "What did we discuss about AI?"}]
|
||||
)
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
The wrapper intercepts OpenAI calls:
|
||||
|
||||
1. **Before**: Retrieves relevant memories and injects as system message
|
||||
2. **After**: Stores conversation to Hindsight
|
||||
|
||||
Your code works exactly as before, but now has memory.
|
||||
|
||||
## Configuration
|
||||
|
||||
```python
|
||||
configure(
|
||||
hindsight_api_url="http://localhost:8888", # Hindsight API
|
||||
agent_id="my-agent", # Required
|
||||
store_conversations=True, # Store conversations
|
||||
inject_memories=True, # Inject memories into prompts
|
||||
document_id="session-123", # Group by document
|
||||
enabled=True, # Master switch
|
||||
)
|
||||
```
|
||||
|
||||
## Memory Injection
|
||||
|
||||
When enabled, memories are automatically injected:
|
||||
|
||||
```python
|
||||
# Your code
|
||||
messages = [{"role": "user", "content": "What trails did Alice recommend?"}]
|
||||
|
||||
# What gets sent to OpenAI
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "Relevant context:\n- Alice loves hiking in Yosemite\n- Alice recommended Half Dome trail"
|
||||
},
|
||||
{"role": "user", "content": "What trails did Alice recommend?"}
|
||||
]
|
||||
```
|
||||
|
||||
## Async Support
|
||||
|
||||
```python
|
||||
from hindsight_openai import configure, AsyncOpenAI
|
||||
|
||||
configure(hindsight_api_url="http://localhost:8888", agent_id="my-agent")
|
||||
|
||||
client = AsyncOpenAI(api_key="sk-...")
|
||||
|
||||
response = await client.chat.completions.create(
|
||||
model="gpt-4",
|
||||
messages=[{"role": "user", "content": "Tell me about my preferences"}]
|
||||
)
|
||||
```
|
||||
|
||||
## Streaming
|
||||
|
||||
Fully supported:
|
||||
|
||||
```python
|
||||
stream = client.chat.completions.create(
|
||||
model="gpt-4",
|
||||
messages=[{"role": "user", "content": "Tell me a story"}],
|
||||
stream=True,
|
||||
)
|
||||
|
||||
for chunk in stream:
|
||||
print(chunk.choices[0].delta.content or "", end="")
|
||||
```
|
||||
|
||||
## Disable Temporarily
|
||||
|
||||
```python
|
||||
from hindsight_openai import configure
|
||||
|
||||
configure(enabled=False) # Disable
|
||||
configure(enabled=True) # Re-enable
|
||||
```
|
||||
@@ -49,15 +49,15 @@ with HindsightServer(
|
||||
client = HindsightClient(base_url=server.url)
|
||||
|
||||
# Retain a memory
|
||||
client.retain(bank_id="my-agent", content="Alice works at Google")
|
||||
client.retain(bank_id="my-bank", content="Alice works at Google")
|
||||
|
||||
# Recall memories
|
||||
results = client.recall(bank_id="my-agent", query="What does Alice do?")
|
||||
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-agent", query="Tell me about Alice")
|
||||
answer = client.reflect(bank_id="my-bank", query="Tell me about Alice")
|
||||
print(answer.text)
|
||||
```
|
||||
|
||||
@@ -70,15 +70,15 @@ from hindsight_client import Hindsight
|
||||
client = Hindsight(base_url="http://localhost:8888")
|
||||
|
||||
# Retain a memory
|
||||
client.retain(bank_id="my-agent", content="Alice works at Google")
|
||||
client.retain(bank_id="my-bank", content="Alice works at Google")
|
||||
|
||||
# Recall memories
|
||||
results = client.recall(bank_id="my-agent", query="What does Alice do?")
|
||||
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-agent", query="Tell me about Alice")
|
||||
answer = client.reflect(bank_id="my-bank", query="Tell me about Alice")
|
||||
print(answer.text)
|
||||
```
|
||||
|
||||
@@ -103,7 +103,7 @@ client = Hindsight(
|
||||
```python
|
||||
# Simple
|
||||
client.retain(
|
||||
bank_id="my-agent",
|
||||
bank_id="my-bank",
|
||||
content="Alice works at Google as a software engineer",
|
||||
)
|
||||
|
||||
@@ -111,7 +111,7 @@ client.retain(
|
||||
from datetime import datetime
|
||||
|
||||
client.retain(
|
||||
bank_id="my-agent",
|
||||
bank_id="my-bank",
|
||||
content="Alice got promoted",
|
||||
context="career update",
|
||||
timestamp=datetime(2024, 1, 15),
|
||||
@@ -124,7 +124,7 @@ client.retain(
|
||||
|
||||
```python
|
||||
client.retain_batch(
|
||||
bank_id="my-agent",
|
||||
bank_id="my-bank",
|
||||
items=[
|
||||
{"content": "Alice works at Google", "context": "career"},
|
||||
{"content": "Bob is a data scientist", "context": "career"},
|
||||
@@ -139,16 +139,16 @@ client.retain_batch(
|
||||
```python
|
||||
# Simple - returns list of RecallResult
|
||||
results = client.recall(
|
||||
bank_id="my-agent",
|
||||
bank_id="my-bank",
|
||||
query="What does Alice do?",
|
||||
)
|
||||
|
||||
for r in results:
|
||||
for r in results.results:
|
||||
print(f"{r.text} (type: {r.type})")
|
||||
|
||||
# With options
|
||||
results = client.recall(
|
||||
bank_id="my-agent",
|
||||
bank_id="my-bank",
|
||||
query="What does Alice do?",
|
||||
types=["world", "opinion"], # Filter by fact type
|
||||
max_tokens=4096,
|
||||
@@ -159,16 +159,15 @@ results = client.recall(
|
||||
### Recall with Full Response
|
||||
|
||||
```python
|
||||
# Returns RecallResponse with entities and trace info
|
||||
response = client.recall_memories(
|
||||
bank_id="my-agent",
|
||||
# Returns RecallResponse with entities and chunks
|
||||
response = client.recall(
|
||||
bank_id="my-bank",
|
||||
query="What does Alice do?",
|
||||
types=["world", "experience"],
|
||||
budget="mid",
|
||||
max_tokens=4096,
|
||||
trace=True,
|
||||
include_entities=True,
|
||||
max_entity_tokens=500,
|
||||
max_entity_tokens=500
|
||||
)
|
||||
|
||||
print(f"Found {len(response.results)} memories")
|
||||
@@ -185,14 +184,13 @@ if response.entities:
|
||||
|
||||
```python
|
||||
answer = client.reflect(
|
||||
bank_id="my-agent",
|
||||
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
|
||||
print(answer.based_on) # Memories used
|
||||
```
|
||||
|
||||
## Bank Management
|
||||
@@ -201,7 +199,7 @@ print(answer.based_on) # Memories used
|
||||
|
||||
```python
|
||||
client.create_bank(
|
||||
bank_id="my-agent",
|
||||
bank_id="my-bank",
|
||||
name="Assistant",
|
||||
background="I am a helpful AI assistant",
|
||||
disposition={
|
||||
@@ -215,16 +213,13 @@ client.create_bank(
|
||||
### List Memories
|
||||
|
||||
```python
|
||||
response = client.list_memories(
|
||||
bank_id="my-agent",
|
||||
client.list_memories(
|
||||
bank_id="my-bank",
|
||||
type="world", # Optional: filter by type
|
||||
search_query="Alice", # Optional: text search
|
||||
limit=100,
|
||||
offset=0,
|
||||
)
|
||||
|
||||
for memory in response.memories:
|
||||
print(f"{memory.id}: {memory.text}")
|
||||
```
|
||||
|
||||
## Async Support
|
||||
@@ -239,15 +234,15 @@ async def main():
|
||||
client = Hindsight(base_url="http://localhost:8888")
|
||||
|
||||
# Async retain
|
||||
await client.aretain(bank_id="my-agent", content="Hello world")
|
||||
await client.aretain(bank_id="my-bank", content="Hello world")
|
||||
|
||||
# Async recall
|
||||
results = await client.arecall(bank_id="my-agent", query="Hello")
|
||||
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-agent", query="What did I say?")
|
||||
answer = await client.areflect(bank_id="my-bank", query="What did I say?")
|
||||
print(answer.text)
|
||||
|
||||
client.close()
|
||||
@@ -255,29 +250,13 @@ async def main():
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## Response Types
|
||||
|
||||
The client exports response types for type hints:
|
||||
|
||||
```python
|
||||
from hindsight_client import (
|
||||
Hindsight,
|
||||
RetainResponse,
|
||||
RecallResponse,
|
||||
RecallResult,
|
||||
ReflectResponse,
|
||||
BankProfileResponse,
|
||||
DispositionTraits,
|
||||
)
|
||||
```
|
||||
|
||||
## Context Manager
|
||||
|
||||
```python
|
||||
from hindsight_client import Hindsight
|
||||
|
||||
with Hindsight(base_url="http://localhost:8888") as client:
|
||||
client.retain(bank_id="my-agent", content="Hello")
|
||||
results = client.recall(bank_id="my-agent", query="Hello")
|
||||
client.retain(bank_id="my-bank", content="Hello")
|
||||
results = client.recall(bank_id="my-bank", query="Hello")
|
||||
# Client automatically closed
|
||||
```
|
||||
|
||||
@@ -5,7 +5,7 @@ import type * as Preset from '@docusaurus/preset-classic';
|
||||
const config: Config = {
|
||||
title: 'Hindsight',
|
||||
tagline: 'Entity-Aware Memory System for AI Agents',
|
||||
favicon: 'img/favicon.ico',
|
||||
favicon: 'img/favicon.png',
|
||||
|
||||
future: {
|
||||
v4: true,
|
||||
@@ -49,7 +49,7 @@ const config: Config = {
|
||||
tagName: 'link',
|
||||
attributes: {
|
||||
rel: 'stylesheet',
|
||||
href: 'https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Nunito+Sans:wght@400;500;600;700;800&display=swap',
|
||||
href: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500;600&family=Space+Grotesk:wght@500;600;700&display=swap',
|
||||
media: 'print',
|
||||
onload: "this.media='all'",
|
||||
},
|
||||
@@ -83,7 +83,7 @@ const config: Config = {
|
||||
},
|
||||
],
|
||||
theme: {
|
||||
primaryColor: '#0d9488',
|
||||
primaryColor: '#0074d9',
|
||||
sidebar: {
|
||||
backgroundColor: '#09090b',
|
||||
},
|
||||
@@ -92,9 +92,9 @@ const config: Config = {
|
||||
},
|
||||
typography: {
|
||||
fontSize: '15px',
|
||||
fontFamily: "'Avenir Book', 'Avenir', 'Nunito Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
||||
fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
||||
headings: {
|
||||
fontFamily: "'Avenir', 'Avenir Book', 'Nunito Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
||||
fontFamily: "'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
||||
},
|
||||
code: {
|
||||
fontFamily: "'JetBrains Mono', 'Fira Code', 'SF Mono', Monaco, Consolas, monospace",
|
||||
@@ -121,51 +121,51 @@ const config: Config = {
|
||||
respectPrefersColorScheme: true,
|
||||
},
|
||||
navbar: {
|
||||
title: 'Hindsight',
|
||||
logo: {
|
||||
alt: 'Hindsight Logo',
|
||||
src: 'img/logo.svg',
|
||||
src: 'img/logo.png',
|
||||
style: { height: '32px' },
|
||||
},
|
||||
items: [
|
||||
{
|
||||
type: 'custom-iconLink',
|
||||
type: 'doc',
|
||||
docId: 'developer/index',
|
||||
position: 'left',
|
||||
icon: 'code',
|
||||
label: 'Developer',
|
||||
to: '/',
|
||||
className: 'navbar-item-developer',
|
||||
},
|
||||
{
|
||||
type: 'custom-iconLink',
|
||||
type: 'doc',
|
||||
docId: 'sdks/python',
|
||||
position: 'left',
|
||||
icon: 'package',
|
||||
label: 'SDKs',
|
||||
to: '/sdks/python',
|
||||
className: 'navbar-item-sdks',
|
||||
},
|
||||
{
|
||||
type: 'custom-iconLink',
|
||||
position: 'left',
|
||||
icon: 'file-code',
|
||||
label: 'API Reference',
|
||||
to: '/api-reference',
|
||||
position: 'left',
|
||||
label: 'API Reference',
|
||||
className: 'navbar-item-api',
|
||||
},
|
||||
{
|
||||
type: 'custom-iconLink',
|
||||
type: 'doc',
|
||||
docId: 'cookbook/index',
|
||||
position: 'left',
|
||||
icon: 'book-open',
|
||||
label: 'Cookbook',
|
||||
to: '/cookbook',
|
||||
className: 'navbar-item-cookbook',
|
||||
},
|
||||
{
|
||||
type: 'custom-iconLink',
|
||||
type: 'doc',
|
||||
docId: 'changelog/index',
|
||||
position: 'left',
|
||||
icon: 'clock',
|
||||
label: 'Changelog',
|
||||
to: '/changelog',
|
||||
className: 'navbar-item-changelog',
|
||||
},
|
||||
{
|
||||
href: 'https://github.com/vectorize-io/hindsight',
|
||||
label: 'GitHub',
|
||||
position: 'right',
|
||||
className: 'header-github-link',
|
||||
'aria-label': 'GitHub repository',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -199,7 +199,7 @@ const config: Config = {
|
||||
],
|
||||
},
|
||||
],
|
||||
copyright: `Copyright © ${new Date().getFullYear()} Hindsight. Built with Docusaurus.`,
|
||||
copyright: `Copyright © ${new Date().getFullYear()} Hindsight.`,
|
||||
},
|
||||
prism: {
|
||||
theme: prismThemes.github,
|
||||
@@ -209,24 +209,39 @@ const config: Config = {
|
||||
mermaid: {
|
||||
theme: {
|
||||
light: 'base',
|
||||
dark: 'dark',
|
||||
dark: 'base',
|
||||
},
|
||||
options: {
|
||||
themeVariables: {
|
||||
primaryColor: '#6366f1',
|
||||
// Gradient start (#0074d9 blue) for nodes
|
||||
primaryColor: '#0074d9',
|
||||
primaryTextColor: '#ffffff',
|
||||
primaryBorderColor: '#4f46e5',
|
||||
secondaryColor: '#f1f5f9',
|
||||
secondaryTextColor: '#1e293b',
|
||||
secondaryBorderColor: '#cbd5e1',
|
||||
tertiaryColor: '#e0e7ff',
|
||||
lineColor: '#94a3b8',
|
||||
primaryBorderColor: '#005db0',
|
||||
// Gradient end (#009296 teal) for edges/clusters
|
||||
secondaryColor: '#009296',
|
||||
secondaryTextColor: '#ffffff',
|
||||
secondaryBorderColor: '#007a7d',
|
||||
// Tertiary
|
||||
tertiaryColor: '#e6f7f8',
|
||||
tertiaryTextColor: '#1e293b',
|
||||
// Lines and edges - gradient end color
|
||||
lineColor: '#009296',
|
||||
// Text
|
||||
textColor: '#1e293b',
|
||||
mainBkg: '#ffffff',
|
||||
nodeBorder: '#4f46e5',
|
||||
clusterBkg: '#f8fafc',
|
||||
clusterBorder: '#e2e8f0',
|
||||
fontFamily: 'system-ui, -apple-system, sans-serif',
|
||||
// Node specific - gradient start
|
||||
nodeBkg: '#0074d9',
|
||||
nodeTextColor: '#ffffff',
|
||||
nodeBorder: '#005db0',
|
||||
// Main background
|
||||
mainBkg: '#0074d9',
|
||||
// Clusters/subgraphs - gradient end
|
||||
clusterBkg: 'rgba(0, 146, 150, 0.08)',
|
||||
clusterBorder: '#009296',
|
||||
// Labels
|
||||
edgeLabelBackground: 'transparent',
|
||||
labelBackground: 'transparent',
|
||||
// Font - Inter to match body text
|
||||
fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1046,6 +1046,47 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Banks"
|
||||
],
|
||||
"summary": "Delete memory bank",
|
||||
"description": "Delete an entire memory bank including all memories, entities, documents, and the bank profile itself. This is a destructive operation that cannot be undone.",
|
||||
"operationId": "delete_bank",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "bank_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Bank Id"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/DeleteResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/default/banks/{bank_id}/memories": {
|
||||
@@ -1494,6 +1535,28 @@
|
||||
"success": {
|
||||
"type": "boolean",
|
||||
"title": "Success"
|
||||
},
|
||||
"message": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Message"
|
||||
},
|
||||
"deleted_count": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Deleted Count"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
@@ -1503,7 +1566,9 @@
|
||||
"title": "DeleteResponse",
|
||||
"description": "Response model for delete operations.",
|
||||
"example": {
|
||||
"success": true
|
||||
"success": true,
|
||||
"message": "Deleted successfully",
|
||||
"deleted_count": 10
|
||||
}
|
||||
},
|
||||
"DispositionTraits": {
|
||||
|
||||
Generated
-14
@@ -13,7 +13,6 @@
|
||||
"@docusaurus/theme-common": "^3.9.2",
|
||||
"@docusaurus/theme-mermaid": "^3.9.2",
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"@phosphor-icons/react": "^2.1.10",
|
||||
"clsx": "^2.0.0",
|
||||
"prism-react-renderer": "^2.3.0",
|
||||
"react": "^19.0.0",
|
||||
@@ -4534,19 +4533,6 @@
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@phosphor-icons/react": {
|
||||
"version": "2.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@phosphor-icons/react/-/react-2.1.10.tgz",
|
||||
"integrity": "sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.8",
|
||||
"react-dom": ">= 16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@pnpm/config.env-replace": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz",
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
"@docusaurus/theme-common": "^3.9.2",
|
||||
"@docusaurus/theme-mermaid": "^3.9.2",
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"@phosphor-icons/react": "^2.1.10",
|
||||
"clsx": "^2.0.0",
|
||||
"prism-react-renderer": "^2.3.0",
|
||||
"react": "^19.0.0",
|
||||
|
||||
@@ -147,28 +147,6 @@ const sidebars: SidebarsConfig = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Integrations',
|
||||
collapsible: false,
|
||||
items: [
|
||||
{
|
||||
type: 'doc',
|
||||
id: 'sdks/openai',
|
||||
label: 'OpenAI',
|
||||
},
|
||||
{
|
||||
type: 'doc',
|
||||
id: 'sdks/langgraph',
|
||||
label: 'LangGraph',
|
||||
},
|
||||
{
|
||||
type: 'doc',
|
||||
id: 'sdks/mcp',
|
||||
label: 'MCP Server',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
cookbookSidebar: [
|
||||
{
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import React from 'react';
|
||||
import Link from '@docusaurus/Link';
|
||||
import {
|
||||
House,
|
||||
Code,
|
||||
Package,
|
||||
FileCode,
|
||||
BookOpen,
|
||||
ClockCounterClockwise,
|
||||
} from '@phosphor-icons/react';
|
||||
|
||||
const iconMap = {
|
||||
house: House,
|
||||
code: Code,
|
||||
package: Package,
|
||||
'file-code': FileCode,
|
||||
'book-open': BookOpen,
|
||||
clock: ClockCounterClockwise,
|
||||
};
|
||||
|
||||
export default function NavbarIconLink({
|
||||
icon,
|
||||
label,
|
||||
to,
|
||||
className,
|
||||
}: {
|
||||
icon: keyof typeof iconMap;
|
||||
label: string;
|
||||
to: string;
|
||||
className?: string;
|
||||
}) {
|
||||
const IconComponent = iconMap[icon];
|
||||
|
||||
return (
|
||||
<Link to={to} className={`navbar__link ${className || ''}`}>
|
||||
{IconComponent && (
|
||||
<IconComponent size={16} weight="bold" style={{ marginRight: '6px', verticalAlign: 'middle' }} />
|
||||
)}
|
||||
<span style={{ verticalAlign: 'middle' }}>{label}</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -2,43 +2,49 @@
|
||||
* Hindsight custom theme - Modern, clean style
|
||||
*/
|
||||
|
||||
/* Primary colors - Teal/Cyan theme */
|
||||
/* Primary colors - Blue to Teal gradient theme */
|
||||
:root {
|
||||
--ifm-color-primary: #0d9488;
|
||||
--ifm-color-primary-dark: #0f766e;
|
||||
--ifm-color-primary-darker: #115e59;
|
||||
--ifm-color-primary-darkest: #134e4a;
|
||||
--ifm-color-primary-light: #14b8a6;
|
||||
--ifm-color-primary-lighter: #2dd4bf;
|
||||
--ifm-color-primary-lightest: #5eead4;
|
||||
/* Primary gradient */
|
||||
--hindsight-gradient: linear-gradient(135deg, #0074d9 0%, #009296 100%);
|
||||
--hindsight-gradient-start: #0074d9;
|
||||
--hindsight-gradient-end: #009296;
|
||||
|
||||
/* Fallback solid colors (midpoint of gradient) */
|
||||
--ifm-color-primary: #0074d9;
|
||||
--ifm-color-primary-dark: #0068c3;
|
||||
--ifm-color-primary-darker: #005db0;
|
||||
--ifm-color-primary-darkest: #004d91;
|
||||
--ifm-color-primary-light: #1a85e0;
|
||||
--ifm-color-primary-lighter: #3396e8;
|
||||
--ifm-color-primary-lightest: #e6f3ff;
|
||||
|
||||
--ifm-code-font-size: 90%;
|
||||
--docusaurus-highlighted-code-line-bg: rgba(13, 148, 136, 0.1);
|
||||
--docusaurus-highlighted-code-line-bg: rgba(0, 116, 217, 0.1);
|
||||
|
||||
/* Typography - Avenir Book */
|
||||
--ifm-font-family-base: 'Avenir Book', 'Avenir', 'Nunito Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
--ifm-heading-font-family: 'Avenir', 'Avenir Book', 'Nunito Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
/* Typography */
|
||||
--ifm-font-family-base: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
--ifm-heading-font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
--ifm-font-family-monospace: 'JetBrains Mono', 'Fira Code', 'SF Mono', Monaco, 'Cascadia Code', Consolas, monospace;
|
||||
--ifm-font-weight-semibold: 600;
|
||||
--ifm-font-size-base: 96%;
|
||||
|
||||
/* Spacing */
|
||||
--ifm-spacing-horizontal: 1.5rem;
|
||||
--ifm-navbar-height: 3.5rem;
|
||||
--ifm-navbar-height: 4.5rem;
|
||||
|
||||
/* Borders */
|
||||
--ifm-global-radius: 0.5rem;
|
||||
}
|
||||
|
||||
[data-theme='dark'] {
|
||||
--ifm-color-primary: #2dd4bf;
|
||||
--ifm-color-primary-dark: #14b8a6;
|
||||
--ifm-color-primary-darker: #0d9488;
|
||||
--ifm-color-primary-darkest: #0f766e;
|
||||
--ifm-color-primary-light: #5eead4;
|
||||
--ifm-color-primary-lighter: #99f6e4;
|
||||
--ifm-color-primary-lightest: #ccfbf1;
|
||||
--docusaurus-highlighted-code-line-bg: rgba(45, 212, 191, 0.15);
|
||||
--ifm-color-primary: #3396e8;
|
||||
--ifm-color-primary-dark: #1a85e0;
|
||||
--ifm-color-primary-darker: #0074d9;
|
||||
--ifm-color-primary-darkest: #0068c3;
|
||||
--ifm-color-primary-light: #66b3f0;
|
||||
--ifm-color-primary-lighter: #99cff5;
|
||||
--ifm-color-primary-lightest: rgba(51, 150, 232, 0.15);
|
||||
--docusaurus-highlighted-code-line-bg: rgba(51, 150, 232, 0.15);
|
||||
|
||||
--ifm-background-color: #09090b;
|
||||
--ifm-background-surface-color: #18181b;
|
||||
@@ -47,6 +53,14 @@
|
||||
--ifm-toc-border-color: #27272a;
|
||||
}
|
||||
|
||||
/* Gradient text utility */
|
||||
.gradient-text {
|
||||
background: var(--hindsight-gradient);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
/* Navbar styling */
|
||||
.navbar {
|
||||
box-shadow: none;
|
||||
@@ -54,9 +68,17 @@
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.navbar__logo {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.navbar__title {
|
||||
font-weight: 700;
|
||||
font-size: 1.125rem;
|
||||
background: var(--hindsight-gradient);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.navbar__items {
|
||||
@@ -64,32 +86,202 @@
|
||||
}
|
||||
|
||||
.navbar__link {
|
||||
font-weight: 500;
|
||||
font-size: 0.8125rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 0.9rem;
|
||||
padding: 0.625rem 1rem;
|
||||
border-radius: 0.375rem;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
/* Navbar icons (desktop only) */
|
||||
@media (min-width: 997px) {
|
||||
.navbar-item-developer::before,
|
||||
.navbar-item-sdks::before,
|
||||
.navbar-item-api::before,
|
||||
.navbar-item-cookbook::before,
|
||||
.navbar-item-changelog::before {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 6px;
|
||||
vertical-align: middle;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
content: '';
|
||||
}
|
||||
|
||||
.navbar-item-developer::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256'%3E%3Cpath fill='%23666' d='M71.68 97.22 34.74 128l36.94 30.78a12 12 0 1 1-15.36 18.44l-48-40a12 12 0 0 1 0-18.44l48-40a12 12 0 0 1 15.36 18.44Zm176 21.56-48-40a12 12 0 1 0-15.36 18.44L221.26 128l-36.94 30.78a12 12 0 1 0 15.36 18.44l48-40a12 12 0 0 0 0-18.44ZM164.1 28.72a12 12 0 0 0-15.38 7.18l-64 176a12 12 0 0 0 7.18 15.37 11.79 11.79 0 0 0 4.1.73 12 12 0 0 0 11.28-7.9l64-176a12 12 0 0 0-7.18-15.38Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.navbar-item-sdks::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256'%3E%3Cpath fill='%23666' d='m225.6 62.64-88-48.17a19.91 19.91 0 0 0-19.2 0l-88 48.17A20 20 0 0 0 20 80.19v95.62a20 20 0 0 0 10.4 17.55l88 48.17a19.89 19.89 0 0 0 19.2 0l88-48.17a20 20 0 0 0 10.4-17.55V80.19a20 20 0 0 0-10.4-17.55ZM128 36.57 200 76l-72 39.42L56 76ZM44 96.82l72 39.43v76.89l-72-39.42Zm96 116.32v-76.89l72-39.43v76.89Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.navbar-item-api::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256'%3E%3Cpath fill='%23666' d='M180.49 143.51a12 12 0 0 1 0 17l-24 24a12 12 0 0 1-17-17L155 152l-15.52-15.51a12 12 0 0 1 17-17ZM112.49 120.49a12 12 0 0 0-17 0l-24 24a12 12 0 0 0 0 17l24 24a12 12 0 0 0 17-17L97 153l15.52-15.51a12 12 0 0 0-.03-17ZM220 88v24a12 12 0 0 1-24 0v-16h-44a12 12 0 0 1-12-12V40H60v68a12 12 0 0 1-24 0V40a20 20 0 0 1 20-20h96a12 12 0 0 1 8.49 3.52l56 56A12 12 0 0 1 220 88Zm-60-8h23L160 57Zm-4 132H60v-12a12 12 0 0 0-24 0v12a20 20 0 0 0 20 20h100a12 12 0 0 0 0-24Zm64-44a12 12 0 0 0-12 12v36h-44a12 12 0 0 0 0 24h44a20 20 0 0 0 20-20v-40a12 12 0 0 0-8-12Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.navbar-item-cookbook::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256'%3E%3Cpath fill='%23666' d='M224 44H160a43.86 43.86 0 0 0-32 13.85A43.86 43.86 0 0 0 96 44H32a20 20 0 0 0-20 20v128a20 20 0 0 0 20 20h64a20 20 0 0 1 20 20 12 12 0 0 0 24 0 20 20 0 0 1 20-20h64a20 20 0 0 0 20-20V64a20 20 0 0 0-20-20ZM96 188H36V68h60a20 20 0 0 1 20 20v108.69A43.74 43.74 0 0 0 96 188Zm124 0h-60a43.74 43.74 0 0 0-20 8.69V88a20 20 0 0 1 20-20h60Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.navbar-item-changelog::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256'%3E%3Cpath fill='%23666' d='M140 80v41.21l34.17 20.5a12 12 0 1 1-12.34 20.58l-40-24A12 12 0 0 1 116 128V80a12 12 0 0 1 24 0Zm-12-52a99.38 99.38 0 0 0-70.76 29.34c-4.69 4.74-9 9.37-13.24 14V64a12 12 0 0 0-24 0v40a12 12 0 0 0 12 12h40a12 12 0 0 0 0-24H53.41c4.24-5.95 8.53-11.93 13.49-16.95A76 76 0 1 1 52 128a12 12 0 0 0-24 0 100 100 0 1 0 100-100Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* Dark mode icons */
|
||||
[data-theme='dark'] .navbar-item-developer::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256'%3E%3Cpath fill='%23ccc' d='M71.68 97.22 34.74 128l36.94 30.78a12 12 0 1 1-15.36 18.44l-48-40a12 12 0 0 1 0-18.44l48-40a12 12 0 0 1 15.36 18.44Zm176 21.56-48-40a12 12 0 1 0-15.36 18.44L221.26 128l-36.94 30.78a12 12 0 1 0 15.36 18.44l48-40a12 12 0 0 0 0-18.44ZM164.1 28.72a12 12 0 0 0-15.38 7.18l-64 176a12 12 0 0 0 7.18 15.37 11.79 11.79 0 0 0 4.1.73 12 12 0 0 0 11.28-7.9l64-176a12 12 0 0 0-7.18-15.38Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
[data-theme='dark'] .navbar-item-sdks::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256'%3E%3Cpath fill='%23ccc' d='m225.6 62.64-88-48.17a19.91 19.91 0 0 0-19.2 0l-88 48.17A20 20 0 0 0 20 80.19v95.62a20 20 0 0 0 10.4 17.55l88 48.17a19.89 19.89 0 0 0 19.2 0l88-48.17a20 20 0 0 0 10.4-17.55V80.19a20 20 0 0 0-10.4-17.55ZM128 36.57 200 76l-72 39.42L56 76ZM44 96.82l72 39.43v76.89l-72-39.42Zm96 116.32v-76.89l72-39.43v76.89Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
[data-theme='dark'] .navbar-item-api::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256'%3E%3Cpath fill='%23ccc' d='M180.49 143.51a12 12 0 0 1 0 17l-24 24a12 12 0 0 1-17-17L155 152l-15.52-15.51a12 12 0 0 1 17-17ZM112.49 120.49a12 12 0 0 0-17 0l-24 24a12 12 0 0 0 0 17l24 24a12 12 0 0 0 17-17L97 153l15.52-15.51a12 12 0 0 0-.03-17ZM220 88v24a12 12 0 0 1-24 0v-16h-44a12 12 0 0 1-12-12V40H60v68a12 12 0 0 1-24 0V40a20 20 0 0 1 20-20h96a12 12 0 0 1 8.49 3.52l56 56A12 12 0 0 1 220 88Zm-60-8h23L160 57Zm-4 132H60v-12a12 12 0 0 0-24 0v12a20 20 0 0 0 20 20h100a12 12 0 0 0 0-24Zm64-44a12 12 0 0 0-12 12v36h-44a12 12 0 0 0 0 24h44a20 20 0 0 0 20-20v-40a12 12 0 0 0-8-12Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
[data-theme='dark'] .navbar-item-cookbook::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256'%3E%3Cpath fill='%23ccc' d='M224 44H160a43.86 43.86 0 0 0-32 13.85A43.86 43.86 0 0 0 96 44H32a20 20 0 0 0-20 20v128a20 20 0 0 0 20 20h64a20 20 0 0 1 20 20 12 12 0 0 0 24 0 20 20 0 0 1 20-20h64a20 20 0 0 0 20-20V64a20 20 0 0 0-20-20ZM96 188H36V68h60a20 20 0 0 1 20 20v108.69A43.74 43.74 0 0 0 96 188Zm124 0h-60a43.74 43.74 0 0 0-20 8.69V88a20 20 0 0 1 20-20h60Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
[data-theme='dark'] .navbar-item-changelog::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256'%3E%3Cpath fill='%23ccc' d='M140 80v41.21l34.17 20.5a12 12 0 1 1-12.34 20.58l-40-24A12 12 0 0 1 116 128V80a12 12 0 0 1 24 0Zm-12-52a99.38 99.38 0 0 0-70.76 29.34c-4.69 4.74-9 9.37-13.24 14V64a12 12 0 0 0-24 0v40a12 12 0 0 0 12 12h40a12 12 0 0 0 0-24H53.41c4.24-5.95 8.53-11.93 13.49-16.95A76 76 0 1 1 52 128a12 12 0 0 0-24 0 100 100 0 1 0 100-100Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
}
|
||||
|
||||
/* GitHub icon link */
|
||||
.header-github-link::before {
|
||||
content: '';
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
background: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='%23666' d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E") no-repeat;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .header-github-link::before {
|
||||
background: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='%23fff' d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E") no-repeat;
|
||||
}
|
||||
|
||||
.header-github-link:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.navbar__link:hover {
|
||||
background-color: var(--ifm-background-surface-color);
|
||||
}
|
||||
|
||||
.navbar__link--active {
|
||||
color: var(--ifm-color-primary);
|
||||
background: var(--hindsight-gradient);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .navbar__link:hover {
|
||||
background-color: #27272a;
|
||||
}
|
||||
|
||||
/* Mobile navbar */
|
||||
@media (max-width: 996px) {
|
||||
:root {
|
||||
--ifm-navbar-height: 3.5rem;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
padding: 0 0.75rem;
|
||||
}
|
||||
|
||||
.navbar__logo {
|
||||
margin-bottom: 0;
|
||||
height: 24px !important;
|
||||
}
|
||||
|
||||
.navbar__logo img {
|
||||
height: 24px !important;
|
||||
}
|
||||
|
||||
/* Hamburger menu toggle */
|
||||
.navbar__toggle {
|
||||
color: var(--ifm-color-primary);
|
||||
}
|
||||
|
||||
/* Mobile sidebar */
|
||||
.navbar-sidebar {
|
||||
background: #ffffff !important;
|
||||
}
|
||||
|
||||
.navbar-sidebar__brand {
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid var(--ifm-toc-border-color);
|
||||
background: #ffffff !important;
|
||||
}
|
||||
|
||||
.navbar-sidebar__items {
|
||||
padding: 1rem 0;
|
||||
background: #ffffff !important;
|
||||
}
|
||||
|
||||
.navbar-sidebar .menu__link {
|
||||
font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
padding: 0.75rem 1.25rem;
|
||||
color: #1e293b !important;
|
||||
}
|
||||
|
||||
.navbar-sidebar .menu__link--active {
|
||||
background: var(--hindsight-gradient);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
/* Close button */
|
||||
.navbar-sidebar__close {
|
||||
color: #1e293b !important;
|
||||
}
|
||||
|
||||
/* Backdrop overlay */
|
||||
.navbar-sidebar__backdrop {
|
||||
background: rgba(0, 0, 0, 0.5) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark mode mobile sidebar */
|
||||
@media (max-width: 996px) {
|
||||
[data-theme='dark'] .navbar-sidebar {
|
||||
background: #09090b !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .navbar-sidebar__brand {
|
||||
background: #09090b !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .navbar-sidebar__items {
|
||||
background: #09090b !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .navbar-sidebar .menu__link {
|
||||
color: #e2e8f0 !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .navbar-sidebar__close {
|
||||
color: #e2e8f0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Hero section */
|
||||
.hero {
|
||||
padding: 4rem 0;
|
||||
}
|
||||
|
||||
.hero--primary {
|
||||
background: linear-gradient(135deg, var(--ifm-color-primary-darkest) 0%, var(--ifm-color-primary-dark) 100%);
|
||||
background: linear-gradient(135deg, var(--hindsight-gradient-start) 0%, var(--hindsight-gradient-end) 100%);
|
||||
}
|
||||
|
||||
[data-theme='dark'] .hero--primary {
|
||||
@@ -115,18 +307,26 @@
|
||||
}
|
||||
|
||||
.button--primary {
|
||||
background: var(--ifm-color-primary);
|
||||
border-color: var(--ifm-color-primary);
|
||||
background: var(--hindsight-gradient);
|
||||
border: none;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.button--primary:hover {
|
||||
background: var(--ifm-color-primary-dark);
|
||||
border-color: var(--ifm-color-primary-dark);
|
||||
background: linear-gradient(135deg, #005db0 0%, #007a7d 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.button--secondary {
|
||||
background: transparent;
|
||||
border: 2px solid currentColor;
|
||||
border: 2px solid var(--hindsight-gradient-start);
|
||||
color: var(--ifm-color-primary);
|
||||
}
|
||||
|
||||
.button--secondary:hover {
|
||||
background: var(--hindsight-gradient);
|
||||
border-color: transparent;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
@@ -137,13 +337,18 @@
|
||||
}
|
||||
|
||||
.menu__link--active {
|
||||
background: var(--ifm-color-primary-lightest);
|
||||
color: var(--ifm-color-primary-darkest);
|
||||
background: linear-gradient(135deg, rgba(0, 116, 217, 0.1) 0%, rgba(0, 146, 150, 0.1) 100%);
|
||||
}
|
||||
|
||||
.menu__link--active .menu__link {
|
||||
background: var(--hindsight-gradient);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .menu__link--active {
|
||||
background: rgba(45, 212, 191, 0.12);
|
||||
color: var(--ifm-color-primary-light);
|
||||
background: linear-gradient(135deg, rgba(0, 116, 217, 0.15) 0%, rgba(0, 146, 150, 0.15) 100%);
|
||||
}
|
||||
|
||||
/* Non-collapsible category styling */
|
||||
@@ -174,11 +379,16 @@
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
pre,
|
||||
pre code,
|
||||
.prism-code {
|
||||
font-family: 'JetBrains Mono', var(--ifm-font-family-monospace) !important;
|
||||
}
|
||||
|
||||
.prism-code {
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.6;
|
||||
font-family: var(--ifm-font-family-monospace);
|
||||
padding: 1rem !important;
|
||||
}
|
||||
|
||||
@@ -187,7 +397,7 @@ code {
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.2rem 0.45rem;
|
||||
font-size: 0.875em;
|
||||
font-family: var(--ifm-font-family-monospace);
|
||||
font-family: 'JetBrains Mono', var(--ifm-font-family-monospace) !important;
|
||||
background-color: #f1f5f9;
|
||||
color: #0f172a;
|
||||
font-weight: 500;
|
||||
@@ -200,7 +410,7 @@ code {
|
||||
|
||||
/* Don't apply inline styles to code inside pre blocks */
|
||||
pre code {
|
||||
background-color: transparent;
|
||||
background-color: transparent !important;
|
||||
border: none;
|
||||
padding: 0;
|
||||
font-size: inherit;
|
||||
@@ -208,32 +418,36 @@ pre code {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
/* Code block container */
|
||||
/* Code block container - single background source */
|
||||
div[class*="codeBlockContainer"] {
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid var(--ifm-toc-border-color);
|
||||
overflow: hidden;
|
||||
background: #f8fafc;
|
||||
background: #f8fafc !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] div[class*="codeBlockContainer"] {
|
||||
background: #0f172a;
|
||||
background: #0f172a !important;
|
||||
}
|
||||
|
||||
div[class*="codeBlockTitle"] {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-bottom: 1px solid var(--ifm-toc-border-color);
|
||||
font-family: var(--ifm-font-family-monospace);
|
||||
font-family: 'JetBrains Mono', var(--ifm-font-family-monospace);
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* Code block content area */
|
||||
div[class*="codeBlockContent"] {
|
||||
background: #f8fafc;
|
||||
/* Code block content area - transparent to show container bg */
|
||||
div[class*="codeBlockContent"],
|
||||
div[class*="codeBlockContent"] pre,
|
||||
div[class*="codeBlockContent"] .prism-code {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] div[class*="codeBlockContent"] {
|
||||
background: #0f172a;
|
||||
/* Prism token backgrounds */
|
||||
.prism-code span {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* Cards/Features */
|
||||
@@ -246,10 +460,21 @@ div[class*="codeBlockContent"] {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
article h1 {
|
||||
/* Page title with gradient */
|
||||
article h1,
|
||||
.markdown h1,
|
||||
header h1,
|
||||
h1[class*="title"] {
|
||||
font-size: 2.25rem;
|
||||
font-weight: 800;
|
||||
margin-bottom: 1.25rem;
|
||||
background-image: linear-gradient(90deg, #0074d9, #009296) !important;
|
||||
background-size: 100% !important;
|
||||
-webkit-background-clip: text !important;
|
||||
background-clip: text !important;
|
||||
-webkit-text-fill-color: transparent !important;
|
||||
color: transparent !important;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
article h2 {
|
||||
@@ -258,7 +483,9 @@ article h2 {
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 0.75rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid var(--ifm-toc-border-color);
|
||||
border-bottom: 2px solid transparent;
|
||||
border-image: var(--hindsight-gradient);
|
||||
border-image-slice: 1;
|
||||
}
|
||||
|
||||
article h3 {
|
||||
@@ -272,10 +499,93 @@ article p {
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
/* Admonitions */
|
||||
.admonition {
|
||||
border-radius: 0.5rem;
|
||||
border-left-width: 4px;
|
||||
/* Links with gradient */
|
||||
article a:not(.button):not([class*="hash-link"]) {
|
||||
background: var(--hindsight-gradient);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
article a:not(.button):not([class*="hash-link"]):hover {
|
||||
text-decoration: underline;
|
||||
text-decoration-color: var(--hindsight-gradient-start);
|
||||
}
|
||||
|
||||
/* Admonitions - gradient themed */
|
||||
.theme-admonition,
|
||||
[class*="admonition_"] {
|
||||
border-radius: 0.5rem !important;
|
||||
border-left: none !important;
|
||||
border: none !important;
|
||||
background: rgba(0, 116, 217, 0.05) !important;
|
||||
position: relative !important;
|
||||
overflow: hidden !important;
|
||||
padding-left: 1.25rem !important;
|
||||
}
|
||||
|
||||
/* Gradient left border using pseudo-element */
|
||||
.theme-admonition::before,
|
||||
[class*="admonition_"]::before {
|
||||
content: '' !important;
|
||||
position: absolute !important;
|
||||
left: 0 !important;
|
||||
top: 0 !important;
|
||||
bottom: 0 !important;
|
||||
width: 4px !important;
|
||||
background: linear-gradient(180deg, #0074d9, #009296) !important;
|
||||
border-radius: 0.5rem 0 0 0.5rem !important;
|
||||
}
|
||||
|
||||
/* Admonition heading text - gradient */
|
||||
[class*="admonitionHeading_"] {
|
||||
background-image: linear-gradient(90deg, #0074d9, #009296) !important;
|
||||
-webkit-background-clip: text !important;
|
||||
background-clip: text !important;
|
||||
-webkit-text-fill-color: transparent !important;
|
||||
color: transparent !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
/* Admonition icon - gradient start color */
|
||||
[class*="admonitionIcon_"] svg,
|
||||
[class*="admonitionIcon_"] svg path {
|
||||
fill: #0074d9 !important;
|
||||
}
|
||||
|
||||
/* Different admonition types */
|
||||
.alert--info[class*="admonition_"],
|
||||
.theme-admonition-info {
|
||||
background: rgba(0, 116, 217, 0.05) !important;
|
||||
}
|
||||
|
||||
.alert--success[class*="admonition_"],
|
||||
.theme-admonition-tip {
|
||||
background: rgba(0, 146, 150, 0.05) !important;
|
||||
}
|
||||
|
||||
.alert--warning[class*="admonition_"],
|
||||
.theme-admonition-warning {
|
||||
background: rgba(0, 116, 217, 0.08) !important;
|
||||
}
|
||||
|
||||
.alert--secondary[class*="admonition_"],
|
||||
.theme-admonition-note {
|
||||
background: rgba(0, 131, 154, 0.05) !important;
|
||||
}
|
||||
|
||||
/* Dark mode admonitions */
|
||||
[data-theme='dark'] .theme-admonition,
|
||||
[data-theme='dark'] [class*="admonition_"] {
|
||||
background: rgba(0, 131, 154, 0.1) !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .theme-admonition::before,
|
||||
[data-theme='dark'] [class*="admonition_"]::before {
|
||||
background: linear-gradient(180deg, #3396e8, #00b4b8) !important;
|
||||
}
|
||||
|
||||
/* Tables - Compact styling */
|
||||
@@ -318,6 +628,14 @@ th {
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.tabs__item--active {
|
||||
border-bottom-color: var(--hindsight-gradient-start);
|
||||
background: var(--hindsight-gradient);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
|
||||
/* Redoc sidebar - expand all tags by default */
|
||||
[class*="redoc-wrap"] [class*="menu-content"] ul {
|
||||
@@ -329,6 +647,153 @@ th {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Mermaid diagram styling - using high specificity selectors */
|
||||
.mermaid svg[id^="mermaid"] {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* Node shapes - gradient start color (#0074d9 blue) */
|
||||
.mermaid svg[id^="mermaid"] .node rect,
|
||||
.mermaid svg[id^="mermaid"] .node circle,
|
||||
.mermaid svg[id^="mermaid"] .node ellipse,
|
||||
.mermaid svg[id^="mermaid"] .node polygon,
|
||||
.mermaid svg[id^="mermaid"] .node path,
|
||||
svg[id^="mermaid"] .node rect,
|
||||
svg[id^="mermaid"] .node circle,
|
||||
svg[id^="mermaid"] .node ellipse,
|
||||
svg[id^="mermaid"] .node polygon,
|
||||
svg[id^="mermaid"] .node path {
|
||||
fill: #0074d9 !important;
|
||||
stroke: #005db0 !important;
|
||||
}
|
||||
|
||||
/* Node text - white on colored background, Inter font */
|
||||
.mermaid svg[id^="mermaid"] .node .label,
|
||||
.mermaid svg[id^="mermaid"] .nodeLabel,
|
||||
.mermaid svg[id^="mermaid"] .node text,
|
||||
.mermaid svg[id^="mermaid"] .node foreignObject div,
|
||||
.mermaid svg[id^="mermaid"] .node foreignObject span,
|
||||
.mermaid svg[id^="mermaid"] .node foreignObject p,
|
||||
svg[id^="mermaid"] .node .label,
|
||||
svg[id^="mermaid"] .nodeLabel,
|
||||
svg[id^="mermaid"] .node text,
|
||||
svg[id^="mermaid"] .node foreignObject div,
|
||||
svg[id^="mermaid"] .node foreignObject span,
|
||||
svg[id^="mermaid"] .node foreignObject p {
|
||||
color: #ffffff !important;
|
||||
fill: #ffffff !important;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
|
||||
font-size: 0.9375rem !important;
|
||||
}
|
||||
|
||||
/* Edge/arrow lines - gradient end color (#009296 teal) */
|
||||
.mermaid svg[id^="mermaid"] .edgePath .path,
|
||||
.mermaid svg[id^="mermaid"] .flowchart-link,
|
||||
svg[id^="mermaid"] .edgePath .path,
|
||||
svg[id^="mermaid"] .flowchart-link {
|
||||
stroke: #009296 !important;
|
||||
}
|
||||
|
||||
/* Arrow heads - gradient end color */
|
||||
.mermaid svg[id^="mermaid"] .marker path,
|
||||
.mermaid svg[id^="mermaid"] .arrowheadPath,
|
||||
.mermaid svg[id^="mermaid"] marker path,
|
||||
svg[id^="mermaid"] .marker path,
|
||||
svg[id^="mermaid"] .arrowheadPath,
|
||||
svg[id^="mermaid"] marker path {
|
||||
fill: #009296 !important;
|
||||
stroke: #009296 !important;
|
||||
}
|
||||
|
||||
/* Edge labels - transparent background, Inter font */
|
||||
.mermaid svg[id^="mermaid"] .edgeLabel,
|
||||
.mermaid svg[id^="mermaid"] .edgeLabel rect,
|
||||
.mermaid svg[id^="mermaid"] .edgeLabel span,
|
||||
.mermaid svg[id^="mermaid"] .labelBkg,
|
||||
svg[id^="mermaid"] .edgeLabel,
|
||||
svg[id^="mermaid"] .edgeLabel rect,
|
||||
svg[id^="mermaid"] .edgeLabel span,
|
||||
svg[id^="mermaid"] .labelBkg {
|
||||
background: transparent !important;
|
||||
background-color: transparent !important;
|
||||
fill: transparent !important;
|
||||
}
|
||||
|
||||
.mermaid svg[id^="mermaid"] .edgeLabel text,
|
||||
.mermaid svg[id^="mermaid"] .edgeLabel span,
|
||||
.mermaid svg[id^="mermaid"] .edgeLabel p,
|
||||
svg[id^="mermaid"] .edgeLabel text,
|
||||
svg[id^="mermaid"] .edgeLabel span,
|
||||
svg[id^="mermaid"] .edgeLabel p {
|
||||
color: var(--ifm-font-color-base) !important;
|
||||
fill: var(--ifm-font-color-base) !important;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
|
||||
font-size: 0.875rem !important;
|
||||
}
|
||||
|
||||
/* Cluster/subgraph boxes - gradient end color border (#009296) */
|
||||
.mermaid svg[id^="mermaid"] .cluster rect,
|
||||
svg[id^="mermaid"] .cluster rect {
|
||||
fill: rgba(0, 146, 150, 0.08) !important;
|
||||
stroke: #009296 !important;
|
||||
stroke-width: 2px !important;
|
||||
}
|
||||
|
||||
/* Cluster labels - Inter font */
|
||||
.mermaid svg[id^="mermaid"] .cluster text,
|
||||
.mermaid svg[id^="mermaid"] .cluster .nodeLabel,
|
||||
.mermaid svg[id^="mermaid"] .cluster-label text,
|
||||
.mermaid svg[id^="mermaid"] .cluster-label span,
|
||||
.mermaid svg[id^="mermaid"] .cluster-label p,
|
||||
svg[id^="mermaid"] .cluster text,
|
||||
svg[id^="mermaid"] .cluster .nodeLabel,
|
||||
svg[id^="mermaid"] .cluster-label text,
|
||||
svg[id^="mermaid"] .cluster-label span,
|
||||
svg[id^="mermaid"] .cluster-label p {
|
||||
color: var(--ifm-font-color-base) !important;
|
||||
fill: var(--ifm-font-color-base) !important;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
|
||||
font-size: 0.9375rem !important;
|
||||
}
|
||||
|
||||
/* All mermaid text should use Inter */
|
||||
.mermaid svg[id^="mermaid"] text,
|
||||
.mermaid svg[id^="mermaid"] span,
|
||||
.mermaid svg[id^="mermaid"] p,
|
||||
svg[id^="mermaid"] text,
|
||||
svg[id^="mermaid"] span,
|
||||
svg[id^="mermaid"] p {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
|
||||
}
|
||||
|
||||
/* Dark mode adjustments */
|
||||
[data-theme='dark'] .mermaid svg[id^="mermaid"] .edgeLabel text,
|
||||
[data-theme='dark'] .mermaid svg[id^="mermaid"] .edgeLabel span,
|
||||
[data-theme='dark'] .mermaid svg[id^="mermaid"] .edgeLabel p,
|
||||
[data-theme='dark'] svg[id^="mermaid"] .edgeLabel text,
|
||||
[data-theme='dark'] svg[id^="mermaid"] .edgeLabel span,
|
||||
[data-theme='dark'] svg[id^="mermaid"] .edgeLabel p {
|
||||
color: #e2e8f0 !important;
|
||||
fill: #e2e8f0 !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .mermaid svg[id^="mermaid"] .cluster text,
|
||||
[data-theme='dark'] .mermaid svg[id^="mermaid"] .cluster-label text,
|
||||
[data-theme='dark'] .mermaid svg[id^="mermaid"] .cluster-label span,
|
||||
[data-theme='dark'] .mermaid svg[id^="mermaid"] .cluster-label p,
|
||||
[data-theme='dark'] svg[id^="mermaid"] .cluster text,
|
||||
[data-theme='dark'] svg[id^="mermaid"] .cluster-label text,
|
||||
[data-theme='dark'] svg[id^="mermaid"] .cluster-label span,
|
||||
[data-theme='dark'] svg[id^="mermaid"] .cluster-label p {
|
||||
color: #e2e8f0 !important;
|
||||
fill: #e2e8f0 !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .mermaid svg[id^="mermaid"] .cluster rect,
|
||||
[data-theme='dark'] svg[id^="mermaid"] .cluster rect {
|
||||
fill: rgba(0, 146, 150, 0.15) !important;
|
||||
}
|
||||
|
||||
/* List styling */
|
||||
article ul, article ol {
|
||||
font-size: 0.9375rem;
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import ComponentTypes from '@theme-original/NavbarItem/ComponentTypes';
|
||||
import NavbarIconLink from '@site/src/components/NavbarIconLink';
|
||||
|
||||
export default {
|
||||
...ComponentTypes,
|
||||
'custom-iconLink': NavbarIconLink,
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user