Compare commits

..
47 Commits
Author SHA1 Message Date
DK09876 39df7eab99 make transformation less specific 2025-12-15 22:11:53 -06:00
DK09876 68957c428f Make the transformation deterministic 2025-12-15 20:50:47 -06:00
DK09876 034d604e72 Fix module import error 2025-12-15 20:09:45 -06:00
DK09876 308e79551a Add dependency step 2025-12-15 19:51:53 -06:00
DK09876 92fde4abf2 parallelize 2025-12-15 19:29:53 -06:00
DK09876 32bd8796e9 Fix root repo issue 2025-12-15 19:16:29 -06:00
DK09876 948d8291a0 remake test 2025-12-15 19:09:34 -06:00
DK09876 de943e88f2 Improve pre filter 2025-12-15 17:56:38 -06:00
DK09876 64baad5e6c Remake of test 2025-12-15 17:17:34 -06:00
DK09876 05562d3472 Get rid of temperature for reasoning models 2025-12-15 16:47:35 -06:00
DK09876 ef483e39a2 Use smarter model for test 2025-12-15 16:39:42 -06:00
DK09876 48483221ee Verified LLM commands 2025-12-15 16:20:52 -06:00
DK09876 8d8a2453c8 Fix file based tests 2025-12-15 15:55:16 -06:00
DK09876 a7aae18721 Skip pytest samples that are already covered 2025-12-15 15:31:37 -06:00
DK09876 06f71f869d Fix some issues seen with the test and async 2025-12-15 15:03:17 -06:00
DK09876 16e5bcfbea Add dependency discover step in the test 2025-12-15 14:19:16 -06:00
DK09876 1ebb182fa0 Create an actionable summary for the test failures 2025-12-15 13:49:49 -06:00
DK09876 70df6d313c Summarize the failed tests using an LLM call 2025-12-15 13:15:54 -06:00
DK09876 f413175799 Properly emit the summary 2025-12-15 12:59:48 -06:00
DK09876 4ce7af0cd4 directory test fixes 2025-12-15 12:21:52 -06:00
DK09876 d13bb728f8 Create summary of test results 2025-12-15 12:00:55 -06:00
DK09876 bca8dd7c94 Tell the test to install all required dependencies 2025-12-15 11:53:23 -06:00
DK09876 69b1af26ac Run tests in parallel 2025-12-15 11:31:22 -06:00
DK09876 e7ccf0b70c More dependency fixes 2025-12-15 11:09:19 -06:00
DK09876 0b044845f2 Follow patterns from other CI 2025-12-15 10:59:43 -06:00
DK09876 787449620b UV fix 2025-12-15 10:55:01 -06:00
DK09876 a32949a342 Add test to CI testing all code samples 2025-12-15 10:46:54 -06:00
Nicolò Boschi 1cef364719 enable model tests on ci (#29) 2025-12-15 15:18:09 +01:00
Nicolò Boschi dff293ca8c fix doc link styling 2025-12-15 14:54:56 +01:00
Nicolò Boschi f4bc8443b3 changelog generator 2025-12-15 14:46:14 +01:00
Nicolò Boschi ae26a8603b models doc 2025-12-15 11:34:34 +01:00
Nicolò Boschi 183b9dacb4 Release v0.1.5
- Update version to 0.1.5 in all components
- Python packages: hindsight-api, hindsight-dev, hindsight-dev/benchmarks, hindsight-all, hindsight-litellm
- Python client: hindsight-clients/python
- TypeScript client: hindsight-clients/typescript
- Rust CLI: hindsight-cli
- Control Plane: hindsight-control-plane
- Helm chart
2025-12-15 10:48:53 +01:00
Nicolò Boschi 8a7c6e4e91 litellm release integration 2025-12-15 10:48:27 +01:00
DK09876andClaude Opus 4.5 dfccbf29f1 Added hindsight_liteLLM implementation (#17)
* Added hindsight_liteLLM implementation

* Add instructions for entity vs bank id

* Add another line about entity

* Address PR review comments and enhance litellm integration

- Remove deprecated limit parameter from recall() and arecall() functions
  since Hindsight uses budget/max_tokens for result control
- Remove dead MODEL_MAX_OUTPUT_TOKENS dict and max_output_tokens property
  from LLMProvider (superseded by hardcoded max_completion_tokens)
- Add test-litellm-integration job to CI workflow
- Add reflect API support with use_reflect config option
- Add verbose mode debug info via get_last_injection_debug()
- Add entity_id support for multi-user memory isolation
- Add retain() and reflect() wrapper functions
- Update docstrings and examples

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* Make max_memories optional to allow unlimited memory injection

- Change max_memories default from 10 to None (no limit)
- When max_memories is None, all results from the API are used
- Fix recall result handling to properly detect list vs object return
- Update wrappers (OpenAI, Anthropic) with same optional behavior

This allows users to control memory limits via max_memory_tokens
and recall_budget without an artificial count limit.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* Remove entity_id from hindsight_litellm; add gpt-4o token cap

Multi-user support now uses separate bank_ids per user instead of
entity_id scoping (e.g., bank_id=f"user-{user_id}"). This simplifies
the API and aligns with the Hindsight architecture.

Also fixes max_completion_tokens error for gpt-4o models by capping
the value at 16384 (gpt-4o's limit) instead of sending the default
65000 which exceeds the model's supported maximum.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* Fix dark mode styling across Control Plane UI components

Improvements to ensure proper text visibility and contrast in both light
and dark modes:

- Add global CSS rules for datetime-local calendar picker icon visibility
  using filter: invert() for both light (0.5) and dark (1) modes
- Fix text colors in dialog components to use theme-aware foreground colors
- Update memory detail panel, document/chunk modals, and data views to use
  proper dark mode text classes (text-foreground, text-card-foreground)
- Fix form labels, headings, and content text in bank selector dialogs
- Update entities view and documents view table styling for dark mode
- Bump package versions to 0.1.4

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* Remove session_id feature and add How It Works section to README

- Remove session_id and session management (new_session, set_session,
  get_session) from config.py, callbacks.py, and __init__.py
- Session management was a client-only abstraction not backed by core API
- Add "How It Works" section to README with visual flow diagram
- Update README to remove session management documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* Fix readme example

* Add dark mode again

---------

Co-authored-by: Claude Opus 4.5 <[email protected]>
2025-12-15 10:42:19 +01:00
Chris Latimer dfea4dbe15 Trademark to README 2025-12-14 18:58:34 -07:00
Derek Bouius fcea8afa6c Change npm packaging structure and fix contributing info (#16)
* change the package to workspace concept

* add provider name and change default model

* add the node_modules to git ignore

* change the npm runs to use workspace

* fix the start scripts to use the workspace

* update the uv.lock

* updated instructions

* update the docker build to use the npm workspace

* Update package-lock.json after merge to sync workspace dependencies

* fix merge conflict
2025-12-12 14:14:19 -05:00
Nicolò Boschi 94c2b85c81 switch to pg0-embedded (#28)
* switch to pg0-embedded

* switch to pg0-embedded

* stricter mcp lib
2025-12-12 19:13:26 +01:00
Chris Bartholomew 160c5581ec fix: add DOM.Iterable lib to resolve URLSearchParams.entries() type error (#27)
The generated queryKeySerializer.gen.ts uses URLSearchParams.entries() which
requires DOM.Iterable in the TypeScript lib config for proper type definitions.
2025-12-12 17:34:47 +01:00
Nicolò Boschi 70983f5817 fix 400 retries on llm 2025-12-12 17:15:56 +01:00
Chris Latimer 44e9571572 README banner 2025-12-12 09:03:59 -07:00
Nicolò Boschi 7445cef7b7 feat: add optional graph retriever MPFP (#26)
* feat: add optional graph retriever MPFP

* feat: add optional graph retriever MPFP
2025-12-12 16:58:50 +01:00
Derek Bouius f018cc5677 fix: upgrade Next.js to 16.0.10 to patch CVE-2025-55184 and CVE-2025-55183 (#25)
CVE-2025-55184 (High) - Denial of Service via malicious HTTP request
CVE-2025-55183 (Medium) - Source Code Exposure of Server Actions

Reference: https://vercel.com/kb/bulletin/security-bulletin-cve-2025-55184-and-cve-2025-55183
2025-12-12 16:43:26 +01:00
Nicolò Boschi 922164e25c fix recall trace visualization 2025-12-12 14:38:37 +01:00
Derek Bouius d6b7b9b398 Fix base CI issues and the defaults in .env.example (#24)
* Add the LLM_PROVIDER in example

* fix the assert in testing recall

* trial to fix failing client tests

NotImplementedError: Cannot copy out of meta tensor; no data! Please use torch.nn.Module.to_empty() instead of torch.nn.Module.to() when moving module from meta to a different device.

* lock the sentence transformer packages to align with the breaking changes around lazy tensor loading

* Add the LLM_PROVIDER in example

* fix the assert in testing recall

* trial to fix failing client tests

* pre-cache the model so CI doesn't need workarounds

* remove assert that is a race condition

The test was checking that the bank count increased, but with parallel tests (-n 8), other tests can delete their banks while this test is running, causing a race condition. The important assertion is assert test_bank_id in final_banks - which verifies the bank was actually created.

* add debug to figure out why docker build fails sometimes

* use the CPU only version of pytorch to avoid pulling cuda libraries

* add best match strategy to uv

* change the example openai model
2025-12-11 16:48:12 -05:00
Nicolò Boschi 158a6aac9a fix cli installer 2025-12-11 16:26:05 +01:00
Nicolò Boschi 38e73a1414 fix cli installer 2025-12-11 16:22:04 +01:00
Nicolò Boschi 2c1be4cf47 Update Docker run command in README o3 mini 2025-12-11 14:53:52 +01:00
74 changed files with 20767 additions and 16001 deletions
+2 -1
View File
@@ -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)
-11
View File
@@ -1,11 +0,0 @@
name: 'Setup pg0'
description: 'Install pg0 embedded PostgreSQL'
runs:
using: 'composite'
steps:
- name: Install pg0
shell: bash
run: |
curl -fsSL https://raw.githubusercontent.com/vectorize-io/pg0/main/install.sh | bash
echo "$HOME/.pg0/bin" >> $GITHUB_PATH
+3 -6
View File
@@ -20,18 +20,15 @@ concurrency:
jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: hindsight-docs
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: hindsight-docs/package-lock.json
- run: npm ci
- run: npm run build
cache-dependency-path: package-lock.json
- run: npm ci --workspace=hindsight-docs
- run: npm run build --workspace=hindsight-docs
- uses: actions/upload-pages-artifact@v3
with:
path: hindsight-docs/build
+17 -48
View File
@@ -38,6 +38,10 @@ jobs:
working-directory: ./hindsight
run: uv build --out-dir dist
- name: Build hindsight-litellm
working-directory: ./hindsight-integrations/litellm
run: uv build --out-dir dist
# Publish in order (client and api first, then hindsight-all which depends on them)
- name: Publish hindsight-client to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
@@ -57,6 +61,12 @@ jobs:
packages-dir: ./hindsight/dist
skip-existing: true
- name: Publish hindsight-litellm to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: ./hindsight-integrations/litellm/dist
skip-existing: true
# Upload artifacts for GitHub release
- name: Upload artifacts
uses: actions/upload-artifact@v4
@@ -66,6 +76,7 @@ jobs:
hindsight-clients/python/dist/*
hindsight-api/dist/*
hindsight/dist/*
hindsight-integrations/litellm/dist/*
retention-days: 1
release-typescript-client:
@@ -80,14 +91,14 @@ jobs:
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
cache: 'npm'
cache-dependency-path: package-lock.json
- name: Install dependencies
working-directory: ./hindsight-clients/typescript
run: npm ci
run: npm ci --workspace=hindsight-clients/typescript
- name: Build
working-directory: ./hindsight-clients/typescript
run: npm run build
run: npm run build --workspace=hindsight-clients/typescript
- name: Publish to npm
working-directory: ./hindsight-clients/typescript
@@ -306,6 +317,7 @@ jobs:
cp artifacts/python-packages/hindsight-clients/python/dist/* release-assets/ || true
cp artifacts/python-packages/hindsight-api/dist/* release-assets/ || true
cp artifacts/python-packages/hindsight/dist/* release-assets/ || true
cp artifacts/python-packages/hindsight-integrations/litellm/dist/* release-assets/ || true
# TypeScript client
cp artifacts/typescript-client/*.tgz release-assets/ || true
# Rust CLI binaries
@@ -316,54 +328,11 @@ jobs:
cp artifacts/helm-chart/*.tgz release-assets/ || true
ls -la release-assets/
- name: Generate release notes
run: |
cat << 'EOF' > release-notes.md
## Quick Start
```bash
# Install the CLI
curl -fsSL https://raw.githubusercontent.com/vectorize-io/hindsight/refs/heads/main/hindsight-cli/install.sh | bash
# Start the server
docker run -p 8888:8888 -p 9999:9999 \
-e HINDSIGHT_API_LLM_PROVIDER=openai \
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
-e HINDSIGHT_API_LLM_MODEL=gpt-4o-mini \
ghcr.io/${{ github.repository_owner }}/hindsight:${{ steps.get_version.outputs.VERSION }}
```
## Docker Images
- `ghcr.io/${{ github.repository_owner }}/hindsight:${{ steps.get_version.outputs.VERSION }}` - Standalone (recommended)
- `ghcr.io/${{ github.repository_owner }}/hindsight-api:${{ steps.get_version.outputs.VERSION }}` - API only
- `ghcr.io/${{ github.repository_owner }}/hindsight-control-plane:${{ steps.get_version.outputs.VERSION }}` - Web UI only
## CLI
```bash
curl -fsSL https://raw.githubusercontent.com/vectorize-io/hindsight/refs/heads/main/hindsight-cli/install.sh | bash
```
## Python
```bash
pip install hindsight-all # or hindsight-api, hindsight-client
```
## TypeScript/JavaScript
```bash
npm install @vectorize-io/hindsight-client
```
## Helm
```bash
helm install hindsight oci://ghcr.io/${{ github.repository_owner }}/charts/hindsight --version ${{ steps.get_version.outputs.VERSION }}
```
EOF
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
files: release-assets/*
body_path: release-notes.md
generate_release_notes: true
draft: false
prerelease: false
env:
+278 -21
View File
@@ -9,6 +9,54 @@ concurrency:
cancel-in-progress: true
jobs:
build-python-packages:
runs-on: ubuntu-latest
strategy:
matrix:
include:
- name: hindsight-all
path: hindsight
- name: hindsight-api
path: hindsight-api
- name: hindsight-client
path: hindsight-clients/python
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"
- name: Build ${{ matrix.name }}
working-directory: ./${{ matrix.path }}
run: uv build
build-typescript-client:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: package-lock.json
- name: Install dependencies
run: npm ci --workspace=hindsight-clients/typescript
- name: Build TypeScript client
run: npm run build --workspace=hindsight-clients/typescript
build-docs:
runs-on: ubuntu-latest
@@ -19,14 +67,14 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: package-lock.json
- name: Install dependencies
working-directory: ./hindsight-docs
run: npm ci
run: npm ci --workspace=hindsight-docs
- name: Build docs
working-directory: ./hindsight-docs
run: npm run build
run: npm run build --workspace=hindsight-docs
build-rust-cli:
runs-on: ubuntu-latest
@@ -106,8 +154,13 @@ jobs:
env:
HINDSIGHT_API_LLM_PROVIDER: groq
HINDSIGHT_API_LLM_API_KEY: ${{ secrets.GROQ_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_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
@@ -123,16 +176,33 @@ jobs:
with:
python-version-file: ".python-version"
- name: Install pg0
uses: ./.github/actions/setup-pg0
- name: Build API
working-directory: ./hindsight-api
run: uv build
- 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 +216,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
@@ -161,9 +233,6 @@ jobs:
with:
python-version-file: ".python-version"
- name: Install pg0
uses: ./.github/actions/setup-pg0
- name: Build API
working-directory: ./hindsight-api
run: uv build
@@ -174,11 +243,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 +292,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
@@ -243,16 +314,13 @@ jobs:
with:
node-version: '20'
- name: Install pg0
uses: ./.github/actions/setup-pg0
- name: Build API
working-directory: ./hindsight-api
run: uv build
- 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 +373,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
@@ -332,16 +402,13 @@ jobs:
hindsight-clients/rust/target
key: ${{ runner.os }}-cargo-client-${{ hashFiles('hindsight-clients/rust/Cargo.lock') }}
- name: Install pg0
uses: ./.github/actions/setup-pg0
- name: Build API
working-directory: ./hindsight-api
run: uv build
- 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: |
@@ -377,3 +444,193 @@ jobs:
run: |
echo "=== API Server Logs ==="
cat /tmp/api-server.log || echo "No API server log found"
test-litellm-integration:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
prune-cache: false
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"
- name: Build litellm integration
working-directory: ./hindsight-integrations/litellm
run: uv build
- name: Install dependencies
working-directory: ./hindsight-integrations/litellm
run: uv sync --extra dev
- name: Run tests
working-directory: ./hindsight-integrations/litellm
run: uv run pytest tests -v
test-doc-examples:
runs-on: ubuntu-latest
env:
HINDSIGHT_API_LLM_PROVIDER: groq
HINDSIGHT_API_LLM_API_KEY: ${{ secrets.GROQ_API_KEY }}
HINDSIGHT_API_LLM_MODEL: openai/gpt-oss-20b
HINDSIGHT_API_URL: http://localhost:8888
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Model for test generation and analysis (options: gpt-4o, o3-mini, o1, etc.)
DOC_TEST_MODEL: o3-mini
# 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
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
prune-cache: false
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version-file: ".python-version"
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: package-lock.json
- name: Build API
working-directory: ./hindsight-api
run: uv build
- name: Build Python client
working-directory: ./hindsight-clients/python
run: uv build
- name: Install Python client
working-directory: ./hindsight-clients/python
run: uv sync --index-strategy unsafe-best-match
- name: Install API dependencies
working-directory: ./hindsight-api
run: uv sync --no-install-project --index-strategy unsafe-best-match
- name: Install test dependencies in API venv
working-directory: ./hindsight-api
run: |
uv pip install ../hindsight-clients/python requests anthropic
uv pip install ../hindsight-integrations/litellm
uv pip install ../hindsight-integrations/openai
- name: Verify Python dependencies
working-directory: ./hindsight-api
run: |
echo "=== Verifying Python dependencies ==="
uv run python -c "
import sys
print(f'Python: {sys.executable}')
print(f'Prefix: {sys.prefix}')
# Check required packages
packages = [
'hindsight_client',
'hindsight_litellm',
'hindsight_openai',
'anthropic',
'openai',
]
missing = []
for pkg in packages:
try:
__import__(pkg)
print(f' ✓ {pkg}')
except ImportError as e:
print(f' ✗ {pkg}: {e}')
missing.append(pkg)
if missing:
print(f'\nERROR: Missing packages: {missing}')
sys.exit(1)
print('\nAll Python dependencies verified!')
"
- name: Install TypeScript client dependencies
run: npm ci
- name: Build TypeScript client
run: npm run build --workspace=hindsight-clients/typescript
- name: Install TypeScript client globally
working-directory: ./hindsight-clients/typescript
run: npm install -g .
- name: Make TypeScript client available for temp files
run: |
# ESM modules don't use NODE_PATH, so create node_modules in /tmp
# where test scripts are written
mkdir -p /tmp/node_modules/@vectorize-io
ln -s ${{ github.workspace }}/hindsight-clients/typescript /tmp/node_modules/@vectorize-io/hindsight-client
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Build and install hindsight CLI
working-directory: ./hindsight-cli
run: |
cargo build --release
sudo cp target/release/hindsight /usr/local/bin/
- name: Create .env file
run: |
cat > .env << EOF
HINDSIGHT_API_LLM_PROVIDER=${{ env.HINDSIGHT_API_LLM_PROVIDER }}
HINDSIGHT_API_LLM_API_KEY=${{ env.HINDSIGHT_API_LLM_API_KEY }}
HINDSIGHT_API_LLM_MODEL=${{ env.HINDSIGHT_API_LLM_MODEL }}
EOF
- name: Start API server
run: |
./scripts/dev/start-api.sh > /tmp/api-server.log 2>&1 &
echo "Waiting for API server to be ready..."
for i in {1..60}; do
if curl -sf http://localhost:8888/health > /dev/null 2>&1; then
echo "API server is ready after ${i}s"
break
fi
if [ $i -eq 60 ]; then
echo "API server failed to start after 60s"
cat /tmp/api-server.log
exit 1
fi
sleep 1
done
- name: Test documentation examples
working-directory: ./hindsight-api
env:
REPO_ROOT: ${{ github.workspace }}
run: uv run python ../scripts/test-doc-examples.py
- name: Write test summary
if: always()
run: |
echo "=== Documentation Test Summary ==="
cat /tmp/doc-test-summary.md
cat /tmp/doc-test-summary.md >> $GITHUB_STEP_SUMMARY
- name: Show API server logs
if: always()
run: |
echo "=== API Server Logs ==="
cat /tmp/api-server.log || echo "No API server log found"
+3
View File
@@ -9,6 +9,9 @@ wheels/
# Virtual environments
.venv
# Node
node_modules/
# Environment variables
.env
+14 -4
View File
@@ -5,13 +5,23 @@ Thanks for your interest in contributing to Hindsight!
## Getting Started
1. Fork and clone the repository
2. Install dependencies:
```bash
cd hindsight-api && uv sync
git clone [email protected]:vectorize-io/hindsight.git
cd hindsight
```
3. Set up your environment:
2. Set up your environment:
```bash
export OPENAI_API_KEY=your-key
cp .env.example .env
```
Edit the .env to add LLM API key and config as required
3. Install dependencies:
```bash
# Python dependencies
uv sync --directory hindsight-api/
# Node dependencies (uses npm workspaces)
npm install
```
## Development
+4 -3
View File
@@ -1,6 +1,6 @@
<div align="center">
![Hindsight Banner](./hindsight-docs/static/img/banner.webp)
![Hindsight Banner](./hindsight-docs/static/img/banner.svg)
[Documentation](https://vectorize-io.github.io/hindsight) • [Paper](#coming-soon) • [Examples](https://github.com/vectorize-io/hindsight-cookbook)
@@ -18,7 +18,7 @@
## What is Hindsight?
Hindsight is an agent memory system built to create smarter agents that learn over time. It eliminates the shortcomings of alternative techniques such as RAG and knowledge graph.
Hindsight is an agent memory system built to create smarter agents that learn over time. It eliminates the shortcomings of alternative techniques such as RAG and knowledge graph.
Hindsight addresses common challenges that have frustrated AI engineers building agents to automate tasks and assist users with conversational interfaces. Many of these challenges stem directly from a lack of memory.
@@ -56,6 +56,7 @@ export OPENAI_API_KEY=your-key
docker run --rm -it --pull always -p 8888:8888 -p 9999:9999 \
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
-e HINDSIGHT_API_LLM_MODEL=o3-mini \
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
ghcr.io/vectorize-io/hindsight:latest
```
@@ -234,4 +235,4 @@ MIT — see [LICENSE](./LICENSE)
---
Built by [Vectorize.io](https://vectorize.io)
Built by [Vectorize.io](https://vectorize.io)
+21 -60
View File
@@ -54,13 +54,15 @@ FROM node:20-slim AS sdk-builder
ARG INCLUDE_CP
RUN if [ "$INCLUDE_CP" != "true" ]; then echo "Skipping SDK build" && exit 0; fi
WORKDIR /app/sdk
WORKDIR /app
COPY hindsight-clients/typescript/package*.json ./
RUN npm ci
# Copy root package files for npm workspaces
COPY package.json package-lock.json ./
COPY hindsight-clients/typescript/ ./hindsight-clients/typescript/
COPY hindsight-clients/typescript/ ./
RUN npm run build
# Install and build SDK using workspace
RUN npm ci -w @vectorize-io/hindsight-client
RUN npm run build -w @vectorize-io/hindsight-client
# =============================================================================
# Stage: Control Plane Builder
@@ -73,7 +75,7 @@ RUN if [ "$INCLUDE_CP" != "true" ]; then echo "Skipping CP build" && exit 0; fi
WORKDIR /app
# Copy built SDK
COPY --from=sdk-builder /app/sdk /app/sdk
COPY --from=sdk-builder /app/hindsight-clients/typescript /app/sdk
# Install Control Plane dependencies
# Only copy package.json (not package-lock.json) to ensure npm installs
@@ -130,34 +132,10 @@ RUN mkdir -p /app/data && chown -R hindsight:hindsight /app
USER hindsight
# Set PATH for hindsight user
ENV PATH="/home/hindsight/.hindsight/bin:/app/api/.venv/bin:${PATH}"
ENV PATH="/app/api/.venv/bin:${PATH}"
# Install pg0 binary
RUN mkdir -p /home/hindsight/.hindsight/bin && \
ARCH=$(uname -m) && \
if [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then \
PG0_BINARY="pg0-linux-aarch64-gnu"; \
elif [ "$ARCH" = "x86_64" ]; then \
PG0_BINARY="pg0-linux-x86_64-gnu"; \
else \
echo "Unsupported architecture: $ARCH" && exit 1; \
fi && \
echo "Installing pg0 binary: $PG0_BINARY" && \
for i in 1 2 3 4 5; do \
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 && \
break || (echo "Retry $i failed, waiting..." && sleep 10); \
done && \
/home/hindsight/.hindsight/bin/pg0 --version
# Pre-download PostgreSQL binaries
# Pre-cache PostgreSQL binaries by starting/stopping pg0-embedded
ENV PG0_HOME=/home/hindsight/.pg0-cache
RUN pg0 start --help && \
(pg0 start --name hindsight --port 5555 --username hindsight --password hindsight --database hindsight && \
sleep 2 && \
pg0 stop --name hindsight && \
echo "PostgreSQL pre-cached to $PG0_HOME") || echo "Pre-download skipped"
ENV PG0_HOME=/home/hindsight/.pg0
@@ -189,7 +167,7 @@ FROM node:20-alpine AS cp-only
WORKDIR /app
# Copy built SDK
COPY --from=sdk-builder /app/sdk /app/sdk
COPY --from=sdk-builder /app/hindsight-clients/typescript /app/sdk
# Copy Control Plane standalone build
WORKDIR /app/control-plane
@@ -242,7 +220,7 @@ RUN useradd -m -s /bin/bash hindsight
COPY --from=api-builder /app/api /app/api
# Copy built SDK
COPY --from=sdk-builder /app/sdk /app/sdk
COPY --from=sdk-builder /app/hindsight-clients/typescript /app/sdk
# Copy Control Plane standalone build
WORKDIR /app/control-plane
@@ -263,34 +241,17 @@ RUN mkdir -p /app/data && chown -R hindsight:hindsight /app
USER hindsight
# Set PATH for hindsight user
ENV PATH="/home/hindsight/.hindsight/bin:/app/api/.venv/bin:${PATH}"
ENV PATH="/app/api/.venv/bin:${PATH}"
# Install pg0 binary
RUN mkdir -p /home/hindsight/.hindsight/bin && \
ARCH=$(uname -m) && \
if [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then \
PG0_BINARY="pg0-linux-aarch64-gnu"; \
elif [ "$ARCH" = "x86_64" ]; then \
PG0_BINARY="pg0-linux-x86_64-gnu"; \
else \
echo "Unsupported architecture: $ARCH" && exit 1; \
fi && \
echo "Installing pg0 binary: $PG0_BINARY" && \
for i in 1 2 3 4 5; do \
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 && \
break || (echo "Retry $i failed, waiting..." && sleep 10); \
done && \
/home/hindsight/.hindsight/bin/pg0 --version
# Pre-download PostgreSQL binaries
# Pre-cache PostgreSQL binaries by starting/stopping pg0-embedded
ENV PG0_HOME=/home/hindsight/.pg0-cache
RUN pg0 start --help && \
(pg0 start --name hindsight --port 5555 --username hindsight --password hindsight --database hindsight && \
sleep 2 && \
pg0 stop --name hindsight && \
echo "PostgreSQL pre-cached to $PG0_HOME") || echo "Pre-download skipped"
RUN /app/api/.venv/bin/python -c "\
from pg0 import Pg0; \
print('Pre-caching PostgreSQL binaries...'); \
pg = Pg0(name='hindsight', port=5555, username='hindsight', password='hindsight', database='hindsight'); \
pg.start(); \
pg.stop(); \
print('PostgreSQL pre-cached to PG0_HOME')" || echo "Pre-download skipped"
ENV PG0_HOME=/home/hindsight/.pg0
+2 -2
View File
@@ -2,8 +2,8 @@ apiVersion: v2
name: hindsight
description: Hindsight helm chart
type: application
version: 0.1.4
appVersion: "0.1.4"
version: 0.1.5
appVersion: "0.1.5"
keywords:
- ai
- memory
+1 -5
View File
@@ -121,11 +121,7 @@ class MCPMiddleware:
self.app = app
self.memory = memory
self.mcp_server = create_mcp_server(memory)
# Use sse_app - http_app requires lifespan management that's complex with middleware
import warnings
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
self.mcp_app = self.mcp_server.sse_app()
self.mcp_app = self.mcp_server.http_app()
async def __call__(self, scope, receive, send):
if scope["type"] != "http":
+9
View File
@@ -29,6 +29,7 @@ ENV_HOST = "HINDSIGHT_API_HOST"
ENV_PORT = "HINDSIGHT_API_PORT"
ENV_LOG_LEVEL = "HINDSIGHT_API_LOG_LEVEL"
ENV_MCP_ENABLED = "HINDSIGHT_API_MCP_ENABLED"
ENV_GRAPH_RETRIEVER = "HINDSIGHT_API_GRAPH_RETRIEVER"
# Default values
DEFAULT_DATABASE_URL = "pg0"
@@ -45,6 +46,7 @@ DEFAULT_HOST = "0.0.0.0"
DEFAULT_PORT = 8888
DEFAULT_LOG_LEVEL = "info"
DEFAULT_MCP_ENABLED = True
DEFAULT_GRAPH_RETRIEVER = "bfs" # Options: "bfs", "mpfp"
# Required embedding dimension for database schema
EMBEDDING_DIMENSION = 384
@@ -79,6 +81,9 @@ class HindsightConfig:
log_level: str
mcp_enabled: bool
# Recall
graph_retriever: str
@classmethod
def from_env(cls) -> "HindsightConfig":
"""Create configuration from environment variables."""
@@ -107,6 +112,9 @@ class HindsightConfig:
port=int(os.getenv(ENV_PORT, DEFAULT_PORT)),
log_level=os.getenv(ENV_LOG_LEVEL, DEFAULT_LOG_LEVEL),
mcp_enabled=os.getenv(ENV_MCP_ENABLED, str(DEFAULT_MCP_ENABLED)).lower() == "true",
# Recall
graph_retriever=os.getenv(ENV_GRAPH_RETRIEVER, DEFAULT_GRAPH_RETRIEVER),
)
def get_llm_base_url(self) -> str:
@@ -147,6 +155,7 @@ class HindsightConfig:
logger.info(f"LLM: provider={self.llm_provider}, model={self.llm_model}")
logger.info(f"Embeddings: provider={self.embeddings_provider}")
logger.info(f"Reranker: provider={self.reranker_provider}")
logger.info(f"Graph retriever: {self.graph_retriever}")
def get_config() -> HindsightConfig:
@@ -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]:
@@ -175,9 +175,13 @@ class LLMProvider:
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
# For GPT-4o models, cap to 16384
is_gpt4_model = any(x in model_lower for x in ["gpt-4.1", "gpt-4-"])
is_gpt4o_model = "gpt-4o" in model_lower
if max_completion_tokens is not None:
if is_gpt4_model and max_completion_tokens > 32000:
if is_gpt4o_model and max_completion_tokens > 16384:
max_completion_tokens = 16384
elif 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
@@ -268,9 +272,9 @@ class LLMProvider:
raise
except APIStatusError as e:
# Fast fail on 4xx client errors (except 429 rate limit and 498 which is treated as server error)
if 400 <= e.status_code < 500 and e.status_code not in (429, 498):
logger.error(f"Client error (HTTP {e.status_code}), not retrying: {str(e)}")
# Fast fail only on 401 (unauthorized) and 403 (forbidden) - these won't recover with retries
if e.status_code in (401, 403):
logger.error(f"Auth error (HTTP {e.status_code}), not retrying: {str(e)}")
raise
last_exception = e
@@ -408,13 +412,13 @@ class LLMProvider:
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)}")
# Fast fail only on 401 (unauthorized) and 403 (forbidden) - these won't recover with retries
if e.code in (401, 403):
logger.error(f"Gemini auth error (HTTP {e.code}), not retrying: {str(e)}")
raise
# Retry on 429 and 5xx
if e.code in (429, 500, 502, 503, 504):
# Retry on retryable errors (rate limits, server errors, and other client errors like 400)
if e.code in (400, 429, 500, 502, 503, 504) or (e.code and e.code >= 500):
last_exception = e
if attempt < max_retries:
backoff = min(initial_backoff * (2 ** attempt), max_backoff)
@@ -1156,22 +1156,22 @@ class MemoryEngine:
aggregated_timings = {"semantic": 0.0, "bm25": 0.0, "graph": 0.0, "temporal": 0.0}
detected_temporal_constraint = None
for idx, (ft_semantic, ft_bm25, ft_graph, ft_temporal, ft_timings, ft_temporal_constraint) in enumerate(all_retrievals):
for idx, retrieval_result in enumerate(all_retrievals):
# Log fact types in this retrieval batch
ft_name = fact_type[idx] if idx < len(fact_type) else "unknown"
logger.debug(f"[RECALL {recall_id}] Fact type '{ft_name}': semantic={len(ft_semantic)}, bm25={len(ft_bm25)}, graph={len(ft_graph)}, temporal={len(ft_temporal) if ft_temporal else 0}")
logger.debug(f"[RECALL {recall_id}] Fact type '{ft_name}': semantic={len(retrieval_result.semantic)}, bm25={len(retrieval_result.bm25)}, graph={len(retrieval_result.graph)}, temporal={len(retrieval_result.temporal) if retrieval_result.temporal else 0}")
semantic_results.extend(ft_semantic)
bm25_results.extend(ft_bm25)
graph_results.extend(ft_graph)
if ft_temporal:
temporal_results.extend(ft_temporal)
semantic_results.extend(retrieval_result.semantic)
bm25_results.extend(retrieval_result.bm25)
graph_results.extend(retrieval_result.graph)
if retrieval_result.temporal:
temporal_results.extend(retrieval_result.temporal)
# Track max timing for each method (since they run in parallel across fact types)
for method, duration in ft_timings.items():
aggregated_timings[method] = max(aggregated_timings[method], duration)
for method, duration in retrieval_result.timings.items():
aggregated_timings[method] = max(aggregated_timings.get(method, 0.0), duration)
# Capture temporal constraint (same across all fact types)
if ft_temporal_constraint:
detected_temporal_constraint = ft_temporal_constraint
if retrieval_result.temporal_constraint:
detected_temporal_constraint = retrieval_result.temporal_constraint
# If no temporal results from any fact type, set to None
if not temporal_results:
@@ -1203,49 +1203,57 @@ class MemoryEngine:
temporal_info = f" | temporal_range={start_dt.strftime('%Y-%m-%d')} to {end_dt.strftime('%Y-%m-%d')}"
log_buffer.append(f" [2] {total_retrievals}-way retrieval ({len(fact_type)} fact_types): {', '.join(timing_parts)} in {step_duration:.3f}s{temporal_info}")
# Record retrieval results for tracer (convert typed results to old format)
# Record retrieval results for tracer - per fact type
if tracer:
# Convert RetrievalResult to old tuple format for tracer
def to_tuple_format(results):
return [(r.id, r.__dict__) for r in results]
# Add semantic retrieval results
tracer.add_retrieval_results(
method_name="semantic",
results=to_tuple_format(semantic_results),
duration_seconds=aggregated_timings["semantic"],
score_field="similarity",
metadata={"limit": thinking_budget}
)
# Add retrieval results per fact type (to show parallel execution in UI)
for idx, rr in enumerate(all_retrievals):
ft_name = fact_type[idx] if idx < len(fact_type) else "unknown"
# Add BM25 retrieval results
tracer.add_retrieval_results(
method_name="bm25",
results=to_tuple_format(bm25_results),
duration_seconds=aggregated_timings["bm25"],
score_field="bm25_score",
metadata={"limit": thinking_budget}
)
# Add graph retrieval results
tracer.add_retrieval_results(
method_name="graph",
results=to_tuple_format(graph_results),
duration_seconds=aggregated_timings["graph"],
score_field="similarity", # Graph uses similarity for activation
metadata={"budget": thinking_budget}
)
# Add temporal retrieval results if present
if temporal_results:
# Add semantic retrieval results for this fact type
tracer.add_retrieval_results(
method_name="temporal",
results=to_tuple_format(temporal_results),
duration_seconds=aggregated_timings["temporal"],
score_field="temporal_score",
metadata={"budget": thinking_budget}
method_name="semantic",
results=to_tuple_format(rr.semantic),
duration_seconds=rr.timings.get("semantic", 0.0),
score_field="similarity",
metadata={"limit": thinking_budget},
fact_type=ft_name
)
# Add BM25 retrieval results for this fact type
tracer.add_retrieval_results(
method_name="bm25",
results=to_tuple_format(rr.bm25),
duration_seconds=rr.timings.get("bm25", 0.0),
score_field="bm25_score",
metadata={"limit": thinking_budget},
fact_type=ft_name
)
# Add graph retrieval results for this fact type
tracer.add_retrieval_results(
method_name="graph",
results=to_tuple_format(rr.graph),
duration_seconds=rr.timings.get("graph", 0.0),
score_field="activation",
metadata={"budget": thinking_budget},
fact_type=ft_name
)
# Add temporal retrieval results for this fact type (even if empty, to show it ran)
if rr.temporal is not None:
tracer.add_retrieval_results(
method_name="temporal",
results=to_tuple_format(rr.temporal),
duration_seconds=rr.timings.get("temporal", 0.0),
score_field="temporal_score",
metadata={"budget": thinking_budget},
fact_type=ft_name
)
# Record entry points (from semantic results) for legacy graph view
for rank, retrieval in enumerate(semantic_results[:10], start=1): # Top 10 as entry points
tracer.add_entry_point(retrieval.id, retrieval.text, retrieval.similarity or 0.0, rank)
@@ -1287,31 +1295,24 @@ class MemoryEngine:
step_duration = time.time() - step_start
log_buffer.append(f" [4] Reranking: {len(scored_results)} candidates scored in {step_duration:.3f}s")
if tracer:
# Convert to old format for tracer
results_dict = [sr.to_dict() for sr in scored_results]
tracer_merged = [(mc.id, mc.retrieval.__dict__, {"rrf_score": mc.rrf_score, **mc.source_ranks})
for mc in merged_candidates]
tracer.add_reranked(results_dict, tracer_merged)
tracer.add_phase_metric("reranking", step_duration, {
"reranker_type": "cross-encoder",
"candidates_reranked": len(scored_results)
})
# Step 4.5: Combine cross-encoder score with retrieval signals
# This preserves retrieval work (RRF, temporal, recency) instead of pure cross-encoder ranking
if scored_results:
# Normalize RRF scores to [0, 1] range
# Normalize RRF scores to [0, 1] range using min-max normalization
rrf_scores = [sr.candidate.rrf_score for sr in scored_results]
max_rrf = max(rrf_scores) if rrf_scores else 1.0
max_rrf = max(rrf_scores) if rrf_scores else 0.0
min_rrf = min(rrf_scores) if rrf_scores else 0.0
rrf_range = max_rrf - min_rrf if max_rrf > min_rrf else 1.0
rrf_range = max_rrf - min_rrf # Don't force to 1.0, let fallback handle it
# Calculate recency based on occurred_start (more recent = higher score)
now = utcnow()
for sr in scored_results:
# Normalize RRF score
sr.rrf_normalized = (sr.candidate.rrf_score - min_rrf) / rrf_range if rrf_range > 0 else 0.5
# Normalize RRF score (0-1 range, 0.5 if all same)
if rrf_range > 0:
sr.rrf_normalized = (sr.candidate.rrf_score - min_rrf) / rrf_range
else:
# All RRF scores are the same, use neutral value
sr.rrf_normalized = 0.5
# Calculate recency (decay over 365 days, minimum 0.1)
sr.recency = 0.5 # default for missing dates
@@ -1343,6 +1344,17 @@ class MemoryEngine:
scored_results.sort(key=lambda x: x.weight, reverse=True)
log_buffer.append(f" [4.6] Combined scoring: cross_encoder(0.6) + rrf(0.2) + temporal(0.1) + recency(0.1)")
# Add reranked results to tracer AFTER combined scoring (so normalized values are included)
if tracer:
results_dict = [sr.to_dict() for sr in scored_results]
tracer_merged = [(mc.id, mc.retrieval.__dict__, {"rrf_score": mc.rrf_score, **mc.source_ranks})
for mc in merged_candidates]
tracer.add_reranked(results_dict, tracer_merged)
tracer.add_phase_metric("reranking", step_duration, {
"reranker_type": "cross-encoder",
"candidates_reranked": len(scored_results)
})
# Step 5: Truncate to thinking_budget * 2 for token filtering
rerank_limit = thinking_budget * 2
top_scored = scored_results[:rerank_limit]
@@ -3,13 +3,27 @@ Search module for memory retrieval.
Provides modular search architecture:
- Retrieval: 4-way parallel (semantic + BM25 + graph + temporal)
- Graph retrieval: Pluggable strategies (BFS, PPR)
- Reranking: Pluggable strategies (heuristic, cross-encoder)
"""
from .retrieval import retrieve_parallel
from .retrieval import (
retrieve_parallel,
get_default_graph_retriever,
set_default_graph_retriever,
ParallelRetrievalResult,
)
from .graph_retrieval import GraphRetriever, BFSGraphRetriever
from .mpfp_retrieval import MPFPGraphRetriever
from .reranking import CrossEncoderReranker
__all__ = [
"retrieve_parallel",
"get_default_graph_retriever",
"set_default_graph_retriever",
"ParallelRetrievalResult",
"GraphRetriever",
"BFSGraphRetriever",
"MPFPGraphRetriever",
"CrossEncoderReranker",
]
@@ -0,0 +1,235 @@
"""
Graph retrieval strategies for memory recall.
This module provides an abstraction for graph-based memory retrieval,
allowing different algorithms (BFS spreading activation, PPR, etc.) to be
swapped without changing the rest of the recall pipeline.
"""
from abc import ABC, abstractmethod
from typing import List, Optional
from datetime import datetime
import logging
from .types import RetrievalResult
from ..db_utils import acquire_with_retry
logger = logging.getLogger(__name__)
class GraphRetriever(ABC):
"""
Abstract base class for graph-based memory retrieval.
Implementations traverse the memory graph (entity links, temporal links,
causal links) to find relevant facts that might not be found by
semantic or keyword search alone.
"""
@property
@abstractmethod
def name(self) -> str:
"""Return identifier for this retrieval strategy (e.g., 'bfs', 'mpfp')."""
pass
@abstractmethod
async def retrieve(
self,
pool,
query_embedding_str: str,
bank_id: str,
fact_type: str,
budget: int,
query_text: Optional[str] = None,
semantic_seeds: Optional[List[RetrievalResult]] = None,
temporal_seeds: Optional[List[RetrievalResult]] = None,
) -> List[RetrievalResult]:
"""
Retrieve relevant facts via graph traversal.
Args:
pool: Database connection pool
query_embedding_str: Query embedding as string (for finding entry points)
bank_id: Memory bank identifier
fact_type: Fact type to filter ('world', 'experience', 'opinion', 'observation')
budget: Maximum number of nodes to explore/return
query_text: Original query text (optional, for some strategies)
semantic_seeds: Pre-computed semantic entry points (from semantic retrieval)
temporal_seeds: Pre-computed temporal entry points (from temporal retrieval)
Returns:
List of RetrievalResult objects with activation scores set
"""
pass
class BFSGraphRetriever(GraphRetriever):
"""
Graph retrieval using BFS-style spreading activation.
Starting from semantic entry points, spreads activation through
the memory graph (entity, temporal, causal links) using breadth-first
traversal with decaying activation.
This is the original Hindsight graph retrieval algorithm.
"""
def __init__(
self,
entry_point_limit: int = 5,
entry_point_threshold: float = 0.5,
activation_decay: float = 0.8,
min_activation: float = 0.1,
batch_size: int = 20,
):
"""
Initialize BFS graph retriever.
Args:
entry_point_limit: Maximum number of entry points to start from
entry_point_threshold: Minimum semantic similarity for entry points
activation_decay: Decay factor per hop (activation *= decay)
min_activation: Minimum activation to continue spreading
batch_size: Number of nodes to process per batch (for neighbor fetching)
"""
self.entry_point_limit = entry_point_limit
self.entry_point_threshold = entry_point_threshold
self.activation_decay = activation_decay
self.min_activation = min_activation
self.batch_size = batch_size
@property
def name(self) -> str:
return "bfs"
async def retrieve(
self,
pool,
query_embedding_str: str,
bank_id: str,
fact_type: str,
budget: int,
query_text: Optional[str] = None,
semantic_seeds: Optional[List[RetrievalResult]] = None,
temporal_seeds: Optional[List[RetrievalResult]] = None,
) -> List[RetrievalResult]:
"""
Retrieve facts using BFS spreading activation.
Algorithm:
1. Find entry points (top semantic matches above threshold)
2. BFS traversal: visit neighbors, propagate decaying activation
3. Boost causal links (causes, enables, prevents)
4. Return visited nodes up to budget
Note: BFS finds its own entry points via embedding search.
The semantic_seeds and temporal_seeds parameters are accepted
for interface compatibility but not used.
"""
async with acquire_with_retry(pool) as conn:
return await self._retrieve_with_conn(
conn, query_embedding_str, bank_id, fact_type, budget
)
async def _retrieve_with_conn(
self,
conn,
query_embedding_str: str,
bank_id: str,
fact_type: str,
budget: int,
) -> List[RetrievalResult]:
"""Internal implementation with connection."""
# Step 1: Find entry points
entry_points = await conn.fetch(
"""
SELECT id, text, context, event_date, occurred_start, occurred_end,
mentioned_at, access_count, embedding, fact_type, document_id, chunk_id,
1 - (embedding <=> $1::vector) AS similarity
FROM memory_units
WHERE bank_id = $2
AND embedding IS NOT NULL
AND fact_type = $3
AND (1 - (embedding <=> $1::vector)) >= $4
ORDER BY embedding <=> $1::vector
LIMIT $5
""",
query_embedding_str, bank_id, fact_type,
self.entry_point_threshold, self.entry_point_limit
)
if not entry_points:
return []
# Step 2: BFS spreading activation
visited = set()
results = []
queue = [
(RetrievalResult.from_db_row(dict(r)), r["similarity"])
for r in entry_points
]
budget_remaining = budget
while queue and budget_remaining > 0:
# Collect a batch of nodes to process
batch_nodes = []
batch_activations = {}
while queue and len(batch_nodes) < self.batch_size and budget_remaining > 0:
current, activation = queue.pop(0)
unit_id = current.id
if unit_id not in visited:
visited.add(unit_id)
budget_remaining -= 1
current.activation = activation
results.append(current)
batch_nodes.append(current.id)
batch_activations[unit_id] = activation
# Batch fetch neighbors
if batch_nodes and budget_remaining > 0:
max_neighbors = len(batch_nodes) * 20
neighbors = await conn.fetch(
"""
SELECT mu.id, mu.text, mu.context, mu.occurred_start, mu.occurred_end,
mu.mentioned_at, mu.access_count, mu.embedding, mu.fact_type,
mu.document_id, mu.chunk_id,
ml.weight, ml.link_type, ml.from_unit_id
FROM memory_links ml
JOIN memory_units mu ON ml.to_unit_id = mu.id
WHERE ml.from_unit_id = ANY($1::uuid[])
AND ml.weight >= $2
AND mu.fact_type = $3
ORDER BY ml.weight DESC
LIMIT $4
""",
batch_nodes, self.min_activation, fact_type, max_neighbors
)
for n in neighbors:
neighbor_id = str(n["id"])
if neighbor_id not in visited:
parent_id = str(n["from_unit_id"])
parent_activation = batch_activations.get(parent_id, 0.5)
# Boost causal links
link_type = n["link_type"]
base_weight = n["weight"]
if link_type in ("causes", "caused_by"):
causal_boost = 2.0
elif link_type in ("enables", "prevents"):
causal_boost = 1.5
else:
causal_boost = 1.0
effective_weight = base_weight * causal_boost
new_activation = parent_activation * effective_weight * self.activation_decay
if new_activation > self.min_activation:
neighbor_result = RetrievalResult.from_db_row(dict(n))
queue.append((neighbor_result, new_activation))
return results
@@ -0,0 +1,454 @@
"""
Meta-Path Forward Push (MPFP) graph retrieval.
A sublinear graph traversal algorithm for memory retrieval over heterogeneous
graphs with multiple edge types (semantic, temporal, causal, entity).
Combines meta-path patterns from HIN literature with Forward Push local
propagation from Approximate PPR.
Key properties:
- Sublinear in graph size (threshold pruning bounds active nodes)
- Predefined patterns capture different retrieval intents
- All patterns run in parallel, results fused via RRF
- No LLM in the loop during traversal
"""
import asyncio
import logging
from dataclasses import dataclass, field
from typing import List, Dict, Optional, Tuple
from collections import defaultdict
from .types import RetrievalResult
from .graph_retrieval import GraphRetriever
from ..db_utils import acquire_with_retry
logger = logging.getLogger(__name__)
# -----------------------------------------------------------------------------
# Data Classes
# -----------------------------------------------------------------------------
@dataclass
class EdgeTarget:
"""A neighbor node with its edge weight."""
node_id: str
weight: float
@dataclass
class TypedAdjacency:
"""Adjacency lists split by edge type."""
# edge_type -> from_node_id -> list of (to_node_id, weight)
graphs: Dict[str, Dict[str, List[EdgeTarget]]] = field(default_factory=dict)
def get_neighbors(self, edge_type: str, node_id: str) -> List[EdgeTarget]:
"""Get neighbors for a node via a specific edge type."""
return self.graphs.get(edge_type, {}).get(node_id, [])
def get_normalized_neighbors(
self,
edge_type: str,
node_id: str,
top_k: int
) -> List[EdgeTarget]:
"""Get top-k neighbors with weights normalized to sum to 1."""
neighbors = self.get_neighbors(edge_type, node_id)[:top_k]
if not neighbors:
return []
total = sum(n.weight for n in neighbors)
if total == 0:
return []
return [
EdgeTarget(node_id=n.node_id, weight=n.weight / total)
for n in neighbors
]
@dataclass
class PatternResult:
"""Result from a single pattern traversal."""
pattern: List[str]
scores: Dict[str, float] # node_id -> accumulated mass
@dataclass
class MPFPConfig:
"""Configuration for MPFP algorithm."""
alpha: float = 0.15 # teleport/keep probability
threshold: float = 1e-6 # mass pruning threshold (lower = explore more)
top_k_neighbors: int = 20 # fan-out limit per node
# Patterns from semantic seeds
patterns_semantic: List[List[str]] = field(default_factory=lambda: [
['semantic', 'semantic'], # topic expansion
['entity', 'temporal'], # entity timeline
['semantic', 'causes'], # reasoning chains (forward)
['semantic', 'caused_by'], # reasoning chains (backward)
['entity', 'semantic'], # entity context
])
# Patterns from temporal seeds
patterns_temporal: List[List[str]] = field(default_factory=lambda: [
['temporal', 'semantic'], # what was happening then
['temporal', 'entity'], # who was involved then
])
@dataclass
class SeedNode:
"""An entry point node with its initial score."""
node_id: str
score: float # initial mass (e.g., similarity score)
# -----------------------------------------------------------------------------
# Core Algorithm
# -----------------------------------------------------------------------------
def mpfp_traverse(
seeds: List[SeedNode],
pattern: List[str],
adjacency: TypedAdjacency,
config: MPFPConfig,
) -> PatternResult:
"""
Forward Push traversal following a meta-path pattern.
Args:
seeds: Entry point nodes with initial scores
pattern: Sequence of edge types to follow
adjacency: Typed adjacency structure
config: Algorithm parameters
Returns:
PatternResult with accumulated scores per node
"""
if not seeds:
return PatternResult(pattern=pattern, scores={})
scores: Dict[str, float] = {}
# Initialize frontier with seed masses (normalized)
total_seed_score = sum(s.score for s in seeds)
if total_seed_score == 0:
total_seed_score = len(seeds) # fallback to uniform
frontier: Dict[str, float] = {
s.node_id: s.score / total_seed_score for s in seeds
}
# Follow pattern hop by hop
for edge_type in pattern:
next_frontier: Dict[str, float] = {}
for node_id, mass in frontier.items():
if mass < config.threshold:
continue
# Keep α portion for this node
scores[node_id] = scores.get(node_id, 0) + config.alpha * mass
# Push (1-α) to neighbors
push_mass = (1 - config.alpha) * mass
neighbors = adjacency.get_normalized_neighbors(
edge_type, node_id, config.top_k_neighbors
)
for neighbor in neighbors:
next_frontier[neighbor.node_id] = (
next_frontier.get(neighbor.node_id, 0) +
push_mass * neighbor.weight
)
frontier = next_frontier
# Final frontier nodes get their remaining mass
for node_id, mass in frontier.items():
if mass >= config.threshold:
scores[node_id] = scores.get(node_id, 0) + mass
return PatternResult(pattern=pattern, scores=scores)
def rrf_fusion(
results: List[PatternResult],
k: int = 60,
top_k: int = 50,
) -> List[Tuple[str, float]]:
"""
Reciprocal Rank Fusion to combine pattern results.
Args:
results: List of pattern results
k: RRF constant (higher = more uniform weighting)
top_k: Number of results to return
Returns:
List of (node_id, fused_score) tuples, sorted by score descending
"""
fused: Dict[str, float] = {}
for result in results:
if not result.scores:
continue
# Rank nodes by their score in this pattern
ranked = sorted(
result.scores.keys(),
key=lambda n: result.scores[n],
reverse=True
)
for rank, node_id in enumerate(ranked):
fused[node_id] = fused.get(node_id, 0) + 1.0 / (k + rank + 1)
# Sort by fused score and return top-k
sorted_results = sorted(
fused.items(),
key=lambda x: x[1],
reverse=True
)
return sorted_results[:top_k]
# -----------------------------------------------------------------------------
# Database Loading
# -----------------------------------------------------------------------------
async def load_typed_adjacency(pool, bank_id: str) -> TypedAdjacency:
"""
Load all edges for a bank, split by edge type.
Single query, then organize in-memory for fast traversal.
"""
async with acquire_with_retry(pool) as conn:
rows = await conn.fetch(
"""
SELECT ml.from_unit_id, ml.to_unit_id, ml.link_type, ml.weight
FROM memory_links ml
JOIN memory_units mu ON ml.from_unit_id = mu.id
WHERE mu.bank_id = $1
AND ml.weight >= 0.1
ORDER BY ml.from_unit_id, ml.weight DESC
""",
bank_id
)
graphs: Dict[str, Dict[str, List[EdgeTarget]]] = defaultdict(
lambda: defaultdict(list)
)
for row in rows:
from_id = str(row['from_unit_id'])
to_id = str(row['to_unit_id'])
link_type = row['link_type']
weight = row['weight']
graphs[link_type][from_id].append(
EdgeTarget(node_id=to_id, weight=weight)
)
return TypedAdjacency(graphs=dict(graphs))
async def fetch_memory_units_by_ids(
pool,
node_ids: List[str],
fact_type: str,
) -> List[RetrievalResult]:
"""Fetch full memory unit details for a list of node IDs."""
if not node_ids:
return []
async with acquire_with_retry(pool) as conn:
rows = await conn.fetch(
"""
SELECT id, text, context, event_date, occurred_start, occurred_end,
mentioned_at, access_count, embedding, fact_type, document_id, chunk_id
FROM memory_units
WHERE id = ANY($1::uuid[])
AND fact_type = $2
""",
node_ids,
fact_type
)
return [RetrievalResult.from_db_row(dict(r)) for r in rows]
# -----------------------------------------------------------------------------
# Graph Retriever Implementation
# -----------------------------------------------------------------------------
class MPFPGraphRetriever(GraphRetriever):
"""
Graph retrieval using Meta-Path Forward Push.
Runs predefined patterns in parallel from semantic and temporal seeds,
then fuses results via RRF.
"""
def __init__(self, config: Optional[MPFPConfig] = None):
"""
Initialize MPFP retriever.
Args:
config: Algorithm configuration (uses defaults if None)
"""
self.config = config or MPFPConfig()
self._adjacency_cache: Dict[str, TypedAdjacency] = {}
@property
def name(self) -> str:
return "mpfp"
async def retrieve(
self,
pool,
query_embedding_str: str,
bank_id: str,
fact_type: str,
budget: int,
query_text: Optional[str] = None,
semantic_seeds: Optional[List[RetrievalResult]] = None,
temporal_seeds: Optional[List[RetrievalResult]] = None,
) -> List[RetrievalResult]:
"""
Retrieve facts using MPFP algorithm.
Args:
pool: Database connection pool
query_embedding_str: Query embedding (used for fallback seed finding)
bank_id: Memory bank ID
fact_type: Fact type to filter
budget: Maximum results to return
query_text: Original query text (optional)
semantic_seeds: Pre-computed semantic entry points
temporal_seeds: Pre-computed temporal entry points
Returns:
List of RetrievalResult with activation scores
"""
# Load typed adjacency (could cache per bank_id with TTL)
adjacency = await load_typed_adjacency(pool, bank_id)
# Convert seeds to SeedNode format
semantic_seed_nodes = self._convert_seeds(semantic_seeds, 'similarity')
temporal_seed_nodes = self._convert_seeds(temporal_seeds, 'temporal_score')
# If no semantic seeds provided, fall back to finding our own
if not semantic_seed_nodes:
semantic_seed_nodes = await self._find_semantic_seeds(
pool, query_embedding_str, bank_id, fact_type
)
# Run all patterns in parallel
tasks = []
# Patterns from semantic seeds
for pattern in self.config.patterns_semantic:
if semantic_seed_nodes:
tasks.append(
asyncio.to_thread(
mpfp_traverse,
semantic_seed_nodes,
pattern,
adjacency,
self.config,
)
)
# Patterns from temporal seeds
for pattern in self.config.patterns_temporal:
if temporal_seed_nodes:
tasks.append(
asyncio.to_thread(
mpfp_traverse,
temporal_seed_nodes,
pattern,
adjacency,
self.config,
)
)
if not tasks:
return []
# Gather pattern results
pattern_results = await asyncio.gather(*tasks)
# Fuse results
fused = rrf_fusion(pattern_results, top_k=budget)
if not fused:
return []
# Get top result IDs (don't exclude seeds - they may be highly relevant)
result_ids = [node_id for node_id, score in fused][:budget]
# Fetch full details
results = await fetch_memory_units_by_ids(pool, result_ids, fact_type)
# Add activation scores from fusion
score_map = {node_id: score for node_id, score in fused}
for result in results:
result.activation = score_map.get(result.id, 0.0)
# Sort by activation
results.sort(key=lambda r: r.activation or 0, reverse=True)
return results
def _convert_seeds(
self,
seeds: Optional[List[RetrievalResult]],
score_attr: str,
) -> List[SeedNode]:
"""Convert RetrievalResult seeds to SeedNode format."""
if not seeds:
return []
result = []
for seed in seeds:
score = getattr(seed, score_attr, None)
if score is None:
score = seed.activation or seed.similarity or 1.0
result.append(SeedNode(node_id=seed.id, score=score))
return result
async def _find_semantic_seeds(
self,
pool,
query_embedding_str: str,
bank_id: str,
fact_type: str,
limit: int = 20,
threshold: float = 0.3,
) -> List[SeedNode]:
"""Fallback: find semantic seeds via embedding search."""
async with acquire_with_retry(pool) as conn:
rows = await conn.fetch(
"""
SELECT id, 1 - (embedding <=> $1::vector) AS similarity
FROM memory_units
WHERE bank_id = $2
AND embedding IS NOT NULL
AND fact_type = $3
AND (1 - (embedding <=> $1::vector)) >= $4
ORDER BY embedding <=> $1::vector
LIMIT $5
""",
query_embedding_str, bank_id, fact_type, threshold, limit
)
return [
SeedNode(node_id=str(r['id']), score=r['similarity'])
for r in rows
]
@@ -4,15 +4,61 @@ Retrieval module for 4-way parallel search.
Implements:
1. Semantic retrieval (vector similarity)
2. BM25 retrieval (keyword/full-text search)
3. Graph retrieval (spreading activation)
3. Graph retrieval (via pluggable GraphRetriever interface)
4. Temporal retrieval (time-aware search with spreading)
"""
from typing import List, Dict, Any, Tuple, Optional
from typing import List, Dict, Optional
from dataclasses import dataclass, field
from datetime import datetime
import asyncio
import logging
from ..db_utils import acquire_with_retry
from .types import RetrievalResult
from .graph_retrieval import GraphRetriever, BFSGraphRetriever
from .mpfp_retrieval import MPFPGraphRetriever
from ...config import get_config
logger = logging.getLogger(__name__)
@dataclass
class ParallelRetrievalResult:
"""Result from parallel retrieval across all methods."""
semantic: List[RetrievalResult]
bm25: List[RetrievalResult]
graph: List[RetrievalResult]
temporal: Optional[List[RetrievalResult]]
timings: Dict[str, float] = field(default_factory=dict)
temporal_constraint: Optional[tuple] = None # (start_date, end_date)
# Default graph retriever instance (can be overridden)
_default_graph_retriever: Optional[GraphRetriever] = None
def get_default_graph_retriever() -> GraphRetriever:
"""Get or create the default graph retriever based on config."""
global _default_graph_retriever
if _default_graph_retriever is None:
config = get_config()
retriever_type = config.graph_retriever.lower()
if retriever_type == "mpfp":
_default_graph_retriever = MPFPGraphRetriever()
logger.info("Using MPFP graph retriever")
elif retriever_type == "bfs":
_default_graph_retriever = BFSGraphRetriever()
logger.info("Using BFS graph retriever")
else:
logger.warning(f"Unknown graph retriever '{retriever_type}', falling back to MPFP")
_default_graph_retriever = MPFPGraphRetriever()
return _default_graph_retriever
def set_default_graph_retriever(retriever: GraphRetriever) -> None:
"""Set the default graph retriever (for configuration/testing)."""
global _default_graph_retriever
_default_graph_retriever = retriever
async def retrieve_semantic(
@@ -105,121 +151,6 @@ async def retrieve_bm25(
return [RetrievalResult.from_db_row(dict(r)) for r in results]
async def retrieve_graph(
conn,
query_emb_str: str,
bank_id: str,
fact_type: str,
budget: int
) -> List[RetrievalResult]:
"""
Graph retrieval via spreading activation.
Args:
conn: Database connection
query_emb_str: Query embedding as string
agent_id: bank ID
fact_type: Fact type to filter
budget: Node budget for graph traversal
Returns:
List of RetrievalResult objects
"""
# Find entry points
entry_points = await conn.fetch(
"""
SELECT id, text, context, event_date, occurred_start, occurred_end, mentioned_at, access_count, embedding, fact_type, document_id, chunk_id,
1 - (embedding <=> $1::vector) AS similarity
FROM memory_units
WHERE bank_id = $2
AND embedding IS NOT NULL
AND fact_type = $3
AND (1 - (embedding <=> $1::vector)) >= 0.5
ORDER BY embedding <=> $1::vector
LIMIT 5
""",
query_emb_str, bank_id, fact_type
)
if not entry_points:
return []
# BFS-style spreading activation with batched neighbor fetching
visited = set()
results = []
queue = [(RetrievalResult.from_db_row(dict(r)), r["similarity"]) for r in entry_points]
budget_remaining = budget
# Process nodes in batches to reduce DB roundtrips
batch_size = 20 # Fetch neighbors for up to 20 nodes at once
while queue and budget_remaining > 0:
# Collect a batch of nodes to process
batch_nodes = []
batch_activations = {}
while queue and len(batch_nodes) < batch_size and budget_remaining > 0:
current, activation = queue.pop(0)
unit_id = current.id
if unit_id not in visited:
visited.add(unit_id)
budget_remaining -= 1
results.append(current)
batch_nodes.append(current.id)
batch_activations[unit_id] = activation
# Batch fetch neighbors for all nodes in this batch
# Fetch top weighted neighbors (batch_size * 20 = ~400 for good distribution)
if batch_nodes and budget_remaining > 0:
max_neighbors = len(batch_nodes) * 20
neighbors = await conn.fetch(
"""
SELECT mu.id, mu.text, mu.context, mu.occurred_start, mu.occurred_end, mu.mentioned_at,
mu.access_count, mu.embedding, mu.fact_type, mu.document_id, mu.chunk_id,
ml.weight, ml.link_type, ml.from_unit_id
FROM memory_links ml
JOIN memory_units mu ON ml.to_unit_id = mu.id
WHERE ml.from_unit_id = ANY($1::uuid[])
AND ml.weight >= 0.1
AND mu.fact_type = $2
ORDER BY ml.weight DESC
LIMIT $3
""",
batch_nodes, fact_type, max_neighbors
)
for n in neighbors:
neighbor_id = str(n["id"])
if neighbor_id not in visited:
# Get parent activation
parent_id = str(n["from_unit_id"])
activation = batch_activations.get(parent_id, 0.5)
# Boost activation for causal links (they're high-value relationships)
link_type = n["link_type"]
base_weight = n["weight"]
# Causal links get 1.5-2.0x boost depending on type
if link_type in ("causes", "caused_by"):
# Direct causation - very strong relationship
causal_boost = 2.0
elif link_type in ("enables", "prevents"):
# Conditional causation - strong but not as direct
causal_boost = 1.5
else:
# Temporal, semantic, entity links - standard weight
causal_boost = 1.0
effective_weight = base_weight * causal_boost
new_activation = activation * effective_weight * 0.8
if new_activation > 0.1:
neighbor_result = RetrievalResult.from_db_row(dict(n))
queue.append((neighbor_result, new_activation))
return results
async def retrieve_temporal(
conn,
query_emb_str: str,
@@ -419,8 +350,9 @@ async def retrieve_parallel(
fact_type: str,
thinking_budget: int,
question_date: Optional[datetime] = None,
query_analyzer: Optional["QueryAnalyzer"] = None
) -> Tuple[List[RetrievalResult], List[RetrievalResult], List[RetrievalResult], Optional[List[RetrievalResult]], Dict[str, float], Optional[Tuple[datetime, datetime]]]:
query_analyzer: Optional["QueryAnalyzer"] = None,
graph_retriever: Optional[GraphRetriever] = None,
) -> ParallelRetrievalResult:
"""
Run 3-way or 4-way parallel retrieval (adds temporal if detected).
@@ -428,76 +360,318 @@ async def retrieve_parallel(
pool: Database connection pool
query_text: Query text
query_embedding_str: Query embedding as string
agent_id: bank ID
bank_id: Bank ID
fact_type: Fact type to filter
thinking_budget: Budget for graph traversal and retrieval limits
question_date: Optional date when question was asked (for temporal filtering)
query_analyzer: Query analyzer to use (defaults to TransformerQueryAnalyzer)
graph_retriever: Graph retrieval strategy (defaults to configured retriever)
Returns:
Tuple of (semantic_results, bm25_results, graph_results, temporal_results, timings, temporal_constraint)
Each results list contains RetrievalResult objects
temporal_results is None if no temporal constraint detected
timings is a dict with per-method latencies in seconds
temporal_constraint is the (start_date, end_date) tuple if detected, else None
ParallelRetrievalResult with semantic, bm25, graph, temporal results and timings
"""
# Detect temporal constraint
from .temporal_extraction import extract_temporal_constraint
import time
temporal_constraint = extract_temporal_constraint(
query_text, reference_date=question_date, analyzer=query_analyzer
)
# Wrapper to track timing for each retrieval method
async def timed_retrieval(name: str, coro):
start = time.time()
result = await coro
duration = time.time() - start
return result, name, duration
retriever = graph_retriever or get_default_graph_retriever()
async def run_semantic():
async with acquire_with_retry(pool) as conn:
return await retrieve_semantic(conn, query_embedding_str, bank_id, fact_type, limit=thinking_budget)
async def run_bm25():
async with acquire_with_retry(pool) as conn:
return await retrieve_bm25(conn, query_text, bank_id, fact_type, limit=thinking_budget)
async def run_graph():
async with acquire_with_retry(pool) as conn:
return await retrieve_graph(conn, query_embedding_str, bank_id, fact_type, budget=thinking_budget)
async def run_temporal(start_date, end_date):
async with acquire_with_retry(pool) as conn:
return await retrieve_temporal(
conn, query_embedding_str, bank_id, fact_type,
start_date, end_date, budget=thinking_budget, semantic_threshold=0.1
)
# Run retrievals in parallel with timing
timings = {}
if temporal_constraint:
start_date, end_date = temporal_constraint
results = await asyncio.gather(
timed_retrieval("semantic", run_semantic()),
timed_retrieval("bm25", run_bm25()),
timed_retrieval("graph", run_graph()),
timed_retrieval("temporal", run_temporal(start_date, end_date))
if retriever.name == "mpfp":
return await _retrieve_parallel_mpfp(
pool, query_text, query_embedding_str, bank_id, fact_type,
thinking_budget, temporal_constraint, retriever
)
semantic_results, _, timings["semantic"] = results[0]
bm25_results, _, timings["bm25"] = results[1]
graph_results, _, timings["graph"] = results[2]
temporal_results, _, timings["temporal"] = results[3]
else:
results = await asyncio.gather(
timed_retrieval("semantic", run_semantic()),
timed_retrieval("bm25", run_bm25()),
timed_retrieval("graph", run_graph())
return await _retrieve_parallel_bfs(
pool, query_text, query_embedding_str, bank_id, fact_type,
thinking_budget, temporal_constraint, retriever
)
semantic_results, _, timings["semantic"] = results[0]
bm25_results, _, timings["bm25"] = results[1]
graph_results, _, timings["graph"] = results[2]
temporal_results = None
return semantic_results, bm25_results, graph_results, temporal_results, timings, temporal_constraint
@dataclass
class _SemanticGraphResult:
"""Internal result from semantic→graph chain."""
semantic: List[RetrievalResult]
graph: List[RetrievalResult]
semantic_time: float
graph_time: float
@dataclass
class _TimedResult:
"""Internal result with timing."""
results: List[RetrievalResult]
time: float
async def _retrieve_parallel_mpfp(
pool,
query_text: str,
query_embedding_str: str,
bank_id: str,
fact_type: str,
thinking_budget: int,
temporal_constraint: Optional[tuple],
retriever: GraphRetriever,
) -> ParallelRetrievalResult:
"""
MPFP retrieval with optimized parallelization.
Runs 2-3 parallel task chains:
- Task 1: Semantic → Graph (chained, graph uses semantic seeds)
- Task 2: BM25 (independent)
- Task 3: Temporal (if constraint detected)
"""
import time
async def run_semantic_then_graph() -> _SemanticGraphResult:
"""Chain: semantic retrieval → graph retrieval (using semantic as seeds)."""
start = time.time()
async with acquire_with_retry(pool) as conn:
semantic = await retrieve_semantic(
conn, query_embedding_str, bank_id, fact_type, limit=thinking_budget
)
semantic_time = time.time() - start
# Get temporal seeds if needed (quick query, part of this chain)
temporal_seeds = None
if temporal_constraint:
tc_start, tc_end = temporal_constraint
async with acquire_with_retry(pool) as conn:
temporal_seeds = await _get_temporal_entry_points(
conn, query_embedding_str, bank_id, fact_type,
tc_start, tc_end, limit=20
)
# Run graph with seeds
start = time.time()
graph = await retriever.retrieve(
pool=pool,
query_embedding_str=query_embedding_str,
bank_id=bank_id,
fact_type=fact_type,
budget=thinking_budget,
query_text=query_text,
semantic_seeds=semantic,
temporal_seeds=temporal_seeds,
)
graph_time = time.time() - start
return _SemanticGraphResult(semantic, graph, semantic_time, graph_time)
async def run_bm25() -> _TimedResult:
"""Independent BM25 retrieval."""
start = time.time()
async with acquire_with_retry(pool) as conn:
results = await retrieve_bm25(conn, query_text, bank_id, fact_type, limit=thinking_budget)
return _TimedResult(results, time.time() - start)
async def run_temporal(tc_start, tc_end) -> _TimedResult:
"""Temporal retrieval (uses its own entry point finding)."""
start = time.time()
async with acquire_with_retry(pool) as conn:
results = await retrieve_temporal(
conn, query_embedding_str, bank_id, fact_type,
tc_start, tc_end, budget=thinking_budget, semantic_threshold=0.1
)
return _TimedResult(results, time.time() - start)
# Run parallel task chains
if temporal_constraint:
tc_start, tc_end = temporal_constraint
sg_result, bm25_result, temporal_result = await asyncio.gather(
run_semantic_then_graph(),
run_bm25(),
run_temporal(tc_start, tc_end),
)
return ParallelRetrievalResult(
semantic=sg_result.semantic,
bm25=bm25_result.results,
graph=sg_result.graph,
temporal=temporal_result.results,
timings={
"semantic": sg_result.semantic_time,
"graph": sg_result.graph_time,
"bm25": bm25_result.time,
"temporal": temporal_result.time,
},
temporal_constraint=temporal_constraint,
)
else:
sg_result, bm25_result = await asyncio.gather(
run_semantic_then_graph(),
run_bm25(),
)
return ParallelRetrievalResult(
semantic=sg_result.semantic,
bm25=bm25_result.results,
graph=sg_result.graph,
temporal=None,
timings={
"semantic": sg_result.semantic_time,
"graph": sg_result.graph_time,
"bm25": bm25_result.time,
},
temporal_constraint=None,
)
async def _get_temporal_entry_points(
conn,
query_embedding_str: str,
bank_id: str,
fact_type: str,
start_date: datetime,
end_date: datetime,
limit: int = 20,
semantic_threshold: float = 0.1,
) -> List[RetrievalResult]:
"""Get temporal entry points (facts in date range with semantic relevance)."""
from datetime import timezone
if start_date.tzinfo is None:
start_date = start_date.replace(tzinfo=timezone.utc)
if end_date.tzinfo is None:
end_date = end_date.replace(tzinfo=timezone.utc)
rows = await conn.fetch(
"""
SELECT id, text, context, event_date, occurred_start, occurred_end, mentioned_at,
access_count, embedding, fact_type, document_id, chunk_id,
1 - (embedding <=> $1::vector) AS similarity
FROM memory_units
WHERE bank_id = $2
AND fact_type = $3
AND embedding IS NOT NULL
AND (
(occurred_start IS NOT NULL AND occurred_end IS NOT NULL
AND occurred_start <= $5 AND occurred_end >= $4)
OR (mentioned_at IS NOT NULL AND mentioned_at BETWEEN $4 AND $5)
OR (occurred_start IS NOT NULL AND occurred_start BETWEEN $4 AND $5)
OR (occurred_end IS NOT NULL AND occurred_end BETWEEN $4 AND $5)
)
AND (1 - (embedding <=> $1::vector)) >= $6
ORDER BY COALESCE(occurred_start, mentioned_at, occurred_end) DESC,
(embedding <=> $1::vector) ASC
LIMIT $7
""",
query_embedding_str, bank_id, fact_type, start_date, end_date, semantic_threshold, limit
)
results = []
total_days = max((end_date - start_date).total_seconds() / 86400, 1)
mid_date = start_date + (end_date - start_date) / 2
for row in rows:
result = RetrievalResult.from_db_row(dict(row))
# Calculate temporal proximity score
best_date = None
if row["occurred_start"] and row["occurred_end"]:
best_date = row["occurred_start"] + (row["occurred_end"] - row["occurred_start"]) / 2
elif row["occurred_start"]:
best_date = row["occurred_start"]
elif row["occurred_end"]:
best_date = row["occurred_end"]
elif row["mentioned_at"]:
best_date = row["mentioned_at"]
if best_date:
days_from_mid = abs((best_date - mid_date).total_seconds() / 86400)
result.temporal_proximity = 1.0 - min(days_from_mid / (total_days / 2), 1.0)
else:
result.temporal_proximity = 0.5
result.temporal_score = result.temporal_proximity
results.append(result)
return results
async def _retrieve_parallel_bfs(
pool,
query_text: str,
query_embedding_str: str,
bank_id: str,
fact_type: str,
thinking_budget: int,
temporal_constraint: Optional[tuple],
retriever: GraphRetriever,
) -> ParallelRetrievalResult:
"""BFS retrieval: all methods run in parallel (original behavior)."""
import time
async def run_semantic() -> _TimedResult:
start = time.time()
async with acquire_with_retry(pool) as conn:
results = await retrieve_semantic(conn, query_embedding_str, bank_id, fact_type, limit=thinking_budget)
return _TimedResult(results, time.time() - start)
async def run_bm25() -> _TimedResult:
start = time.time()
async with acquire_with_retry(pool) as conn:
results = await retrieve_bm25(conn, query_text, bank_id, fact_type, limit=thinking_budget)
return _TimedResult(results, time.time() - start)
async def run_graph() -> _TimedResult:
start = time.time()
results = await retriever.retrieve(
pool=pool,
query_embedding_str=query_embedding_str,
bank_id=bank_id,
fact_type=fact_type,
budget=thinking_budget,
query_text=query_text,
)
return _TimedResult(results, time.time() - start)
async def run_temporal(tc_start, tc_end) -> _TimedResult:
start = time.time()
async with acquire_with_retry(pool) as conn:
results = await retrieve_temporal(
conn, query_embedding_str, bank_id, fact_type,
tc_start, tc_end, budget=thinking_budget, semantic_threshold=0.1
)
return _TimedResult(results, time.time() - start)
if temporal_constraint:
tc_start, tc_end = temporal_constraint
semantic_r, bm25_r, graph_r, temporal_r = await asyncio.gather(
run_semantic(),
run_bm25(),
run_graph(),
run_temporal(tc_start, tc_end),
)
return ParallelRetrievalResult(
semantic=semantic_r.results,
bm25=bm25_r.results,
graph=graph_r.results,
temporal=temporal_r.results,
timings={
"semantic": semantic_r.time,
"bm25": bm25_r.time,
"graph": graph_r.time,
"temporal": temporal_r.time,
},
temporal_constraint=temporal_constraint,
)
else:
semantic_r, bm25_r, graph_r = await asyncio.gather(
run_semantic(),
run_bm25(),
run_graph(),
)
return ParallelRetrievalResult(
semantic=semantic_r.results,
bm25=bm25_r.results,
graph=graph_r.results,
temporal=None,
timings={
"semantic": semantic_r.time,
"bm25": bm25_r.time,
"graph": graph_r.time,
},
temporal_constraint=None,
)
@@ -108,6 +108,7 @@ class RetrievalResult(BaseModel):
class RetrievalMethodResults(BaseModel):
"""Results from a single retrieval method."""
method_name: Literal["semantic", "bm25", "graph", "temporal"] = Field(description="Name of retrieval method")
fact_type: Optional[str] = Field(default=None, description="Fact type this retrieval was for (world, experience, opinion)")
results: List[RetrievalResult] = Field(description="Retrieved results with ranks")
duration_seconds: float = Field(description="Time taken for this retrieval")
metadata: Dict[str, Any] = Field(default_factory=dict, description="Method-specific metadata")
@@ -289,7 +289,8 @@ class SearchTracer:
results: List[tuple], # List of (doc_id, data) tuples
duration_seconds: float,
score_field: str, # e.g., "similarity", "bm25_score"
metadata: Optional[Dict[str, Any]] = None
metadata: Optional[Dict[str, Any]] = None,
fact_type: Optional[str] = None
):
"""
Record results from a single retrieval method.
@@ -300,6 +301,7 @@ class SearchTracer:
duration_seconds: Time taken for this retrieval
score_field: Field name containing the score in data dict
metadata: Optional metadata about this retrieval method
fact_type: Fact type this retrieval was for (world, experience, opinion)
"""
retrieval_results = []
for rank, (doc_id, data) in enumerate(results, start=1):
@@ -313,7 +315,7 @@ class SearchTracer:
text=data.get("text", ""),
context=data.get("context", ""),
event_date=data.get("event_date"),
fact_type=data.get("fact_type"),
fact_type=data.get("fact_type") or fact_type,
score=score,
score_name=score_field,
)
@@ -322,6 +324,7 @@ class SearchTracer:
self.retrieval_results.append(
RetrievalMethodResults(
method_name=method_name,
fact_type=fact_type,
results=retrieval_results,
duration_seconds=duration_seconds,
metadata=metadata or {},
@@ -367,8 +370,10 @@ class SearchTracer:
rank_change = rrf_rank - rank # Positive = moved up
# Extract score components (only include non-None values)
# Keys from ScoredResult.to_dict(): cross_encoder_score, cross_encoder_score_normalized,
# rrf_normalized, temporal, recency, combined_score, weight
score_components = {}
for key in ["semantic_similarity", "bm25_score", "rrf_score", "recency_normalized", "frequency_normalized", "cross_encoder_score", "cross_encoder_score_normalized"]:
for key in ["cross_encoder_score", "cross_encoder_score_normalized", "rrf_score", "rrf_normalized", "temporal", "recency", "combined_score"]:
if key in result and result[key] is not None:
score_components[key] = result[key]
@@ -31,8 +31,9 @@ class RetrievalResult:
embedding: Optional[List[float]] = None
# Retrieval-specific scores (only one will be set depending on retrieval method)
similarity: Optional[float] = None # Semantic/graph retrieval
similarity: Optional[float] = None # Semantic retrieval
bm25_score: Optional[float] = None # BM25 retrieval
activation: Optional[float] = None # Graph retrieval (spreading activation)
temporal_score: Optional[float] = None # Temporal retrieval
temporal_proximity: Optional[float] = None # Temporal retrieval
@@ -54,6 +55,7 @@ class RetrievalResult:
embedding=row.get("embedding"),
similarity=row.get("similarity"),
bm25_score=row.get("bm25_score"),
activation=row.get("activation"),
temporal_score=row.get("temporal_score"),
temporal_proximity=row.get("temporal_proximity"),
)
@@ -152,6 +154,7 @@ class ScoredResult:
result["cross_encoder_score"] = self.cross_encoder_score
result["cross_encoder_score_normalized"] = self.cross_encoder_score_normalized
result["rrf_normalized"] = self.rrf_normalized
result["temporal"] = self.temporal
result["recency"] = self.recency
result["combined_score"] = self.combined_score
result["weight"] = self.weight
+53 -325
View File
@@ -1,373 +1,116 @@
import asyncio
import json
import logging
import os
import platform
import re
import shutil
import stat
import subprocess
from pathlib import Path
from typing import Optional
import httpx
from pg0 import Pg0
logger = logging.getLogger(__name__)
# pg0 configuration
BINARY_NAME = "pg0"
DEFAULT_PORT = 5555
DEFAULT_USERNAME = "hindsight"
DEFAULT_PASSWORD = "hindsight"
DEFAULT_DATABASE = "hindsight"
def get_platform_binary_name() -> str:
"""Get the appropriate binary name for the current platform.
Supported platforms:
- macOS ARM64 (darwin-aarch64)
- Linux x86_64 (gnu)
- Linux ARM64 (gnu)
- Windows x86_64
"""
system = platform.system().lower()
machine = platform.machine().lower()
# Normalize architecture names
if machine in ("x86_64", "amd64"):
arch = "x86_64"
elif machine in ("arm64", "aarch64"):
arch = "aarch64"
else:
raise RuntimeError(
f"Embedded PostgreSQL is not supported on architecture: {machine}. "
f"Supported architectures: x86_64/amd64 (Linux, Windows), aarch64/arm64 (macOS, Linux)"
)
if system == "darwin" and arch == "aarch64":
return "pg0-darwin-aarch64"
elif system == "linux" and arch == "x86_64":
return "pg0-linux-x86_64-gnu"
elif system == "linux" and arch == "aarch64":
return "pg0-linux-aarch64-gnu"
elif system == "windows" and arch == "x86_64":
return "pg0-windows-x86_64.exe"
else:
raise RuntimeError(
f"Embedded PostgreSQL is not supported on {system}-{arch}. "
f"Supported platforms: darwin-aarch64 (macOS ARM), linux-x86_64-gnu, linux-aarch64-gnu, windows-x86_64"
)
def get_download_url(
version: str = "latest",
repo: str = "vectorize-io/pg0",
) -> str:
"""Get the download URL for pg0 binary."""
binary_name = get_platform_binary_name()
if version == "latest":
return f"https://github.com/{repo}/releases/latest/download/{binary_name}"
else:
return f"https://github.com/{repo}/releases/download/{version}/{binary_name}"
def _find_pg0_binary() -> Optional[Path]:
"""Find pg0 binary in PATH or default install location."""
# First check PATH
pg0_in_path = shutil.which("pg0")
if pg0_in_path:
return Path(pg0_in_path)
# Fall back to default install location
default_path = Path.home() / ".hindsight" / "bin" / "pg0"
if default_path.exists() and os.access(default_path, os.X_OK):
return default_path
return None
class EmbeddedPostgres:
"""
Manages an embedded PostgreSQL server instance using pg0.
This class handles:
- Finding or downloading the pg0 CLI
- Starting/stopping the PostgreSQL server
- Getting the connection URI
Example:
pg = EmbeddedPostgres()
await pg.ensure_installed()
await pg.start()
uri = await pg.get_uri()
# ... use uri with asyncpg ...
await pg.stop()
"""
"""Manages an embedded PostgreSQL server instance using pg0-embedded."""
def __init__(
self,
version: str = "latest",
port: int = DEFAULT_PORT,
username: str = DEFAULT_USERNAME,
password: str = DEFAULT_PASSWORD,
database: str = DEFAULT_DATABASE,
name: str = "hindsight",
**kwargs,
):
"""
Initialize the embedded PostgreSQL manager.
Args:
version: Version of pg0 to download if not found. Defaults to "latest"
port: Port to listen on. Defaults to 5555
username: Username for the database. Defaults to "hindsight"
password: Password for the database. Defaults to "hindsight"
database: Database name to create. Defaults to "hindsight"
name: Instance name for pg0. Defaults to "hindsight"
"""
self.version = version
self.port = port
self.username = username
self.password = password
self.database = database
self.name = name
self._pg0: Optional[Pg0] = None
# Will be set when binary is found/installed
self._binary_path: Optional[Path] = _find_pg0_binary()
@property
def binary_path(self) -> Path:
"""Get the path to the pg0 binary."""
if self._binary_path is None:
# Default install location
return Path.home() / ".hindsight" / "bin" / "pg0"
return self._binary_path
def is_installed(self) -> bool:
"""Check if pg0 is available (in PATH or installed)."""
self._binary_path = _find_pg0_binary()
return self._binary_path is not None
async def ensure_installed(self) -> None:
"""
Ensure pg0 is available.
Checks PATH and default location. If not found, raises an error
instructing the user to install pg0 manually.
"""
if self.is_installed():
logger.debug(f"pg0 found at {self._binary_path}")
return
raise RuntimeError(
"pg0 is not installed. Please install it manually:\n"
" curl -fsSL https://github.com/vectorize-io/pg0/releases/latest/download/pg0-linux-amd64 -o ~/.local/bin/pg0 && chmod +x ~/.local/bin/pg0\n"
"Or visit: https://github.com/vectorize-io/pg0/releases"
)
def _run_command(self, *args: str, capture_output: bool = True) -> subprocess.CompletedProcess:
"""Run a pg0 command synchronously."""
cmd = [str(self.binary_path), *args]
return subprocess.run(cmd, capture_output=capture_output, text=True)
async def _run_command_async(self, *args: str, timeout: int = 120) -> tuple[int, str, str]:
"""Run a pg0 command asynchronously."""
cmd = [str(self.binary_path), *args]
def run_sync():
try:
result = subprocess.run(
cmd,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=timeout,
)
return result.returncode, result.stdout, result.stderr
except subprocess.TimeoutExpired:
return 1, "", "Command timed out"
loop = asyncio.get_event_loop()
return await loop.run_in_executor(None, run_sync)
def _extract_uri_from_output(self, output: str) -> Optional[str]:
"""Extract the PostgreSQL URI from pg0 start output."""
match = re.search(r"Connection URI:\s*(postgresql://[^\s]+)", output)
if match:
return match.group(1)
return None
async def _get_version(self) -> str:
"""Get the pg0 version."""
returncode, stdout, stderr = await self._run_command_async("--version", timeout=10)
if returncode == 0 and stdout:
return stdout.strip()
return "unknown"
def _get_pg0(self) -> Pg0:
if self._pg0 is None:
self._pg0 = Pg0(
name=self.name,
port=self.port,
username=self.username,
password=self.password,
database=self.database,
)
return self._pg0
async def start(self, max_retries: int = 3, retry_delay: float = 2.0) -> str:
"""
Start the PostgreSQL server with retry logic.
Args:
max_retries: Maximum number of start attempts (default: 3)
retry_delay: Initial delay between retries in seconds (default: 2.0)
Returns:
The connection URI for the started server.
Raises:
RuntimeError: If the server fails to start after all retries.
"""
if not self.is_installed():
raise RuntimeError("pg0 is not installed. Call ensure_installed() first.")
# Log pg0 version
version = await self._get_version()
logger.info(f"Starting embedded PostgreSQL with pg0 {version} (name: {self.name}, port: {self.port})...")
"""Start the PostgreSQL server with retry logic."""
logger.info(f"Starting embedded PostgreSQL (name: {self.name}, port: {self.port})...")
pg0 = self._get_pg0()
last_error = None
for attempt in range(1, max_retries + 1):
returncode, stdout, stderr = await self._run_command_async(
"start",
"--name", self.name,
"--port", str(self.port),
"--username", self.username,
"--password", self.password,
"--database", self.database,
timeout=300,
)
# Try to extract URI from output
uri = self._extract_uri_from_output(stdout)
if uri:
logger.info(f"PostgreSQL started on port {self.port}")
return uri
# Check if pg0 info can find the running instance
try:
uri = await self.get_uri()
loop = asyncio.get_event_loop()
info = await loop.run_in_executor(None, pg0.start)
logger.info(f"PostgreSQL started on port {self.port}")
# Construct URI manually since pg0-embedded may return None
uri = info.uri if info and info.uri else f"postgresql://{self.username}:{self.password}@localhost:{self.port}/{self.database}"
return uri
except RuntimeError:
pass
except Exception as e:
last_error = str(e)
if attempt < max_retries:
delay = retry_delay * (2 ** (attempt - 1))
logger.debug(f"pg0 start attempt {attempt}/{max_retries} failed: {last_error}")
logger.debug(f"Retrying in {delay:.1f}s...")
await asyncio.sleep(delay)
else:
logger.debug(f"pg0 start attempt {attempt}/{max_retries} failed: {last_error}")
# Start failed, log and retry
last_error = stderr or f"pg0 start returned exit code {returncode}"
if attempt < max_retries:
delay = retry_delay * (2 ** (attempt - 1))
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.debug(f"pg0 start attempt {attempt}/{max_retries} failed: {last_error.strip()}")
# 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'}"
f"Last error: {last_error}"
)
async def stop(self) -> None:
"""Stop the PostgreSQL server."""
if not self.is_installed():
return
pg0 = self._get_pg0()
logger.info(f"Stopping embedded PostgreSQL (name: {self.name})...")
returncode, stdout, stderr = await self._run_command_async("stop", "--name", self.name)
if returncode != 0:
if "not running" in stderr.lower():
return
raise RuntimeError(f"Failed to stop PostgreSQL: {stderr}")
logger.info("Embedded PostgreSQL stopped")
async def _get_info(self) -> dict:
"""Get info from pg0 using the `info -o json` command."""
if not self.is_installed():
raise RuntimeError("pg0 is not installed.")
returncode, stdout, stderr = await self._run_command_async(
"info", "--name", self.name, "-o", "json"
)
if returncode != 0:
raise RuntimeError(f"Failed to get PostgreSQL info: {stderr}")
try:
return json.loads(stdout.strip())
except json.JSONDecodeError as e:
raise RuntimeError(f"Failed to parse pg0 info output: {e}")
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, pg0.stop)
logger.info("Embedded PostgreSQL stopped")
except Exception as e:
if "not running" in str(e).lower():
return
raise RuntimeError(f"Failed to stop PostgreSQL: {e}")
async def get_uri(self) -> str:
"""Get the connection URI for the PostgreSQL server."""
info = await self._get_info()
uri = info.get("uri")
if not uri:
raise RuntimeError("PostgreSQL server is not running or URI not available")
pg0 = self._get_pg0()
loop = asyncio.get_event_loop()
info = await loop.run_in_executor(None, pg0.info)
# Construct URI manually since pg0-embedded may return None
uri = info.uri if info and info.uri else f"postgresql://{self.username}:{self.password}@localhost:{self.port}/{self.database}"
return uri
async def status(self) -> dict:
"""Get the status of the PostgreSQL server."""
if not self.is_installed():
return {"installed": False, "running": False}
try:
info = await self._get_info()
return {
"installed": True,
"running": info.get("running", False),
"uri": info.get("uri"),
}
except RuntimeError:
return {"installed": True, "running": False}
async def is_running(self) -> bool:
"""Check if the PostgreSQL server is currently running."""
if not self.is_installed():
return False
try:
info = await self._get_info()
return info.get("running", False)
except RuntimeError:
pg0 = self._get_pg0()
loop = asyncio.get_event_loop()
info = await loop.run_in_executor(None, pg0.info)
return info is not None and info.running
except Exception:
return False
async def ensure_running(self) -> str:
"""
Ensure the PostgreSQL server is running.
Installs if needed, starts if not running.
Returns:
The connection URI.
"""
await self.ensure_installed()
"""Ensure the PostgreSQL server is running, starting it if needed."""
if await self.is_running():
return await self.get_uri()
return await self.start()
def uninstall(self) -> None:
"""Remove the pg0 binary (only if we installed it)."""
default_path = Path.home() / ".hindsight" / "bin" / "pg0"
if default_path.exists():
default_path.unlink()
logger.info(f"Removed {default_path}")
def clear_data(self) -> None:
"""Remove all PostgreSQL data (destructive!)."""
result = self._run_command("drop", "--name", self.name, "--force")
if result.returncode == 0:
logger.info(f"Dropped pg0 instance {self.name}")
else:
logger.warning(f"Failed to drop pg0 instance {self.name}: {result.stderr}")
# Convenience functions
_default_instance: Optional[EmbeddedPostgres] = None
@@ -375,33 +118,18 @@ _default_instance: Optional[EmbeddedPostgres] = None
def get_embedded_postgres() -> EmbeddedPostgres:
"""Get or create the default EmbeddedPostgres instance."""
global _default_instance
if _default_instance is None:
_default_instance = EmbeddedPostgres()
return _default_instance
async def start_embedded_postgres() -> str:
"""
Quick start function for embedded PostgreSQL.
Downloads, installs, and starts PostgreSQL in one call.
Returns:
Connection URI string
Example:
db_url = await start_embedded_postgres()
conn = await asyncpg.connect(db_url)
"""
pg = get_embedded_postgres()
return await pg.ensure_running()
"""Quick start function for embedded PostgreSQL."""
return await get_embedded_postgres().ensure_running()
async def stop_embedded_postgres() -> None:
"""Stop the default embedded PostgreSQL instance."""
global _default_instance
if _default_instance:
await _default_instance.stop()
+6 -5
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "hindsight-api"
version = "0.1.4"
version = "0.1.5"
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,11 +24,12 @@ 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",
"fastmcp>=2.3.0",
"pg0-embedded>=0.1.0",
"python-dateutil>=2.8.0",
"opentelemetry-api>=1.20.0",
"opentelemetry-sdk>=1.20.0",
@@ -0,0 +1,323 @@
"""
Tests for combined scoring functionality.
Verifies that:
1. RRF scores are properly normalized to [0, 1] range
2. Combined scoring formula is applied correctly
3. Tracer captures normalized values (not raw values)
"""
import pytest
from datetime import datetime, timezone
from hindsight_api.engine.search.types import RetrievalResult, MergedCandidate, ScoredResult
from hindsight_api.engine.memory_engine import Budget
class TestRRFNormalization:
"""Test that RRF scores are properly normalized."""
def test_rrf_normalized_range(self):
"""RRF normalized values should be in [0, 1] range, not raw [0.04, 0.06]."""
# Simulate RRF scores like what we get from actual retrieval
raw_rrf_scores = [0.0607, 0.0550, 0.0480, 0.0390]
max_rrf = max(raw_rrf_scores)
min_rrf = min(raw_rrf_scores)
rrf_range = max_rrf - min_rrf
normalized = []
for score in raw_rrf_scores:
if rrf_range > 0:
norm = (score - min_rrf) / rrf_range
else:
norm = 0.5
normalized.append(norm)
# Verify normalized values are in [0, 1]
for i, norm in enumerate(normalized):
assert 0.0 <= norm <= 1.0, f"Normalized RRF {norm} not in [0, 1] for raw {raw_rrf_scores[i]}"
# Highest raw should be 1.0
assert normalized[0] == 1.0, f"Highest RRF should normalize to 1.0, got {normalized[0]}"
# Lowest raw should be 0.0
assert normalized[-1] == 0.0, f"Lowest RRF should normalize to 0.0, got {normalized[-1]}"
def test_rrf_all_same_scores(self):
"""When all RRF scores are the same, normalized should be 0.5 (neutral)."""
raw_rrf_scores = [0.0500, 0.0500, 0.0500]
max_rrf = max(raw_rrf_scores)
min_rrf = min(raw_rrf_scores)
rrf_range = max_rrf - min_rrf
normalized = []
for score in raw_rrf_scores:
if rrf_range > 0:
norm = (score - min_rrf) / rrf_range
else:
norm = 0.5 # Neutral value when all same
normalized.append(norm)
# All should be 0.5 when scores are identical
for norm in normalized:
assert norm == 0.5, f"Expected 0.5 for identical scores, got {norm}"
class TestCombinedScoringFormula:
"""Test that the combined scoring formula is applied correctly."""
def test_combined_score_calculation(self):
"""Verify the weighted combination: 0.6*CE + 0.2*RRF + 0.1*temporal + 0.1*recency."""
# Test case 1: All components at 1.0
ce_norm = 1.0
rrf_norm = 1.0
temporal = 1.0
recency = 1.0
expected = 0.6 * ce_norm + 0.2 * rrf_norm + 0.1 * temporal + 0.1 * recency
assert expected == 1.0, f"All 1.0 should give 1.0, got {expected}"
# Test case 2: All components at 0.0
ce_norm = 0.0
rrf_norm = 0.0
temporal = 0.0
recency = 0.0
expected = 0.6 * ce_norm + 0.2 * rrf_norm + 0.1 * temporal + 0.1 * recency
assert expected == 0.0, f"All 0.0 should give 0.0, got {expected}"
# Test case 3: High CE, low RRF (cross-encoder finds something retrieval missed)
ce_norm = 0.999
rrf_norm = 0.0 # Lowest in set
temporal = 0.5
recency = 0.5
expected = 0.6 * ce_norm + 0.2 * rrf_norm + 0.1 * temporal + 0.1 * recency
# 0.5994 + 0.0 + 0.05 + 0.05 = 0.6994
assert abs(expected - 0.6994) < 0.001, f"Expected ~0.6994, got {expected}"
# Test case 4: Medium CE, high RRF (retrieval consensus)
ce_norm = 0.8
rrf_norm = 1.0 # Highest in set
temporal = 0.5
recency = 0.5
expected = 0.6 * ce_norm + 0.2 * rrf_norm + 0.1 * temporal + 0.1 * recency
# 0.48 + 0.2 + 0.05 + 0.05 = 0.78
assert abs(expected - 0.78) < 0.001, f"Expected ~0.78, got {expected}"
def test_rrf_contribution_is_significant(self):
"""Verify RRF actually contributes to the final score (not negligible)."""
# Same CE, different RRF
ce_norm = 0.8
temporal = 0.5
recency = 0.5
# Low RRF
score_low_rrf = 0.6 * ce_norm + 0.2 * 0.0 + 0.1 * temporal + 0.1 * recency
# High RRF
score_high_rrf = 0.6 * ce_norm + 0.2 * 1.0 + 0.1 * temporal + 0.1 * recency
# Difference should be 0.2 (20% contribution)
diff = score_high_rrf - score_low_rrf
assert abs(diff - 0.2) < 0.001, f"RRF should contribute 0.2 difference, got {diff}"
@pytest.mark.asyncio
async def test_trace_has_normalized_rrf(memory):
"""Integration test: verify trace contains normalized RRF values, not raw."""
bank_id = f"test_scoring_{datetime.now(timezone.utc).timestamp()}"
try:
# Store multiple memories to ensure different RRF scores
await memory.retain_async(
bank_id=bank_id,
content="Python is a programming language created by Guido van Rossum",
context="tech facts",
)
await memory.retain_async(
bank_id=bank_id,
content="JavaScript was created by Brendan Eich at Netscape",
context="tech facts",
)
await memory.retain_async(
bank_id=bank_id,
content="The Eiffel Tower is located in Paris, France",
context="geography facts",
)
await memory.retain_async(
bank_id=bank_id,
content="Mount Everest is the tallest mountain on Earth",
context="geography facts",
)
# Search with tracing
result = await memory.recall_async(
bank_id=bank_id,
query="programming languages",
fact_type=["world"],
budget=Budget.LOW,
max_tokens=1024,
enable_trace=True,
)
assert result.trace is not None, "Trace should be present"
trace = result.trace
# Check reranked results have proper score_components
assert "reranked" in trace, "Trace should have reranked results"
assert len(trace["reranked"]) > 0, "Should have reranked results"
has_valid_rrf = False
has_valid_temporal = False
has_valid_recency = False
for r in trace["reranked"]:
sc = r.get("score_components", {})
# Check RRF normalized is present and in valid range
if "rrf_normalized" in sc:
rrf_norm = sc["rrf_normalized"]
assert 0.0 <= rrf_norm <= 1.0, f"rrf_normalized {rrf_norm} should be in [0, 1]"
# Should NOT be raw RRF score (which would be ~0.04-0.06)
# A normalized value of exactly 0.0 or 1.0 is valid (min/max of set)
# But raw scores like 0.0607 should never appear as normalized
if rrf_norm > 0.1: # Any value > 0.1 is likely properly normalized
has_valid_rrf = True
# Check temporal is present and in valid range
if "temporal" in sc:
temporal = sc["temporal"]
assert 0.0 <= temporal <= 1.0, f"temporal {temporal} should be in [0, 1]"
has_valid_temporal = True
# Check recency is present and in valid range
if "recency" in sc:
recency = sc["recency"]
assert 0.0 <= recency <= 1.0, f"recency {recency} should be in [0, 1]"
has_valid_recency = True
# At least some results should have these components
# (might not have rrf > 0.1 if all scores are same, which is fine)
assert has_valid_temporal, "Should have temporal scores in trace"
assert has_valid_recency, "Should have recency scores in trace"
print("\n✓ Combined scoring trace test passed!")
print(f" - Reranked results: {len(trace['reranked'])}")
if trace["reranked"]:
sc = trace["reranked"][0].get("score_components", {})
print(f" - First result score components: {sc}")
finally:
await memory.delete_bank(bank_id)
@pytest.mark.asyncio
async def test_rrf_normalized_not_raw_in_trace(memory):
"""Verify that raw RRF scores (0.04-0.06 range) don't appear as normalized values."""
bank_id = f"test_rrf_raw_{datetime.now(timezone.utc).timestamp()}"
try:
# Store enough memories to get varied RRF scores
for i in range(5):
await memory.retain_async(
bank_id=bank_id,
content=f"Test fact number {i} about various topics",
context="test context",
)
result = await memory.recall_async(
bank_id=bank_id,
query="test fact",
fact_type=["world"],
budget=Budget.LOW,
max_tokens=512,
enable_trace=True,
)
trace = result.trace
assert trace is not None
# Check that rrf_normalized values are NOT in the raw range
raw_rrf_range = (0.01, 0.08) # Raw RRF scores are typically in this range
for r in trace.get("reranked", []):
sc = r.get("score_components", {})
if "rrf_normalized" in sc and "rrf_score" in sc:
rrf_norm = sc["rrf_normalized"]
rrf_raw = sc["rrf_score"]
# Raw should be in the typical range
assert raw_rrf_range[0] <= rrf_raw <= raw_rrf_range[1], \
f"Raw RRF {rrf_raw} should be in typical range {raw_rrf_range}"
# Normalized should either be:
# - 0.0 (min in set)
# - 1.0 (max in set)
# - 0.5 (all same)
# - Something in between (0.0 to 1.0)
# But NOT the same as raw (which would indicate no normalization)
if len(trace["reranked"]) > 1:
# If we have multiple results, normalized should differ from raw
# (unless by coincidence, which is very unlikely)
assert rrf_norm != rrf_raw, \
f"Normalized RRF ({rrf_norm}) should differ from raw ({rrf_raw})"
print("\n✓ RRF raw vs normalized test passed!")
finally:
await memory.delete_bank(bank_id)
@pytest.mark.asyncio
async def test_combined_score_matches_components(memory):
"""Verify the final score actually equals the weighted sum of components."""
bank_id = f"test_combined_{datetime.now(timezone.utc).timestamp()}"
try:
await memory.retain_async(
bank_id=bank_id,
content="The quick brown fox jumps over the lazy dog",
context="test",
)
await memory.retain_async(
bank_id=bank_id,
content="A quick test of the emergency broadcast system",
context="test",
)
result = await memory.recall_async(
bank_id=bank_id,
query="quick test",
fact_type=["world"],
budget=Budget.LOW,
max_tokens=512,
enable_trace=True,
)
trace = result.trace
assert trace is not None
for r in trace.get("reranked", []):
sc = r.get("score_components", {})
final_score = r.get("rerank_score", 0)
# Get components (use defaults if missing)
ce = sc.get("cross_encoder_score_normalized", 0)
rrf = sc.get("rrf_normalized", 0.5)
tmp = sc.get("temporal", 0.5)
rec = sc.get("recency", 0.5)
# Calculate expected score
expected = 0.6 * ce + 0.2 * rrf + 0.1 * tmp + 0.1 * rec
# Allow small floating point difference
assert abs(final_score - expected) < 0.01, \
f"Final score {final_score} doesn't match expected {expected} from components"
print("\n✓ Combined score verification test passed!")
finally:
await memory.delete_bank(bank_id)
@@ -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
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "hindsight-cli"
version = "0.1.4"
version = "0.1.5"
edition = "2021"
authors = ["Hindsight Team"]
description = "A beautiful CLI for Hindsight - semantic memory system"
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "hindsight-client"
version = "0.1.4"
version = "0.1.5"
description = "Python client for Hindsight - Semantic memory system with personality-driven thinking"
authors = [
{name = "Hindsight Team"}
@@ -239,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(
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@vectorize-io/hindsight-client",
"version": "0.1.4",
"version": "0.1.5",
"description": "TypeScript client for Hindsight - Semantic memory system with personality-driven thinking",
"main": "./dist/src/index.js",
"types": "./dist/src/index.d.ts",
+1 -1
View File
@@ -2,7 +2,7 @@
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020", "DOM"],
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"declaration": true,
"outDir": "./dist",
"rootDir": "./",
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "hindsight-control-plane",
"version": "0.1.4",
"version": "0.1.5",
"private": true,
"scripts": {
"dev": "next dev",
@@ -36,7 +36,7 @@
"eslint": "^9.39.1",
"eslint-config-next": "^16.0.1",
"lucide-react": "^0.553.0",
"next": "^16.0.7",
"next": "^16.0.10",
"postcss": "^8.5.6",
"react": "^19.2.0",
"react-chrono": "^2.9.1",
@@ -180,4 +180,13 @@ code, pre {
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Fix datetime-local calendar icon visibility in both light and dark modes */
input[type="datetime-local"]::-webkit-calendar-picker-indicator {
filter: invert(0.5);
}
.dark input[type="datetime-local"]::-webkit-calendar-picker-indicator {
filter: invert(1);
}
+1 -1
View File
@@ -18,7 +18,7 @@ export default function RootLayout({
}>) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<body className="bg-background text-foreground">
<ThemeProvider>
<BankProvider>
{children}
@@ -239,7 +239,7 @@ export function BankProfileView() {
<div className="flex gap-2">
{editMode ? (
<>
<Button onClick={handleCancel} variant="outline" disabled={saving}>
<Button onClick={handleCancel} variant="secondary" disabled={saving}>
Cancel
</Button>
<Button onClick={handleSave} disabled={saving}>
@@ -258,7 +258,7 @@ export function BankProfileView() {
</>
) : (
<>
<Button onClick={loadData} variant="outline" size="sm">
<Button onClick={loadData} variant="secondary" size="sm">
<RefreshCw className="w-4 h-4 mr-2" />
Refresh
</Button>
@@ -266,7 +266,7 @@ function BankSelectorInner() {
</div>
<DialogFooter>
<Button
variant="outline"
variant="secondary"
onClick={() => {
setCreateDialogOpen(false);
setNewBankId('');
@@ -295,7 +295,7 @@ function BankSelectorInner() {
</DialogHeader>
<div className="py-4 space-y-4">
<div>
<label className="font-bold block mb-1 text-sm">Content *</label>
<label className="font-bold block mb-1 text-sm text-foreground">Content *</label>
<Textarea
value={docContent}
onChange={(e) => setDocContent(e.target.value)}
@@ -306,7 +306,7 @@ function BankSelectorInner() {
</div>
<div>
<label className="font-bold block mb-1 text-sm">Context</label>
<label className="font-bold block mb-1 text-sm text-foreground">Context</label>
<Input
type="text"
value={docContext}
@@ -317,16 +317,17 @@ function BankSelectorInner() {
<div className="grid grid-cols-2 gap-4">
<div>
<label className="font-bold block mb-1 text-sm">Event Date</label>
<label className="font-bold block mb-1 text-sm text-foreground">Event Date</label>
<Input
type="datetime-local"
value={docEventDate}
onChange={(e) => setDocEventDate(e.target.value)}
className="text-foreground"
/>
</div>
<div>
<label className="font-bold block mb-1 text-sm">Document ID</label>
<label className="font-bold block mb-1 text-sm text-foreground">Document ID</label>
<Input
type="text"
value={docDocumentId}
@@ -342,7 +343,7 @@ function BankSelectorInner() {
checked={docAsync}
onCheckedChange={(checked) => setDocAsync(checked as boolean)}
/>
<label htmlFor="async-doc" className="text-sm cursor-pointer">
<label htmlFor="async-doc" className="text-sm cursor-pointer text-foreground">
Process in background (async)
</label>
</div>
@@ -353,7 +354,7 @@ function BankSelectorInner() {
</div>
<DialogFooter>
<Button
variant="outline"
variant="secondary"
onClick={() => {
setDocDialogOpen(false);
setDocContent('');
@@ -480,7 +480,7 @@ export function DataView({ factType }: DataViewProps) {
}`}
>
<TableCell className="py-2">
<div className="line-clamp-2 text-sm leading-snug">{row.text}</div>
<div className="line-clamp-2 text-sm leading-snug text-foreground">{row.text}</div>
{row.context && (
<div className="text-xs text-muted-foreground mt-0.5 truncate">{row.context}</div>
)}
@@ -506,10 +506,10 @@ export function DataView({ factType }: DataViewProps) {
<span className="text-xs text-muted-foreground">-</span>
)}
</TableCell>
<TableCell className="text-xs py-2">
<TableCell className="text-xs py-2 text-foreground">
{occurredDisplay || <span className="text-muted-foreground">-</span>}
</TableCell>
<TableCell className="text-xs py-2">
<TableCell className="text-xs py-2 text-foreground">
{mentionedDisplay || <span className="text-muted-foreground">-</span>}
</TableCell>
<TableCell className="py-2">
@@ -519,7 +519,7 @@ export function DataView({ factType }: DataViewProps) {
copyToClipboard(row.id);
}}
size="sm"
variant="ghost"
variant="secondary"
className="h-6 w-6 p-0"
title="Copy ID"
>
@@ -799,7 +799,7 @@ function TimelineView({ data, filteredRows }: { data: any; filteredRows: any[] }
{/* Zoom controls */}
<div className="flex items-center border border-border rounded mr-2">
<Button
variant="ghost"
variant="secondary"
size="sm"
onClick={zoomOut}
disabled={granularity === 'year'}
@@ -808,11 +808,11 @@ function TimelineView({ data, filteredRows }: { data: any; filteredRows: any[] }
>
<ZoomOut className="h-3 w-3" />
</Button>
<span className="text-[10px] px-2 min-w-[50px] text-center border-x border-border">
<span className="text-[10px] px-2 min-w-[50px] text-center border-x border-border text-foreground">
{granularityLabels[granularity]}
</span>
<Button
variant="ghost"
variant="secondary"
size="sm"
onClick={zoomIn}
disabled={granularity === 'day'}
@@ -826,7 +826,7 @@ function TimelineView({ data, filteredRows }: { data: any; filteredRows: any[] }
{/* Navigation controls */}
<div className="flex items-center border border-border rounded">
<Button
variant="ghost"
variant="secondary"
size="sm"
onClick={() => scrollToGroup(0)}
disabled={timelineGroups.length <= 1}
@@ -836,7 +836,7 @@ function TimelineView({ data, filteredRows }: { data: any; filteredRows: any[] }
<ChevronsLeft className="h-3 w-3" />
</Button>
<Button
variant="ghost"
variant="secondary"
size="sm"
onClick={() => scrollToGroup(currentIndex - 1)}
disabled={currentIndex === 0}
@@ -845,11 +845,11 @@ function TimelineView({ data, filteredRows }: { data: any; filteredRows: any[] }
>
<ChevronLeft className="h-3 w-3" />
</Button>
<span className="text-[10px] px-2 min-w-[60px] text-center border-x border-border">
<span className="text-[10px] px-2 min-w-[60px] text-center border-x border-border text-foreground">
{currentIndex + 1} / {timelineGroups.length}
</span>
<Button
variant="ghost"
variant="secondary"
size="sm"
onClick={() => scrollToGroup(currentIndex + 1)}
disabled={currentIndex >= timelineGroups.length - 1}
@@ -859,7 +859,7 @@ function TimelineView({ data, filteredRows }: { data: any; filteredRows: any[] }
<ChevronRight className="h-3 w-3" />
</Button>
<Button
variant="ghost"
variant="secondary"
size="sm"
onClick={() => scrollToGroup(timelineGroups.length - 1)}
disabled={timelineGroups.length <= 1}
@@ -94,7 +94,7 @@ export function DocumentChunkModal({ type, id, onClose }: DocumentChunkModalProp
<div className="text-xs font-bold text-muted-foreground uppercase mb-1">
Document ID
</div>
<div className="text-sm font-mono break-all">{data.id}</div>
<div className="text-sm font-mono break-all text-foreground">{data.id}</div>
</div>
{data.created_at && (
<div className="grid grid-cols-2 gap-3">
@@ -102,7 +102,7 @@ export function DocumentChunkModal({ type, id, onClose }: DocumentChunkModalProp
<div className="text-xs font-bold text-muted-foreground uppercase mb-1">
Created
</div>
<div className="text-sm">
<div className="text-sm text-foreground">
{new Date(data.created_at).toLocaleString()}
</div>
</div>
@@ -110,7 +110,7 @@ export function DocumentChunkModal({ type, id, onClose }: DocumentChunkModalProp
<div className="text-xs font-bold text-muted-foreground uppercase mb-1">
Memory Units
</div>
<div className="text-sm">{data.memory_unit_count}</div>
<div className="text-sm text-foreground">{data.memory_unit_count}</div>
</div>
</div>
)}
@@ -119,7 +119,7 @@ export function DocumentChunkModal({ type, id, onClose }: DocumentChunkModalProp
<div className="text-xs font-bold text-muted-foreground uppercase mb-1">
Text Length
</div>
<div className="text-sm">
<div className="text-sm text-foreground">
{data.original_text.length.toLocaleString()} characters
</div>
</div>
@@ -132,7 +132,7 @@ export function DocumentChunkModal({ type, id, onClose }: DocumentChunkModalProp
Original Text
</div>
<div className="p-4 bg-muted rounded-lg border border-border max-h-[300px] overflow-y-auto">
<pre className="text-sm whitespace-pre-wrap font-mono">
<pre className="text-sm whitespace-pre-wrap font-mono text-foreground">
{data.original_text}
</pre>
</div>
@@ -146,7 +146,7 @@ export function DocumentChunkModal({ type, id, onClose }: DocumentChunkModalProp
<div className="text-xs font-bold text-muted-foreground uppercase mb-1">
Chunk ID
</div>
<div className="text-sm font-mono break-all">
<div className="text-sm font-mono break-all text-foreground">
{data.chunk_id}
</div>
</div>
@@ -155,7 +155,7 @@ export function DocumentChunkModal({ type, id, onClose }: DocumentChunkModalProp
<div className="text-xs font-bold text-muted-foreground uppercase mb-1">
Document ID
</div>
<div className="text-sm font-mono break-all">
<div className="text-sm font-mono break-all text-foreground">
{data.document_id}
</div>
</div>
@@ -163,7 +163,7 @@ export function DocumentChunkModal({ type, id, onClose }: DocumentChunkModalProp
<div className="text-xs font-bold text-muted-foreground uppercase mb-1">
Chunk Index
</div>
<div className="text-sm">{data.chunk_index}</div>
<div className="text-sm text-foreground">{data.chunk_index}</div>
</div>
</div>
{data.created_at && (
@@ -171,7 +171,7 @@ export function DocumentChunkModal({ type, id, onClose }: DocumentChunkModalProp
<div className="text-xs font-bold text-muted-foreground uppercase mb-1">
Created
</div>
<div className="text-sm">
<div className="text-sm text-foreground">
{new Date(data.created_at).toLocaleString()}
</div>
</div>
@@ -181,7 +181,7 @@ export function DocumentChunkModal({ type, id, onClose }: DocumentChunkModalProp
<div className="text-xs font-bold text-muted-foreground uppercase mb-1">
Text Length
</div>
<div className="text-sm">
<div className="text-sm text-foreground">
{data.chunk_text.length.toLocaleString()} characters
</div>
</div>
@@ -194,7 +194,7 @@ export function DocumentChunkModal({ type, id, onClose }: DocumentChunkModalProp
Chunk Text
</div>
<div className="p-4 bg-muted rounded-lg border border-border max-h-[300px] overflow-y-auto">
<pre className="text-sm whitespace-pre-wrap font-mono">
<pre className="text-sm whitespace-pre-wrap font-mono text-foreground">
{data.chunk_text}
</pre>
</div>
@@ -122,17 +122,17 @@ export function DocumentsView() {
className={`cursor-pointer hover:bg-muted/50 ${selectedDocument?.id === doc.id ? 'bg-primary/10' : ''}`}
onClick={() => viewDocumentText(doc.id)}
>
<TableCell title={doc.id}>
<TableCell title={doc.id} className="text-card-foreground">
{doc.id.length > 30 ? doc.id.substring(0, 30) + '...' : doc.id}
</TableCell>
<TableCell>
<TableCell className="text-card-foreground">
{doc.created_at ? new Date(doc.created_at).toLocaleString() : 'N/A'}
</TableCell>
<TableCell>
<TableCell className="text-card-foreground">
{doc.retain_params?.context || '-'}
</TableCell>
<TableCell>{doc.text_length?.toLocaleString()} chars</TableCell>
<TableCell>{doc.memory_unit_count}</TableCell>
<TableCell className="text-card-foreground">{doc.text_length?.toLocaleString()} chars</TableCell>
<TableCell className="text-card-foreground">{doc.memory_unit_count}</TableCell>
<TableCell>
<Button
onClick={(e) => {
@@ -140,7 +140,7 @@ export function DocumentsView() {
viewDocumentText(doc.id);
}}
size="sm"
variant={selectedDocument?.id === doc.id ? 'default' : 'outline'}
variant={selectedDocument?.id === doc.id ? 'default' : 'secondary'}
title="View original text"
>
View Text
@@ -171,7 +171,7 @@ export function DocumentsView() {
<p className="text-sm text-muted-foreground mt-1">Original document text and metadata</p>
</div>
<Button
variant="outline"
variant="secondary"
size="sm"
onClick={() => setSelectedDocument(null)}
className="h-9 px-3 gap-2"
@@ -193,7 +193,7 @@ export function DocumentsView() {
{/* Document ID */}
<div className="p-4 bg-muted/50 rounded-lg">
<div className="text-xs font-bold text-muted-foreground uppercase mb-2">Document ID</div>
<div className="text-sm font-mono break-all">{selectedDocument.id}</div>
<div className="text-sm font-mono break-all text-card-foreground">{selectedDocument.id}</div>
</div>
{/* Created & Memory Units */}
@@ -201,11 +201,11 @@ export function DocumentsView() {
<div className="grid grid-cols-2 gap-4">
<div className="p-4 bg-muted/50 rounded-lg">
<div className="text-xs font-bold text-muted-foreground uppercase mb-2">Created</div>
<div className="text-sm font-medium">{new Date(selectedDocument.created_at).toLocaleString()}</div>
<div className="text-sm font-medium text-card-foreground">{new Date(selectedDocument.created_at).toLocaleString()}</div>
</div>
<div className="p-4 bg-muted/50 rounded-lg">
<div className="text-xs font-bold text-muted-foreground uppercase mb-2">Memory Units</div>
<div className="text-sm font-medium">{selectedDocument.memory_unit_count}</div>
<div className="text-sm font-medium text-card-foreground">{selectedDocument.memory_unit_count}</div>
</div>
</div>
)}
@@ -214,7 +214,7 @@ export function DocumentsView() {
{selectedDocument.original_text && (
<div className="p-4 bg-muted/50 rounded-lg">
<div className="text-xs font-bold text-muted-foreground uppercase mb-2">Text Length</div>
<div className="text-sm font-medium">{selectedDocument.original_text.length.toLocaleString()} characters</div>
<div className="text-sm font-medium text-card-foreground">{selectedDocument.original_text.length.toLocaleString()} characters</div>
</div>
)}
@@ -222,7 +222,7 @@ export function DocumentsView() {
{selectedDocument.retain_params && (
<div className="p-4 bg-muted/50 rounded-lg">
<div className="text-xs font-bold text-muted-foreground uppercase mb-2">Retain Parameters</div>
<div className="text-sm space-y-2">
<div className="text-sm space-y-2 text-card-foreground">
{selectedDocument.retain_params.context && (
<div><span className="font-semibold">Context:</span> {selectedDocument.retain_params.context}</div>
)}
@@ -232,7 +232,7 @@ export function DocumentsView() {
{selectedDocument.retain_params.metadata && (
<div className="mt-2">
<span className="font-semibold">Metadata:</span>
<pre className="mt-1 text-xs bg-background p-2 rounded">{JSON.stringify(selectedDocument.retain_params.metadata, null, 2)}</pre>
<pre className="mt-1 text-xs bg-background p-2 rounded text-card-foreground">{JSON.stringify(selectedDocument.retain_params.metadata, null, 2)}</pre>
</div>
)}
</div>
@@ -244,7 +244,7 @@ export function DocumentsView() {
<div>
<div className="text-xs font-bold text-muted-foreground uppercase mb-2">Original Text</div>
<div className="p-4 bg-muted/50 rounded-lg border border-border max-h-[400px] overflow-y-auto">
<pre className="text-sm whitespace-pre-wrap font-mono leading-relaxed">{selectedDocument.original_text}</pre>
<pre className="text-sm whitespace-pre-wrap font-mono leading-relaxed text-card-foreground">{selectedDocument.original_text}</pre>
</div>
</div>
)}
@@ -92,9 +92,9 @@ export function EntitiesView() {
};
return (
<div className="flex gap-4">
<div>
{/* Entity List */}
<div className="flex-1">
<div>
{loading ? (
<div className="flex items-center justify-center py-20">
<div className="text-center">
@@ -111,7 +111,6 @@ export function EntitiesView() {
<Table>
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead>Name</TableHead>
<TableHead>Mentions</TableHead>
<TableHead>First Seen</TableHead>
@@ -123,15 +122,14 @@ export function EntitiesView() {
<TableRow
key={entity.id}
onClick={() => loadEntityDetail(entity.id)}
className={`cursor-pointer ${
selectedEntity?.id === entity.id ? 'bg-accent' : ''
className={`cursor-pointer hover:bg-muted/50 ${
selectedEntity?.id === entity.id ? 'bg-primary/10' : ''
}`}
>
<TableCell className="text-xs text-muted-foreground font-mono" title={entity.id}>{entity.id.slice(0, 8)}...</TableCell>
<TableCell className="font-medium">{entity.canonical_name}</TableCell>
<TableCell>{entity.mention_count}</TableCell>
<TableCell>{formatDate(entity.first_seen)}</TableCell>
<TableCell>{formatDate(entity.last_seen)}</TableCell>
<TableCell className="font-medium text-card-foreground">{entity.canonical_name}</TableCell>
<TableCell className="text-card-foreground">{entity.mention_count}</TableCell>
<TableCell className="text-card-foreground">{formatDate(entity.first_seen)}</TableCell>
<TableCell className="text-card-foreground">{formatDate(entity.last_seen)}</TableCell>
</TableRow>
))}
</TableBody>
@@ -149,60 +147,81 @@ export function EntitiesView() {
)}
</div>
{/* Entity Detail Panel */}
{/* Entity Detail Panel - Fixed overlay */}
{selectedEntity && (
<div className="w-96 bg-card border-2 border-primary rounded-lg p-4">
<div className="flex justify-between items-start mb-4">
<h3 className="text-lg font-bold text-card-foreground">{selectedEntity.canonical_name}</h3>
<Button
variant="ghost"
size="sm"
onClick={() => setSelectedEntity(null)}
>
X
</Button>
</div>
<div className="text-sm text-muted-foreground mb-4">
<div className="font-mono text-xs mb-1" title={selectedEntity.id}>ID: {selectedEntity.id}</div>
<div>Mentions: {selectedEntity.mention_count}</div>
<div>First seen: {formatDate(selectedEntity.first_seen)}</div>
<div>Last seen: {formatDate(selectedEntity.last_seen)}</div>
</div>
<div className="mb-4">
<div className="flex justify-between items-center mb-2">
<h4 className="font-bold text-card-foreground">Observations</h4>
<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">
<div className="p-5">
{/* Header */}
<div className="flex justify-between items-center mb-6 pb-4 border-b border-border">
<div>
<h3 className="text-xl font-bold text-card-foreground">{selectedEntity.canonical_name}</h3>
<p className="text-sm text-muted-foreground mt-1">Entity details</p>
</div>
<Button
onClick={regenerateObservations}
disabled={regenerating}
variant="secondary"
variant="ghost"
size="sm"
onClick={() => setSelectedEntity(null)}
className="h-8 w-8 p-0"
>
{regenerating ? 'Regenerating...' : 'Regenerate'}
<span className="text-lg">×</span>
</Button>
</div>
{loadingDetail ? (
<div className="text-muted-foreground text-sm">Loading observations...</div>
) : selectedEntity.observations && selectedEntity.observations.length > 0 ? (
<ul className="space-y-2">
{selectedEntity.observations.map((obs, idx) => (
<li key={idx} className="p-2 bg-muted rounded text-sm">
<div>{obs.text}</div>
{obs.mentioned_at && (
<div className="text-xs text-muted-foreground mt-1">
{formatDate(obs.mentioned_at)}
</div>
)}
</li>
))}
</ul>
) : (
<div className="text-muted-foreground text-sm">
No observations yet. Click &quot;Regenerate&quot; to generate observations from facts.
<div className="space-y-5">
{/* Entity Info */}
<div className="grid grid-cols-2 gap-4">
<div className="p-4 bg-muted/50 rounded-lg">
<div className="text-xs font-bold text-muted-foreground uppercase mb-2">Mentions</div>
<div className="text-lg font-semibold text-card-foreground">{selectedEntity.mention_count}</div>
</div>
<div className="p-4 bg-muted/50 rounded-lg">
<div className="text-xs font-bold text-muted-foreground uppercase mb-2">First Seen</div>
<div className="text-sm font-medium text-card-foreground">{formatDate(selectedEntity.first_seen)}</div>
</div>
</div>
)}
{/* ID */}
<div className="p-4 bg-muted/50 rounded-lg">
<div className="text-xs font-bold text-muted-foreground uppercase mb-2">Entity ID</div>
<code className="text-xs font-mono break-all text-muted-foreground">{selectedEntity.id}</code>
</div>
{/* Observations */}
<div>
<div className="flex justify-between items-center mb-3">
<div className="text-xs font-bold text-muted-foreground uppercase">Observations</div>
<Button
onClick={regenerateObservations}
disabled={regenerating}
variant="outline"
size="sm"
>
{regenerating ? 'Regenerating...' : 'Regenerate'}
</Button>
</div>
{loadingDetail ? (
<div className="text-muted-foreground text-sm">Loading observations...</div>
) : selectedEntity.observations && selectedEntity.observations.length > 0 ? (
<ul className="space-y-2">
{selectedEntity.observations.map((obs, idx) => (
<li key={idx} className="p-3 bg-muted/50 rounded-lg">
<div className="text-sm text-card-foreground">{obs.text}</div>
{obs.mentioned_at && (
<div className="text-xs text-muted-foreground mt-2">
{formatDate(obs.mentioned_at)}
</div>
)}
</li>
))}
</ul>
) : (
<div className="text-muted-foreground text-sm p-4 bg-muted/50 rounded-lg">
No observations yet. Click &quot;Regenerate&quot; to generate observations from facts.
</div>
)}
</div>
</div>
</div>
</div>
)}
@@ -49,6 +49,9 @@ export function MemoryDetailPanel({
if (!memory) return null;
// Handle both 'id' and 'node_id' (trace results use node_id)
const memoryId = memory.id || memory.node_id;
const labelSize = compact ? 'text-[10px]' : 'text-xs';
const textSize = compact ? 'text-xs' : 'text-sm';
@@ -64,7 +67,7 @@ export function MemoryDetailPanel({
<p className="text-sm text-muted-foreground mt-1">Full memory content and metadata</p>
</div>
<Button
variant="ghost"
variant="secondary"
size="sm"
onClick={onClose}
className="h-8 w-8 p-0"
@@ -77,14 +80,14 @@ export function MemoryDetailPanel({
{/* Full Text */}
<div>
<div className="text-xs font-bold text-muted-foreground uppercase mb-2">Full Text</div>
<div className="text-sm whitespace-pre-wrap leading-relaxed">{memory.text}</div>
<div className="text-sm whitespace-pre-wrap leading-relaxed text-foreground">{memory.text}</div>
</div>
{/* Context */}
{memory.context && (
<div className="p-4 bg-muted/50 rounded-lg">
<div className="text-xs font-bold text-muted-foreground uppercase mb-2">Context</div>
<div className="text-sm">{memory.context}</div>
<div className="text-sm text-foreground">{memory.context}</div>
</div>
)}
@@ -92,7 +95,7 @@ export function MemoryDetailPanel({
<div className="grid grid-cols-2 gap-4">
<div className="p-4 bg-muted/50 rounded-lg">
<div className="text-xs font-bold text-muted-foreground uppercase mb-2">Occurred</div>
<div className="text-sm font-medium">
<div className="text-sm font-medium text-foreground">
{memory.occurred_start
? new Date(memory.occurred_start).toLocaleString()
: 'N/A'}
@@ -100,7 +103,7 @@ export function MemoryDetailPanel({
</div>
<div className="p-4 bg-muted/50 rounded-lg">
<div className="text-xs font-bold text-muted-foreground uppercase mb-2">Mentioned</div>
<div className="text-sm font-medium">
<div className="text-sm font-medium text-foreground">
{memory.mentioned_at
? new Date(memory.mentioned_at).toLocaleString()
: 'N/A'}
@@ -129,24 +132,26 @@ export function MemoryDetailPanel({
)}
{/* ID */}
<div className="p-4 bg-muted/50 rounded-lg">
<div className="text-xs font-bold text-muted-foreground uppercase mb-2">Memory ID</div>
<div className="flex items-center gap-2">
<code className="text-xs font-mono break-all flex-1 text-muted-foreground">{memory.id}</code>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0 flex-shrink-0"
onClick={() => copyToClipboard(memory.id)}
>
{copiedId === memory.id ? (
<Check className="h-4 w-4 text-green-600" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
{memoryId && (
<div className="p-4 bg-muted/50 rounded-lg">
<div className="text-xs font-bold text-muted-foreground uppercase mb-2">Memory ID</div>
<div className="flex items-center gap-2">
<code className="text-xs font-mono break-all flex-1 text-muted-foreground">{memoryId}</code>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0 flex-shrink-0"
onClick={() => copyToClipboard(memoryId)}
>
{copiedId === memoryId ? (
<Check className="h-4 w-4 text-green-600" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
</div>
</div>
</div>
)}
{/* Document/Chunk buttons */}
{(memory.document_id || memory.chunk_id) && (
@@ -154,7 +159,7 @@ export function MemoryDetailPanel({
{memory.document_id && (
<Button
onClick={() => openDocumentModal(memory.document_id)}
variant="outline"
variant="secondary"
className="flex-1"
>
View Document
@@ -163,7 +168,7 @@ export function MemoryDetailPanel({
{memory.chunk_id && (
<Button
onClick={() => openChunkModal(memory.chunk_id)}
variant="outline"
variant="secondary"
className="flex-1"
>
View Chunk
@@ -267,24 +272,26 @@ export function MemoryDetailPanel({
)}
{/* ID */}
<div className={`${compact ? 'p-2' : 'p-3'} bg-muted rounded-lg`}>
<div className={`${labelSize} font-bold text-muted-foreground uppercase mb-1`}>Memory ID</div>
<div className="flex items-center gap-2">
<span className={`${compact ? 'text-[10px]' : 'text-sm'} font-mono break-all`}>{memory.id}</span>
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 flex-shrink-0"
onClick={() => copyToClipboard(memory.id)}
>
{copiedId === memory.id ? (
<Check className="h-3 w-3 text-green-600" />
) : (
<Copy className="h-3 w-3" />
)}
</Button>
{memoryId && (
<div className={`${compact ? 'p-2' : 'p-3'} bg-muted rounded-lg`}>
<div className={`${labelSize} font-bold text-muted-foreground uppercase mb-1`}>Memory ID</div>
<div className="flex items-center gap-2">
<span className={`${compact ? 'text-[10px]' : 'text-sm'} font-mono break-all`}>{memoryId}</span>
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 flex-shrink-0"
onClick={() => copyToClipboard(memoryId)}
>
{copiedId === memoryId ? (
<Check className="h-3 w-3 text-green-600" />
) : (
<Copy className="h-3 w-3" />
)}
</Button>
</div>
</div>
</div>
)}
{/* Document/Chunk buttons */}
{(memory.document_id || memory.chunk_id) && (
@@ -293,7 +300,7 @@ export function MemoryDetailPanel({
<Button
onClick={() => openDocumentModal(memory.document_id)}
size="sm"
variant="outline"
variant="secondary"
className={`flex-1 ${compact ? 'h-7 text-xs' : ''}`}
>
View Document
@@ -303,7 +310,7 @@ export function MemoryDetailPanel({
<Button
onClick={() => openChunkModal(memory.chunk_id)}
size="sm"
variant="outline"
variant="secondary"
className={`flex-1 ${compact ? 'h-7 text-xs' : ''}`}
>
View Chunk
@@ -9,7 +9,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
import { Checkbox } from '@/components/ui/checkbox';
import { Label } from '@/components/ui/label';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Search, Clock, Zap, ChevronRight, Database, FileText, Users } from 'lucide-react';
import { Search, Clock, Zap, ChevronRight, ChevronDown, Database, FileText, Users, ArrowDown } from 'lucide-react';
import JsonView from 'react18-json-view';
import 'react18-json-view/src/style.css';
import { MemoryDetailPanel } from './memory-detail-panel';
@@ -38,6 +38,34 @@ export function SearchDebugView() {
const [loading, setLoading] = useState(false);
const [viewMode, setViewMode] = useState<ViewMode>('results');
const [selectedMemory, setSelectedMemory] = useState<any | null>(null);
const [expandedSteps, setExpandedSteps] = useState<Set<string>>(new Set());
const [expandedResults, setExpandedResults] = useState<Set<string>>(new Set());
const toggleStep = (step: string) => {
setExpandedSteps(prev => {
const next = new Set(prev);
if (next.has(step)) {
next.delete(step);
} else {
next.add(step);
}
return next;
});
};
const toggleExpandResults = (key: string) => {
setExpandedResults(prev => {
const next = new Set(prev);
if (next.has(key)) {
next.delete(key);
} else {
next.add(key);
}
return next;
});
};
const INITIAL_RESULTS_COUNT = 5;
const runSearch = async () => {
if (!currentBank) {
@@ -316,55 +344,431 @@ export function SearchDebugView() {
{/* Trace View */}
{viewMode === 'trace' && trace && (
<Card>
<CardHeader>
<CardTitle className="text-lg">Recall Trace</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* Retrieval Methods */}
{trace.retrieval_results && (
<div className="space-y-4">
{/* Parallel Retrieval Methods - Grouped by Fact Type */}
{trace.retrieval_results && trace.retrieval_results.length > 0 && (() => {
// Group retrieval results by fact type
const factTypeGroups: Record<string, any[]> = {};
trace.retrieval_results.forEach((method: any) => {
const ft = method.fact_type || 'all';
if (!factTypeGroups[ft]) factTypeGroups[ft] = [];
factTypeGroups[ft].push(method);
});
const factTypes = Object.keys(factTypeGroups);
return (
<div>
<h4 className="font-semibold mb-3">Retrieval Methods</h4>
<div className="grid grid-cols-2 gap-4">
{trace.retrieval_results.map((method: any, idx: number) => (
<div key={idx} className="p-4 rounded-lg bg-muted/50">
<div className="flex items-center justify-between mb-2">
<span className="font-medium capitalize">{method.method_name}</span>
<span className="text-sm text-muted-foreground">
{method.duration_seconds?.toFixed(3)}s
</span>
<div className="text-xs font-medium text-muted-foreground mb-3 flex items-center gap-2">
<div className="flex-1 h-px bg-border" />
<span>PARALLEL RETRIEVAL</span>
<div className="flex-1 h-px bg-border" />
</div>
{/* Fact type lanes */}
<div className="space-y-2">
{factTypes.map((factType, ftIdx) => {
const methods = factTypeGroups[factType];
const laneKey = `lane-${factType}`;
const isLaneExpanded = expandedSteps.has(laneKey);
const totalResults = methods.reduce((sum: number, m: any) => sum + (m.results?.length || 0), 0);
const totalDuration = Math.max(...methods.map((m: any) => m.duration_seconds || 0));
// Color coding for fact types
const ftColors: Record<string, { bg: string; text: string; border: string }> = {
world: { bg: 'bg-blue-500/10', text: 'text-blue-500', border: 'border-blue-500/30' },
experience: { bg: 'bg-green-500/10', text: 'text-green-500', border: 'border-green-500/30' },
opinion: { bg: 'bg-purple-500/10', text: 'text-purple-500', border: 'border-purple-500/30' },
all: { bg: 'bg-gray-500/10', text: 'text-gray-500', border: 'border-gray-500/30' },
};
const colors = ftColors[factType] || ftColors.all;
return (
<Card
key={laneKey}
className={`transition-colors ${isLaneExpanded ? 'border-primary' : colors.border}`}
>
<CardContent className="py-3 px-4">
{/* Lane Header */}
<div
className="flex items-center gap-3 cursor-pointer"
onClick={() => toggleStep(laneKey)}
>
<div className={`w-8 h-8 rounded-lg ${colors.bg} flex items-center justify-center`}>
<span className={`text-sm font-bold ${colors.text} capitalize`}>
{factType.charAt(0).toUpperCase()}
</span>
</div>
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="font-semibold text-foreground capitalize">{factType}</span>
<span className="text-xs text-muted-foreground">
{methods.length} methods
</span>
</div>
{/* Method summary pills */}
<div className="flex gap-1.5 mt-1">
{methods.map((m: any, mIdx: number) => (
<span
key={mIdx}
className="text-[10px] px-2 py-0.5 rounded-full bg-muted text-muted-foreground capitalize"
>
{m.method_name}: {m.results?.length || 0}
</span>
))}
</div>
</div>
<div className="text-right">
<div className="text-2xl font-bold text-foreground">{totalResults}</div>
<div className="text-[10px] text-muted-foreground">{totalDuration.toFixed(2)}s</div>
</div>
{isLaneExpanded ? (
<ChevronDown className="h-5 w-5 text-muted-foreground" />
) : (
<ChevronRight className="h-5 w-5 text-muted-foreground" />
)}
</div>
{/* Expanded: Show methods grid */}
{isLaneExpanded && (
<div className="mt-4 pt-4 border-t border-border">
<div className={`grid gap-3 ${
methods.length === 1 ? 'grid-cols-1' :
methods.length === 2 ? 'grid-cols-2' :
methods.length === 3 ? 'grid-cols-3' :
'grid-cols-4'
}`}>
{methods.map((method: any, mIdx: number) => {
const methodKey = `${laneKey}-method-${mIdx}`;
const isMethodExpanded = expandedSteps.has(methodKey);
const methodResults = method.results || [];
return (
<div key={methodKey} className="flex flex-col">
<div
className={`p-3 rounded-lg cursor-pointer transition-colors ${
isMethodExpanded ? 'bg-primary/10 border border-primary' : 'bg-muted/50 hover:bg-muted'
}`}
onClick={(e) => {
e.stopPropagation();
toggleStep(methodKey);
}}
>
<div className="flex items-center justify-between mb-1">
<span className="font-medium text-sm text-foreground capitalize">{method.method_name}</span>
{isMethodExpanded ? (
<ChevronDown className="h-3 w-3 text-muted-foreground" />
) : (
<ChevronRight className="h-3 w-3 text-muted-foreground" />
)}
</div>
<div className="flex items-end justify-between">
<div className="text-2xl font-bold text-foreground">{methodResults.length}</div>
<div className="text-[10px] text-muted-foreground">{method.duration_seconds?.toFixed(2)}s</div>
</div>
</div>
{/* Method Results */}
{isMethodExpanded && methodResults.length > 0 && (() => {
const resultsKey = `results-${methodKey}`;
const showAll = expandedResults.has(resultsKey);
const displayResults = showAll ? methodResults : methodResults.slice(0, INITIAL_RESULTS_COUNT);
const hasMore = methodResults.length > INITIAL_RESULTS_COUNT;
return (
<div className="mt-2 space-y-1.5 max-h-[300px] overflow-y-auto">
{displayResults.map((r: any, rIdx: number) => (
<div
key={rIdx}
className="p-2 bg-background rounded cursor-pointer hover:bg-muted/50 transition-colors border border-border"
onClick={(e) => {
e.stopPropagation();
setSelectedMemory(r);
}}
>
<div className="flex items-start gap-2">
<span className="text-[10px] font-mono text-muted-foreground mt-0.5">{rIdx + 1}</span>
<div className="flex-1 min-w-0">
<p className="text-xs text-foreground line-clamp-2">{r.text}</p>
<div className="flex items-center gap-2 mt-1">
<span className="text-[10px] text-muted-foreground">
{(r.score || r.similarity || 0).toFixed(4)}
</span>
</div>
</div>
</div>
</div>
))}
{hasMore && (
<button
className="w-full text-[10px] text-primary hover:text-primary/80 py-1.5 hover:bg-muted/50 rounded transition-colors"
onClick={(e) => {
e.stopPropagation();
toggleExpandResults(resultsKey);
}}
>
{showAll ? `Show less` : `View all ${methodResults.length} results`}
</button>
)}
</div>
);
})()}
</div>
);
})}
</div>
</div>
)}
</CardContent>
</Card>
);
})}
</div>
{/* Parallel indicator - vertical lines showing all run together */}
<div className="flex justify-center py-2">
<div className="flex items-center gap-2">
{factTypes.map((ft, i) => {
const ftColors: Record<string, string> = {
world: 'bg-blue-500',
experience: 'bg-green-500',
opinion: 'bg-purple-500',
all: 'bg-gray-500',
};
return (
<div key={i} className="flex flex-col items-center">
<div className={`w-1 h-4 ${ftColors[ft] || ftColors.all} rounded-full opacity-50`} />
</div>
);
})}
</div>
</div>
<div className="flex justify-center">
<ArrowDown className="h-5 w-5 text-muted-foreground/50" />
</div>
</div>
);
})()}
{/* Step 2: RRF Merge */}
{trace.rrf_merged && (() => {
const stepKey = 'rrf-merge';
const isExpanded = expandedSteps.has(stepKey);
return (
<div>
<Card
className={`cursor-pointer transition-colors ${isExpanded ? 'border-primary' : 'hover:border-primary/50'}`}
onClick={() => toggleStep(stepKey)}
>
<CardContent className="py-4">
<div className="flex items-center gap-4">
<div className="flex-shrink-0 w-10 h-10 rounded-full bg-purple-500/10 flex items-center justify-center">
<span className="text-sm font-bold text-purple-500"></span>
</div>
<div className="text-2xl font-bold">{method.results?.length || 0}</div>
<div className="text-xs text-muted-foreground">results</div>
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="font-semibold text-foreground">RRF Fusion</span>
<span className="text-xs px-2 py-0.5 rounded bg-muted text-muted-foreground">merge</span>
</div>
<div className="text-sm text-muted-foreground mt-0.5">
Reciprocal Rank Fusion of all retrieval results
</div>
</div>
<div className="text-2xl font-bold text-foreground">{trace.rrf_merged.length}</div>
{isExpanded ? (
<ChevronDown className="h-5 w-5 text-muted-foreground" />
) : (
<ChevronRight className="h-5 w-5 text-muted-foreground" />
)}
</div>
))}
</div>
</div>
)}
</CardContent>
</Card>
{/* RRF Merge */}
{trace.rrf_merged && (
<div>
<h4 className="font-semibold mb-3">RRF Merge</h4>
<div className="p-4 rounded-lg bg-muted/50">
<div className="text-2xl font-bold">{trace.rrf_merged.length}</div>
<div className="text-xs text-muted-foreground">candidates after fusion</div>
</div>
</div>
)}
{/* Expanded Results */}
{isExpanded && trace.rrf_merged.length > 0 && (() => {
const resultsKey = 'results-rrf';
const showAll = expandedResults.has(resultsKey);
const displayResults = showAll ? trace.rrf_merged : trace.rrf_merged.slice(0, INITIAL_RESULTS_COUNT);
const hasMore = trace.rrf_merged.length > INITIAL_RESULTS_COUNT;
{/* Reranking */}
{trace.reranked && (
<div>
<h4 className="font-semibold mb-3">Reranking</h4>
<div className="p-4 rounded-lg bg-muted/50">
<div className="text-2xl font-bold">{trace.reranked.length}</div>
<div className="text-xs text-muted-foreground">results after cross-encoder</div>
return (
<div className="ml-6 mt-2 space-y-2 border-l-2 border-muted pl-4 max-h-[400px] overflow-y-auto">
{displayResults.map((r: any, rIdx: number) => (
<div
key={rIdx}
className="p-3 bg-muted/30 rounded-lg cursor-pointer hover:bg-muted/50 transition-colors"
onClick={(e) => {
e.stopPropagation();
setSelectedMemory(r);
}}
>
<div className="flex items-start gap-3">
<span className="text-xs font-mono text-muted-foreground">{rIdx + 1}</span>
<div className="flex-1 min-w-0">
<p className="text-sm text-foreground line-clamp-2">{r.text}</p>
<div className="text-xs text-muted-foreground mt-1">
RRF Score: {(r.rrf_score || r.score || 0).toFixed(4)}
</div>
</div>
</div>
</div>
))}
{hasMore && (
<button
className="w-full text-xs text-primary hover:text-primary/80 py-2 hover:bg-muted/50 rounded transition-colors"
onClick={(e) => {
e.stopPropagation();
toggleExpandResults(resultsKey);
}}
>
{showAll ? `Show less` : `View all ${trace.rrf_merged.length} results`}
</button>
)}
</div>
);
})()}
{/* Arrow */}
<div className="flex justify-center py-2">
<ArrowDown className="h-4 w-4 text-muted-foreground/50" />
</div>
</div>
)}
</CardContent>
</Card>
);
})()}
{/* Step 3: Combined Scoring */}
{trace.reranked && (() => {
const stepKey = 'reranking';
const isExpanded = expandedSteps.has(stepKey);
return (
<div>
<Card
className={`cursor-pointer transition-colors ${isExpanded ? 'border-primary' : 'hover:border-primary/50'}`}
onClick={() => toggleStep(stepKey)}
>
<CardContent className="py-4">
<div className="flex items-center gap-4">
<div className="flex-shrink-0 w-10 h-10 rounded-full bg-amber-500/10 flex items-center justify-center">
<span className="text-sm font-bold text-amber-500"></span>
</div>
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="font-semibold text-foreground">Combined Scoring</span>
<span className="text-xs px-2 py-0.5 rounded bg-muted text-muted-foreground">rerank</span>
</div>
<div className="text-sm text-muted-foreground mt-0.5">
<span className="font-mono text-xs">0.6×cross_encoder + 0.2×rrf + 0.1×temporal + 0.1×recency</span>
</div>
</div>
<div className="text-2xl font-bold text-foreground">{trace.reranked.length}</div>
{isExpanded ? (
<ChevronDown className="h-5 w-5 text-muted-foreground" />
) : (
<ChevronRight className="h-5 w-5 text-muted-foreground" />
)}
</div>
</CardContent>
</Card>
{/* Expanded Results */}
{isExpanded && trace.reranked.length > 0 && (() => {
const resultsKey = 'results-rerank';
const showAll = expandedResults.has(resultsKey);
const displayResults = showAll ? trace.reranked : trace.reranked.slice(0, INITIAL_RESULTS_COUNT);
const hasMore = trace.reranked.length > INITIAL_RESULTS_COUNT;
return (
<div className="ml-6 mt-2 space-y-2 border-l-2 border-muted pl-4 max-h-[400px] overflow-y-auto">
{displayResults.map((r: any, rIdx: number) => {
const sc = r.score_components || {};
return (
<div
key={rIdx}
className="p-3 bg-muted/30 rounded-lg cursor-pointer hover:bg-muted/50 transition-colors"
onClick={(e) => {
e.stopPropagation();
setSelectedMemory(r);
}}
>
<div className="flex items-start gap-3">
<span className="text-xs font-mono text-muted-foreground">{rIdx + 1}</span>
<div className="flex-1 min-w-0">
<p className="text-sm text-foreground line-clamp-2">{r.text}</p>
<div className="flex flex-wrap gap-x-3 gap-y-1 mt-2 text-[10px] text-muted-foreground font-mono">
<span className="font-semibold text-foreground">
= {(r.rerank_score || r.score || 0).toFixed(4)}
</span>
{sc.cross_encoder_score_normalized !== undefined && (
<span title="Cross-encoder (60%)">
CE: {sc.cross_encoder_score_normalized.toFixed(3)}
</span>
)}
{sc.rrf_normalized !== undefined && (
<span title={`RRF normalized (20%) - raw: ${sc.rrf_score?.toFixed(4) || 'N/A'}`}>
RRF: {sc.rrf_normalized.toFixed(3)}
</span>
)}
{sc.temporal !== undefined && (
<span title="Temporal proximity (10%)">
Tmp: {sc.temporal.toFixed(3)}
</span>
)}
{sc.recency !== undefined && (
<span title="Recency (10%)">
Rec: {sc.recency.toFixed(3)}
</span>
)}
</div>
</div>
</div>
</div>
);
})}
{hasMore && (
<button
className="w-full text-xs text-primary hover:text-primary/80 py-2 hover:bg-muted/50 rounded transition-colors"
onClick={(e) => {
e.stopPropagation();
toggleExpandResults(resultsKey);
}}
>
{showAll ? `Show less` : `View all ${trace.reranked.length} results`}
</button>
)}
</div>
);
})()}
{/* Arrow */}
<div className="flex justify-center py-2">
<ArrowDown className="h-4 w-4 text-muted-foreground/50" />
</div>
</div>
);
})()}
{/* Final: Results */}
<Card className="border-primary bg-primary/5">
<CardContent className="py-4">
<div className="flex items-center gap-4">
<div className="flex-shrink-0 w-10 h-10 rounded-full bg-primary/20 flex items-center justify-center">
<span className="text-sm font-bold text-primary"></span>
</div>
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="font-semibold text-foreground">Final Results</span>
<span className="text-xs px-2 py-0.5 rounded bg-primary/20 text-primary">output</span>
</div>
<div className="text-sm text-muted-foreground mt-0.5">
Top results after all processing steps
</div>
</div>
<div className="text-2xl font-bold text-primary">{results?.length || 0}</div>
</div>
</CardContent>
</Card>
</div>
)}
{/* JSON View */}
@@ -44,7 +44,7 @@ const DialogContent = React.forwardRef<
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm ring-offset-background transition-opacity hover:opacity-80 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground text-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
@@ -88,7 +88,7 @@ const DialogTitle = React.forwardRef<
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
"text-lg font-semibold leading-none tracking-tight text-foreground",
className
)}
{...props}
@@ -0,0 +1,359 @@
#!/usr/bin/env python3
"""
Generate changelog entry for a new release.
This script fetches the commit diff between releases, uses an LLM to summarize,
and prepends the entry to the changelog page.
"""
import argparse
import json
import os
import re
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path
from openai import OpenAI
from pydantic import BaseModel
from rich.console import Console
console = Console()
GITHUB_REPO = "vectorize-io/hindsight"
GITHUB_RELEASES_URL = f"https://github.com/{GITHUB_REPO}/releases"
GITHUB_COMMIT_URL = f"https://github.com/{GITHUB_REPO}/commit"
REPO_PATH = Path(__file__).parent.parent.parent
CHANGELOG_PATH = REPO_PATH / "hindsight-docs" / "docs" / "changelog" / "index.md"
class ChangelogEntry(BaseModel):
"""A single changelog entry."""
category: str # "feature", "improvement", "bugfix", "breaking", "other"
summary: str # Brief description of the change
commit_id: str # Short commit hash
class ChangelogResponse(BaseModel):
"""Structured response from LLM."""
entries: list[ChangelogEntry]
@dataclass
class Commit:
"""Parsed commit from git log."""
hash: str
message: str
def parse_semver(version: str) -> tuple[int, int, int]:
"""Parse a semver string into (major, minor, patch)."""
version = version.lstrip("v")
match = re.match(r"^(\d+)\.(\d+)\.(\d+)", version)
if not match:
raise ValueError(f"Invalid semver: {version}")
return int(match.group(1)), int(match.group(2)), int(match.group(3))
def get_git_tags() -> list[str]:
"""Get all git tags sorted by semver (newest first)."""
result = subprocess.run(
["git", "tag"],
cwd=REPO_PATH,
capture_output=True,
text=True,
check=True,
)
tags = [t.strip() for t in result.stdout.strip().split("\n") if t.strip()]
valid_tags = []
for tag in tags:
try:
parse_semver(tag)
valid_tags.append(tag)
except ValueError:
continue
valid_tags.sort(key=lambda t: parse_semver(t), reverse=True)
return valid_tags
def find_previous_version(new_version: str, existing_tags: list[str]) -> str | None:
"""Find the previous version based on semver rules."""
new_major, new_minor, new_patch = parse_semver(new_version)
candidates = []
for tag in existing_tags:
try:
major, minor, patch = parse_semver(tag)
except ValueError:
continue
if (major, minor, patch) >= (new_major, new_minor, new_patch):
continue
candidates.append((tag, (major, minor, patch)))
if not candidates:
return None
candidates.sort(key=lambda x: x[1], reverse=True)
return candidates[0][0]
def get_commits(from_ref: str | None, to_ref: str) -> list[Commit]:
"""Get commits between two refs as structured data."""
if from_ref:
cmd = ["git", "log", "--format=%h|%s", "--no-merges", f"{from_ref}..{to_ref}"]
else:
cmd = ["git", "log", "--format=%h|%s", "--no-merges", to_ref]
result = subprocess.run(
cmd,
cwd=REPO_PATH,
capture_output=True,
text=True,
check=True,
)
commits = []
for line in result.stdout.strip().split("\n"):
if not line:
continue
parts = line.split("|", 1)
if len(parts) == 2:
commits.append(Commit(hash=parts[0], message=parts[1]))
return commits
def get_detailed_diff(from_ref: str | None, to_ref: str) -> str:
"""Get file change stats between two refs."""
if from_ref:
cmd = ["git", "diff", "--stat", f"{from_ref}..{to_ref}"]
else:
cmd = ["git", "diff", "--stat", f"{to_ref}^..{to_ref}"]
result = subprocess.run(
cmd,
cwd=REPO_PATH,
capture_output=True,
text=True,
)
return result.stdout.strip()
def analyze_commits_with_llm(
client: OpenAI,
model: str,
version: str,
commits: list[Commit],
file_diff: str,
) -> list[ChangelogEntry]:
"""Use LLM to analyze commits and return structured changelog entries."""
commits_json = json.dumps(
[{"commit_id": c.hash, "message": c.message} for c in commits],
indent=2
)
prompt = f"""Analyze the following git commits for release {version} of Hindsight (an AI memory system).
For each meaningful change, create a changelog entry with:
- category: one of "feature", "improvement", "bugfix", "breaking", "other"
- summary: brief one-line description of the change (user-facing, not technical)
- commit_id: the commit hash from the input
Rules:
- Group related commits into a single entry if they're part of the same change
- Skip trivial changes (typo fixes, formatting, internal refactoring)
- Skip repository-only changes: README updates, CI/GitHub Actions, release scripts, changelog updates, version bumps
- Focus on user-facing changes that affect the product functionality
- Use the exact commit_id from the input (pick the most relevant one if grouping)
- If no meaningful changes remain after filtering, return an empty list
Commits:
{commits_json}
Files changed summary:
{file_diff[:4000]}"""
response = client.beta.chat.completions.parse(
model=model,
messages=[{"role": "user", "content": prompt}],
response_format=ChangelogResponse,
max_completion_tokens=16000,
)
return response.choices[0].message.parsed.entries
def build_changelog_markdown(
version: str,
tag: str,
entries: list[ChangelogEntry],
) -> str:
"""Build markdown changelog from structured entries."""
release_url = f"{GITHUB_RELEASES_URL}/tag/{tag}"
# Group entries by category
categories = {
"breaking": ("Breaking Changes", []),
"feature": ("Features", []),
"improvement": ("Improvements", []),
"bugfix": ("Bug Fixes", []),
"other": ("Other", []),
}
for entry in entries:
cat = entry.category.lower()
if cat in categories:
categories[cat][1].append(entry)
else:
categories["other"][1].append(entry)
# Build markdown
lines = [f"## [{version}]({release_url})", ""]
for cat_key in ["breaking", "feature", "improvement", "bugfix", "other"]:
cat_name, cat_entries = categories[cat_key]
if cat_entries:
lines.append(f"**{cat_name}**")
lines.append("")
for entry in cat_entries:
commit_url = f"{GITHUB_COMMIT_URL}/{entry.commit_id}"
lines.append(f"- {entry.summary} ([`{entry.commit_id}`]({commit_url}))")
lines.append("")
return "\n".join(lines)
def read_existing_changelog() -> tuple[str, str]:
"""Read existing changelog and split into header and content."""
if not CHANGELOG_PATH.exists():
header = """---
sidebar_position: 1
---
# Changelog
For full release details, see [GitHub Releases](https://github.com/vectorize-io/hindsight/releases).
"""
return header, ""
content = CHANGELOG_PATH.read_text()
match = re.search(r"^## ", content, re.MULTILINE)
if match:
header = content[:match.start()].rstrip() + "\n\n"
releases = content[match.start():]
else:
header = content.rstrip() + "\n\n"
releases = ""
return header, releases
def write_changelog(header: str, new_entry: str, existing_releases: str) -> None:
"""Write changelog with new entry prepended."""
content = header + new_entry + "\n" + existing_releases
CHANGELOG_PATH.parent.mkdir(parents=True, exist_ok=True)
CHANGELOG_PATH.write_text(content.rstrip() + "\n")
def generate_changelog_entry(
version: str,
llm_model: str = "gpt-5.2",
) -> None:
"""Generate changelog entry for a specific version."""
api_key = os.environ.get("OPENAI_API_KEY")
if not api_key:
console.print("[red]Error: OPENAI_API_KEY environment variable not set[/red]")
sys.exit(1)
client = OpenAI(api_key=api_key)
tag = version if version.startswith("v") else f"v{version}"
display_version = version.lstrip("v")
console.print(f"[blue]Fetching tags from repository...[/blue]")
existing_tags = get_git_tags()
if tag not in existing_tags and display_version not in existing_tags:
console.print(f"[red]Error: Tag {tag} not found in repository[/red]")
console.print("[red]Create the tag first before generating changelog[/red]")
sys.exit(1)
actual_tag = tag if tag in existing_tags else display_version
previous_tag = find_previous_version(display_version, existing_tags)
if previous_tag:
console.print(f"[green]Found previous version: {previous_tag}[/green]")
else:
console.print("[yellow]No previous version found, will include all commits[/yellow]")
console.print(f"[blue]Getting commits...[/blue]")
commits = get_commits(previous_tag, actual_tag)
file_diff = get_detailed_diff(previous_tag, actual_tag)
if not commits:
console.print("[red]Error: No commits found for this release[/red]")
sys.exit(1)
console.print(f"[blue]Found {len(commits)} commits[/blue]")
# Log commits
console.print("\n[bold]Commits:[/bold]")
for c in commits:
console.print(f" {c.hash} {c.message}")
console.print("\n[bold]Files changed:[/bold]")
console.print(file_diff[:4000] if len(file_diff) > 4000 else file_diff)
console.print("")
console.print(f"[blue]Analyzing commits with LLM ({llm_model})...[/blue]")
entries = analyze_commits_with_llm(client, llm_model, display_version, commits, file_diff)
console.print(f"\n[bold]LLM identified {len(entries)} changelog entries:[/bold]")
for entry in entries:
console.print(f" [{entry.category}] {entry.summary} ({entry.commit_id})")
new_entry = build_changelog_markdown(display_version, tag, entries)
header, existing_releases = read_existing_changelog()
if f"## [{display_version}]" in existing_releases:
console.print(f"[red]Error: Version {display_version} already exists in changelog[/red]")
sys.exit(1)
write_changelog(header, new_entry, existing_releases)
console.print(f"\n[green]Changelog updated: {CHANGELOG_PATH}[/green]")
console.print(f"\n[bold]New entry:[/bold]\n{new_entry}")
def main():
parser = argparse.ArgumentParser(
description="Generate changelog entry for a release",
usage="generate-changelog VERSION [--model MODEL]",
)
parser.add_argument(
"version",
help="Version to generate changelog for (e.g., 1.0.5, v1.0.5)",
)
parser.add_argument(
"--model",
default="gpt-5.2",
help="OpenAI model to use (default: gpt-5.2)",
)
args = parser.parse_args()
generate_changelog_entry(
version=args.version,
llm_model=args.model,
)
if __name__ == "__main__":
main()
+2 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "hindsight-dev"
version = "0.1.4"
version = "0.1.5"
description = "Development utilities for Hindsight"
requires-python = ">=3.11"
dependencies = [
@@ -24,3 +24,4 @@ hindsight-api = { workspace = true }
[project.scripts]
generate-openapi = "hindsight_dev.generate_openapi:generate_openapi_spec"
generate-changelog = "hindsight_dev.generate_changelog:main"
+32 -1
View File
@@ -4,4 +4,35 @@ sidebar_position: 1
# Changelog
Coming soon.
For full release details, see [GitHub Releases](https://github.com/vectorize-io/hindsight/releases).
## [0.1.5](https://github.com/vectorize-io/hindsight/releases/tag/v0.1.5)
**Features**
- Added LiteLLM integration so Hindsight can capture and manage memories from LiteLLM-based LLM calls. ([`dfccbf2`](https://github.com/vectorize-io/hindsight/commit/dfccbf2))
- Added an optional graph-based retriever (MPFP) to improve recall by leveraging relationships between memories. ([`7445cef`](https://github.com/vectorize-io/hindsight/commit/7445cef))
**Improvements**
- Switched the embedded Postgres layer to pg0-embedded for a smoother local/standalone experience. ([`94c2b85`](https://github.com/vectorize-io/hindsight/commit/94c2b85))
**Bug Fixes**
- Fixed repeated retries on 400 errors from the LLM, preventing unnecessary request loops and failures. ([`70983f5`](https://github.com/vectorize-io/hindsight/commit/70983f5))
- Fixed recall trace visualization in the control plane so search/recall debugging displays correctly. ([`922164e`](https://github.com/vectorize-io/hindsight/commit/922164e))
- Fixed the CLI installer to make installation more reliable. ([`158a6aa`](https://github.com/vectorize-io/hindsight/commit/158a6aa))
- Updated Next.js to patch security vulnerabilities (CVE-2025-55184, CVE-2025-55183). ([`f018cc5`](https://github.com/vectorize-io/hindsight/commit/f018cc5))
## [0.1.3](https://github.com/vectorize-io/hindsight/releases/tag/v0.1.3)
**Improvements**
- Improved CLI and UI branding/polish, including new banner/logo assets and updated interface styling. ([`fa554b8`](https://github.com/vectorize-io/hindsight/commit/fa554b8))
## [0.1.2](https://github.com/vectorize-io/hindsight/releases/tag/v0.1.2)
**Bug Fixes**
- Fixed the standalone Docker image so it builds/runs correctly. ([`1056a20`](https://github.com/vectorize-io/hindsight/commit/1056a20))
+62 -48
View File
@@ -6,14 +6,70 @@ Hindsight uses several machine learning models for different tasks.
| Model Type | Purpose | Default | Configurable |
|------------|---------|---------|--------------|
| **LLM** | Fact extraction, reasoning, generation | Provider-specific | Yes |
| **Embedding** | Vector representations for semantic search | `BAAI/bge-small-en-v1.5` | Yes |
| **Cross-Encoder** | Reranking search results | `cross-encoder/ms-marco-MiniLM-L-6-v2` | Yes |
| **LLM** | Fact extraction, reasoning, generation | Provider-specific | Yes |
All local models (embedding, cross-encoder) are automatically downloaded from HuggingFace on first run.
---
## LLM
Used for fact extraction, entity resolution, opinion generation, and answer synthesis.
**Supported providers:** OpenAI, Gemini, Groq, Ollama
### Tested Models
The following models have been tested and verified to work correctly with Hindsight:
| Provider | Model |
|----------|-------|
| **OpenAI** | `gpt-5` |
| **OpenAI** | `gpt-5-mini` |
| **OpenAI** | `gpt-5-nano` |
| **OpenAI** | `gpt-4.1-mini` |
| **OpenAI** | `gpt-4.1-nano` |
| **OpenAI** | `gpt-4o-mini` |
| **Gemini** | `gemini-2.5-flash` |
| **Gemini** | `gemini-2.5-flash-lite` |
| **Groq** | `openai/gpt-oss-120b` |
| **Groq** | `openai/gpt-oss-20b` |
| **Groq** | `llama-3.3-70b-versatile` |
### Using Other Models
Other LLM models not listed above may work with Hindsight, but they must support **at least 65,000 output tokens** to ensure reliable fact extraction. If you need support for a specific model that doesn't meet this requirement, please [open an issue](https://github.com/hindsight-ai/hindsight/issues) to request an exception.
### Configuration
```bash
# Groq (recommended)
export HINDSIGHT_API_LLM_PROVIDER=groq
export HINDSIGHT_API_LLM_API_KEY=gsk_xxxxxxxxxxxx
export HINDSIGHT_API_LLM_MODEL=openai/gpt-oss-20b
# OpenAI
export HINDSIGHT_API_LLM_PROVIDER=openai
export HINDSIGHT_API_LLM_API_KEY=sk-xxxxxxxxxxxx
export HINDSIGHT_API_LLM_MODEL=gpt-4o
# Gemini
export HINDSIGHT_API_LLM_PROVIDER=gemini
export HINDSIGHT_API_LLM_API_KEY=xxxxxxxxxxxx
export HINDSIGHT_API_LLM_MODEL=gemini-2.0-flash
# Ollama (local)
export HINDSIGHT_API_LLM_PROVIDER=ollama
export HINDSIGHT_API_LLM_BASE_URL=http://localhost:11434/v1
export HINDSIGHT_API_LLM_MODEL=llama3.1
```
**Note:** The LLM is the primary bottleneck for retain operations. See [Performance](./performance) for optimization strategies.
---
## Embedding Model
Converts text into dense vector representations for semantic similarity search.
@@ -22,14 +78,13 @@ Converts text into dense vector representations for semantic similarity search.
**Alternatives:**
| Model | Dimensions | Use Case |
|-------|------------|----------|
| `BAAI/bge-small-en-v1.5` | 384 | Default, fast, good quality |
| `BAAI/bge-base-en-v1.5` | 768 | Higher accuracy, slower |
| `sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2` | 384 | Multilingual (50+ languages) |
| Model | Use Case |
|-------|----------|
| `BAAI/bge-small-en-v1.5` | Default, fast, good quality |
| `sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2` | Multilingual (50+ languages) |
:::warning
All embedding models must produce 384-dimensional vectors to match the database schema.
All embedding models must produce **384-dimensional vectors** to match the database schema.
:::
**Configuration:**
@@ -71,44 +126,3 @@ export HINDSIGHT_API_RERANKER_LOCAL_MODEL=cross-encoder/ms-marco-MiniLM-L-6-v2
export HINDSIGHT_API_RERANKER_PROVIDER=tei
export HINDSIGHT_API_RERANKER_TEI_URL=http://localhost:8081
```
---
## LLM
Used for fact extraction, entity resolution, opinion generation, and answer synthesis.
**Supported providers:** Groq, OpenAI, Gemini, Ollama
| Provider | Recommended Model | Best For |
|----------|------------------|----------|
| **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:**
```bash
# Groq (recommended)
export HINDSIGHT_API_LLM_PROVIDER=groq
export HINDSIGHT_API_LLM_API_KEY=gsk_xxxxxxxxxxxx
export HINDSIGHT_API_LLM_MODEL=openai/gpt-oss-20b
# OpenAI
export HINDSIGHT_API_LLM_PROVIDER=openai
export HINDSIGHT_API_LLM_API_KEY=sk-xxxxxxxxxxxx
export HINDSIGHT_API_LLM_MODEL=gpt-4o
# Gemini
export HINDSIGHT_API_LLM_PROVIDER=gemini
export HINDSIGHT_API_LLM_API_KEY=xxxxxxxxxxxx
export HINDSIGHT_API_LLM_MODEL=gemini-2.0-flash
# Ollama (local)
export HINDSIGHT_API_LLM_PROVIDER=ollama
export HINDSIGHT_API_LLM_BASE_URL=http://localhost:11434/v1
export HINDSIGHT_API_LLM_MODEL=llama3.1
```
**Note:** The LLM is the primary bottleneck for retain operations. See [Performance](./performance) for optimization strategies.
@@ -0,0 +1,345 @@
---
sidebar_position: 1
---
# LiteLLM
Universal LLM memory integration via [LiteLLM](https://github.com/BerriAI/litellm). Add persistent memory to any LLM application with just a few lines of code.
## Features
- **Universal LLM Support** - Works with 100+ LLM providers via LiteLLM (OpenAI, Anthropic, Groq, Azure, AWS Bedrock, Google Vertex AI, and more)
- **Simple Integration** - Just configure, enable, and use `hindsight_litellm.completion()`
- **Automatic Memory Injection** - Relevant memories are injected into prompts before LLM calls
- **Automatic Conversation Storage** - Conversations are stored to Hindsight for future recall
- **Two Memory Modes** - Choose between `reflect` (synthesized context) or `recall` (raw memory retrieval)
- **Direct Memory APIs** - Query, synthesize, and store memories manually
- **Native Client Wrappers** - Alternative wrappers for OpenAI and Anthropic SDKs
## Installation
```bash
pip install hindsight-litellm
```
## Quick Start
```python
import hindsight_litellm
# Configure and enable memory integration
hindsight_litellm.configure(
hindsight_api_url="http://localhost:8888",
bank_id="my-agent",
)
hindsight_litellm.enable()
# Use the convenience wrapper - memory is automatically injected and stored
response = hindsight_litellm.completion(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "What did we discuss about AI?"}]
)
```
## How It Works
When you call `completion()`, the following happens automatically:
1. **Memory Retrieval** - Hindsight is queried for relevant memories based on the conversation
2. **Prompt Injection** - Memories are injected into the system message
3. **LLM Call** - The enriched prompt is sent to the LLM
4. **Conversation Storage** - The conversation is stored to Hindsight for future recall
5. **Response Returned** - You receive the response as normal
## Configuration Options
```python
hindsight_litellm.configure(
# Required
hindsight_api_url="http://localhost:8888", # Hindsight API server URL
bank_id="my-agent", # Memory bank ID
api_key="your-api-key", # Optional API key for authentication
# Optional - Memory behavior
store_conversations=True, # Store conversations after LLM calls
inject_memories=True, # Inject relevant memories into prompts
use_reflect=False, # Use reflect API (synthesized) vs recall (raw memories)
reflect_include_facts=False, # Include source facts with reflect responses
max_memories=None, # Maximum memories to inject (None = unlimited)
max_memory_tokens=4096, # Maximum tokens for memory context
recall_budget="mid", # Recall budget: "low", "mid", "high"
fact_types=["world", "agent"], # Filter fact types to inject
# Optional - Bank Configuration
bank_name="My Agent", # Human-readable display name for the memory bank
background="This agent...", # Instructions guiding what Hindsight should remember
# Optional - Advanced
injection_mode="system_message", # or "prepend_user"
excluded_models=["gpt-3.5*"], # Exclude certain models
verbose=True, # Enable verbose logging and debug info
)
```
### Bank Configuration
The `background` and `bank_name` parameters configure the memory bank itself. When provided, `configure()` will automatically create or update the bank with these settings.
```python
hindsight_litellm.configure(
hindsight_api_url="http://localhost:8888",
bank_id="support-router",
bank_name="Customer Support Router",
background="""This agent routes customer support requests to the appropriate team.
Remember which types of issues should go to which teams (billing, technical, sales).
Track customer preferences for communication channels and past issue resolutions.""",
)
```
### Memory Modes: Reflect vs Recall
- **Recall mode** (`use_reflect=False`, default): Retrieves raw memory facts and injects them as a numbered list. Best when you need precise, individual memories.
- **Reflect mode** (`use_reflect=True`): Synthesizes memories into a coherent context paragraph. Best for natural, conversational memory context.
```python
# Recall mode - raw memories
hindsight_litellm.configure(
bank_id="my-agent",
use_reflect=False, # Default
)
# Injects: "1. [WORLD] User prefers Python\n2. [OPINION] User dislikes Java..."
# Reflect mode - synthesized context
hindsight_litellm.configure(
bank_id="my-agent",
use_reflect=True,
)
# Injects: "Based on previous conversations, the user is a Python developer who..."
```
## Multi-Provider Support
Works with any LiteLLM-supported provider:
```python
import hindsight_litellm
hindsight_litellm.configure(
hindsight_api_url="http://localhost:8888",
bank_id="my-agent",
)
hindsight_litellm.enable()
# OpenAI
hindsight_litellm.completion(model="gpt-4o", messages=[...])
# Anthropic
hindsight_litellm.completion(model="claude-3-5-sonnet-20241022", messages=[...])
# Groq
hindsight_litellm.completion(model="groq/llama-3.1-70b-versatile", messages=[...])
# Azure OpenAI
hindsight_litellm.completion(model="azure/gpt-4", messages=[...])
# AWS Bedrock
hindsight_litellm.completion(model="bedrock/anthropic.claude-3", messages=[...])
# Google Vertex AI
hindsight_litellm.completion(model="vertex_ai/gemini-pro", messages=[...])
```
## Direct Memory APIs
### Recall - Query raw memories
```python
from hindsight_litellm import configure, recall
configure(bank_id="my-agent", hindsight_api_url="http://localhost:8888")
memories = recall("what projects am I working on?", budget="mid")
for m in memories:
print(f"- [{m.fact_type}] {m.text}")
```
### Reflect - Get synthesized context
```python
from hindsight_litellm import configure, reflect
configure(bank_id="my-agent", hindsight_api_url="http://localhost:8888")
result = reflect("what do you know about the user's preferences?")
print(result.text)
```
### Retain - Store memories
```python
from hindsight_litellm import configure, retain
configure(bank_id="my-agent", hindsight_api_url="http://localhost:8888")
result = retain(
content="User mentioned they're working on a machine learning project",
context="Discussion about current projects",
)
```
### Async APIs
```python
from hindsight_litellm import arecall, areflect, aretain
# Async versions of all memory APIs
memories = await arecall("what do you know about me?")
context = await areflect("summarize user preferences")
result = await aretain(content="New information to remember")
```
## Native Client Wrappers
Alternative to LiteLLM callbacks for direct SDK integration.
### OpenAI Wrapper
```python
from openai import OpenAI
from hindsight_litellm import wrap_openai
client = OpenAI()
wrapped = wrap_openai(
client,
bank_id="my-agent",
hindsight_api_url="http://localhost:8888",
)
response = wrapped.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": "What do you know about me?"}]
)
```
### Anthropic Wrapper
```python
from anthropic import Anthropic
from hindsight_litellm import wrap_anthropic
client = Anthropic()
wrapped = wrap_anthropic(
client,
bank_id="my-agent",
hindsight_api_url="http://localhost:8888",
)
response = wrapped.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello!"}]
)
```
## Debug Mode
When `verbose=True`, you can inspect exactly what memories are being injected:
```python
from hindsight_litellm import configure, enable, completion, get_last_injection_debug
configure(
bank_id="my-agent",
hindsight_api_url="http://localhost:8888",
verbose=True,
)
enable()
response = completion(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "What's my favorite color?"}]
)
# Inspect what was injected
debug = get_last_injection_debug()
if debug:
print(f"Mode: {debug.mode}") # "reflect" or "recall"
print(f"Injected: {debug.injected}") # True/False
print(f"Results: {debug.results_count}")
print(f"Memory context:\n{debug.memory_context}")
```
## Context Manager
```python
from hindsight_litellm import hindsight_memory
import litellm
with hindsight_memory(bank_id="user-123"):
response = litellm.completion(model="gpt-4", messages=[...])
# Memory integration automatically disabled after context
```
## Disabling and Cleanup
```python
from hindsight_litellm import disable, cleanup
# Temporarily disable memory integration
disable()
# Clean up all resources (call when shutting down)
cleanup()
```
## API Reference
### Main Functions
| Function | Description |
|----------|-------------|
| `configure(...)` | Configure global Hindsight settings |
| `enable()` | Enable memory integration with LiteLLM |
| `disable()` | Disable memory integration |
| `is_enabled()` | Check if memory integration is enabled |
| `cleanup()` | Clean up all resources |
### Configuration Functions
| Function | Description |
|----------|-------------|
| `get_config()` | Get current configuration |
| `is_configured()` | Check if Hindsight is configured |
| `reset_config()` | Reset configuration to defaults |
### Memory Functions
| Function | Description |
|----------|-------------|
| `recall(query, ...)` | Synchronously query raw memories |
| `arecall(query, ...)` | Asynchronously query raw memories |
| `reflect(query, ...)` | Synchronously get synthesized memory context |
| `areflect(query, ...)` | Asynchronously get synthesized memory context |
| `retain(content, ...)` | Synchronously store a memory |
| `aretain(content, ...)` | Asynchronously store a memory |
### Debug Functions
| Function | Description |
|----------|-------------|
| `get_last_injection_debug()` | Get debug info from last memory injection |
| `clear_injection_debug()` | Clear stored debug info |
### Client Wrappers
| Function | Description |
|----------|-------------|
| `wrap_openai(client, ...)` | Wrap OpenAI client with memory |
| `wrap_anthropic(client, ...)` | Wrap Anthropic client with memory |
## Requirements
- Python >= 3.10
- litellm >= 1.40.0
- A running Hindsight API server
+12
View File
@@ -147,6 +147,18 @@ const sidebars: SidebarsConfig = {
},
],
},
{
type: 'category',
label: 'Integrations',
collapsible: false,
items: [
{
type: 'doc',
id: 'sdks/integrations/litellm',
label: 'LiteLLM',
},
],
},
],
cookbookSidebar: [
{
+21
View File
@@ -514,6 +514,27 @@ article a:not(.button):not([class*="hash-link"]):hover {
text-decoration-color: var(--hindsight-gradient-start);
}
/* Links inside code blocks - use solid color instead of gradient */
code a,
pre a,
article code a,
article pre a {
background: none !important;
-webkit-background-clip: unset !important;
-webkit-text-fill-color: var(--ifm-color-primary) !important;
background-clip: unset !important;
color: var(--ifm-color-primary) !important;
text-decoration: underline;
}
code a:hover,
pre a:hover,
article code a:hover,
article pre a:hover {
color: var(--ifm-color-primary-dark) !important;
-webkit-text-fill-color: var(--ifm-color-primary-dark) !important;
}
/* Admonitions - gradient themed */
.theme-admonition,
[class*="admonition_"] {
+44 -7
View File
@@ -33,9 +33,14 @@ print_warning() {
print_banner() {
echo ""
echo -e "${BLUE}╔══════════════════════════════════════════════════╗${NC}"
echo -e "${BLUE}║ HINDSIGHT CLI INSTALLER ║${NC}"
echo -e "${BLUE}╚══════════════════════════════════════════════════╝${NC}"
# ANSI logo
echo -e " \033[38;2;9;127;184m▄\033[0m\033[48;2;8;130;178m\033[38;2;5;133;186m▄\033[0m \033[48;2;10;143;160m\033[38;2;10;143;165m▄\033[0m\033[38;2;7;140;156m▄\033[0m "
echo -e " \033[38;2;8;125;192m▄\033[0m \033[38;2;3;132;191m▀\033[0m\033[38;2;2;133;192m▄\033[0m \033[38;2;3;132;180m▄\033[0m\033[38;2;1;137;184m▄\033[0m\033[38;2;3;133;174m▄\033[0m \033[38;2;3;142;176m▄\033[0m\033[38;2;4;142;169m▀\033[0m \033[38;2;10;144;164m▄\033[0m "
echo -e "\033[38;2;6;121;195m▀\033[0m\033[38;2;5;128;203m▀\033[0m\033[48;2;5;124;195m\033[38;2;3;125;200m▄\033[0m\033[38;2;2;126;196m▄\033[0m\033[48;2;3;128;188m\033[38;2;1;131;196m▄\033[0m\033[48;2;0;152;219m\033[38;2;2;131;191m▄\033[0m\033[38;2;1;141;196m▀\033[0m\033[38;2;1;135;183m▀\033[0m\033[38;2;1;148;198m▀\033[0m\033[48;2;1;156;202m\033[38;2;2;135;180m▄\033[0m\033[48;2;4;134;169m\033[38;2;1;137;177m▄\033[0m\033[38;2;3;138;173m▄\033[0m\033[48;2;6;137;165m\033[38;2;2;140;170m▄\033[0m\033[38;2;7;144;169m▀\033[0m\033[38;2;7;139;158m▀\033[0m"
echo -e " \033[48;2;2;128;202m\033[38;2;2;124;201m▄\033[0m\033[48;2;1;130;201m\033[38;2;0;135;212m▄\033[0m\033[38;2;2;128;196m▄\033[0m \033[48;2;2;142;204m\033[38;2;7;138;199m▄\033[0m \033[38;2;1;135;186m▄\033[0m\033[48;2;1;142;186m\033[38;2;2;144;194m▄\033[0m\033[48;2;3;138;176m\033[38;2;2;134;176m▄\033[0m "
echo -e " \033[48;2;8;118;200m\033[38;2;8;121;209m▄\033[0m\033[38;2;3;121;203m▀\033[0m \033[38;2;3;122;192m▀\033[0m\033[38;2;1;138;216m▀\033[0m\033[48;2;0;138;210m\033[38;2;3;128;198m▄\033[0m\033[48;2;0;126;188m\033[38;2;2;131;198m▄\033[0m\033[48;2;0;142;205m\033[38;2;3;132;193m▄\033[0m\033[38;2;1;140;196m▀\033[0m \033[38;2;4;134;175m▀\033[0m\033[48;2;13;135;167m\033[38;2;8;136;174m▄\033[0m "
echo ""
echo -e " ${BLUE}HINDSIGHT CLI INSTALLER${NC}"
echo ""
}
@@ -72,16 +77,44 @@ detect_platform() {
esac
}
# Get latest version from GitHub API
get_latest_version() {
local api_url="https://api.github.com/repos/vectorize-io/hindsight/releases/latest"
local version=""
if command -v curl > /dev/null 2>&1; then
version=$(curl -fsSL "$api_url" | grep '"tag_name":' | sed -E 's/.*"tag_name": *"([^"]+)".*/\1/')
elif command -v wget > /dev/null 2>&1; then
version=$(wget -qO- "$api_url" | grep '"tag_name":' | sed -E 's/.*"tag_name": *"([^"]+)".*/\1/')
fi
if [[ -z "$version" ]]; then
print_error "Failed to get latest version from GitHub" >&2
exit 1
fi
echo "$version"
}
# Download binary
download_binary() {
local platform=$1
local download_url="${REPO_URL}/releases/latest/download/hindsight-${platform}"
local version=$2
local download_url="${REPO_URL}/releases/download/${version}/hindsight-${platform}"
local tmp_file="/tmp/hindsight-$$"
print_info "Downloading Hindsight CLI for $platform..." >&2
print_info "Downloading Hindsight CLI ${version} for $platform..." >&2
if command -v curl > /dev/null 2>&1; then
curl -fsSL "$download_url" -o "$tmp_file"
# GitHub returns 302 redirect to Azure blob storage
# curl -L sometimes fails with 503, so manually handle redirect
local redirect_url=$(curl -sI "$download_url" 2>/dev/null | grep -i "^location:" | sed 's/location: //i' | tr -d '\r\n')
if [[ -n "$redirect_url" ]]; then
curl -fsSL "$redirect_url" -o "$tmp_file"
else
# Fallback to direct download if no redirect
curl -fsSL "$download_url" -o "$tmp_file"
fi
elif command -v wget > /dev/null 2>&1; then
wget -q "$download_url" -O "$tmp_file"
else
@@ -136,8 +169,12 @@ main() {
platform=$(detect_platform)
print_info "Detected platform: $platform"
# Get latest version
version=$(get_latest_version)
print_info "Latest version: $version"
# Download binary
tmp_file=$(download_binary "$platform")
tmp_file=$(download_binary "$platform" "$version")
# Install binary
install_binary "$tmp_file"
+799
View File
@@ -0,0 +1,799 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 1572 273" style="enable-background:new 0 0 1572 273;" xml:space="preserve">
<style type="text/css">
.st0{fill:url(#SVGID_1_);}
.st1{fill:none;stroke:#FFFFFF;stroke-width:3;stroke-miterlimit:10;}
.st2{fill:#FFFFFF;}
.st3{opacity:0.1;clip-path:url(#SVGID_00000129914111459972265670000000360129721314932366_);}
.st4{opacity:0.0163;}
.st5{opacity:0.3419;}
.st6{opacity:0.3053;}
.st7{opacity:0.4947;}
.st8{opacity:0.1716;}
.st9{opacity:0.2827;}
.st10{opacity:0.4396;}
.st11{opacity:0.2191;}
.st12{opacity:0.3687;}
.st13{opacity:0.3264;}
.st14{opacity:0.2876;}
.st15{opacity:0.2448;}
.st16{opacity:0.0105;}
.st17{opacity:0.3395;}
.st18{opacity:0.1932;}
.st19{opacity:0.2467;}
.st20{opacity:0.2535;}
.st21{opacity:0.2581;}
.st22{opacity:0.4273;}
.st23{opacity:0.149;}
.st24{opacity:0.2501;}
.st25{opacity:0.0713;}
.st26{opacity:0.1763;}
.st27{opacity:0.2282;}
.st28{opacity:0.2712;}
.st29{opacity:0.384;}
.st30{opacity:0.4021;}
.st31{opacity:0.2087;}
.st32{opacity:0.42;}
.st33{opacity:0.3495;}
.st34{opacity:0.2778;}
.st35{opacity:0.2694;}
.st36{opacity:0.2895;}
.st37{opacity:0.3209;}
.st38{opacity:0.2074;}
.st39{opacity:0.4718;}
.st40{opacity:0.477;}
.st41{opacity:0.359;}
.st42{opacity:0.3551;}
.st43{opacity:0.2133;}
.st44{opacity:0.1705;}
.st45{opacity:0.3991;}
.st46{opacity:0.4943;}
.st47{opacity:0.2426;}
.st48{opacity:0.5;}
.st49{opacity:0.6;}
.st50{opacity:0.4;}
.st51{opacity:0.3;}
.st52{opacity:0.0838;}
.st53{opacity:0.3084;}
.st54{opacity:0.5;fill:#7C0FEF;}
</style>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="0" y1="136.5" x2="1572" y2="136.5">
<stop offset="0" style="stop-color:#009296"/>
<stop offset="0.838" style="stop-color:#0079CE"/>
<stop offset="1" style="stop-color:#0074D9"/>
</linearGradient>
<rect y="-0.63" class="st0" width="1572" height="274.27"/>
<line class="st1" x1="834.29" y1="190.07" x2="834.29" y2="82.93"/>
<g>
<path class="st2" d="M916.92,119.5l13.15-36.13h2.26l13.15,36.13h-2.26l-12.8-35.18h1.5l-12.75,35.18H916.92z M921.94,110.12v-2.01
h18.47v2.01H921.94z"/>
<path class="st2" d="M960.28,120.25c-2.56,0-4.71-0.63-6.46-1.89c-1.75-1.26-3.07-2.98-3.98-5.14c-0.9-2.17-1.35-4.6-1.35-7.31
c0-2.66,0.44-5.07,1.33-7.23c0.89-2.16,2.18-3.87,3.88-5.13c1.7-1.26,3.76-1.89,6.19-1.89c2.53,0,4.65,0.61,6.39,1.84
c1.73,1.23,3.04,2.92,3.93,5.07c0.89,2.15,1.33,4.6,1.33,7.34c0,2.69-0.44,5.12-1.32,7.29c-0.88,2.17-2.15,3.89-3.83,5.16
S962.67,120.25,960.28,120.25z M960.33,132.3c-1.3,0-2.62-0.18-3.94-0.55c-1.32-0.37-2.56-0.99-3.73-1.87
c-1.16-0.88-2.15-2.08-2.97-3.6l1.86-1.25c0.9,1.86,2.17,3.17,3.81,3.95c1.64,0.78,3.29,1.17,4.97,1.17c2.43,0,4.32-0.46,5.68-1.37
c1.36-0.91,2.32-2.27,2.87-4.06c0.55-1.8,0.83-4.04,0.83-6.71v-6.93h0.2V92.4h1.96V118c0,0.82-0.02,1.61-0.05,2.38
c-0.03,0.77-0.1,1.53-0.2,2.28c-0.27,2.19-0.85,4-1.74,5.42c-0.9,1.42-2.13,2.48-3.7,3.17C964.6,131.95,962.66,132.3,960.33,132.3z
M960.28,118.15c2.12,0,3.9-0.54,5.33-1.61c1.43-1.07,2.5-2.53,3.22-4.38c0.72-1.85,1.08-3.94,1.08-6.26
c0-2.36-0.36-4.45-1.09-6.29c-0.73-1.83-1.81-3.27-3.24-4.3c-1.43-1.04-3.2-1.56-5.31-1.56c-2.16,0-3.94,0.53-5.36,1.59
c-1.41,1.06-2.46,2.51-3.15,4.34c-0.69,1.83-1.03,3.9-1.03,6.21c0,2.33,0.36,4.41,1.08,6.26c0.72,1.85,1.79,3.31,3.2,4.38
C956.44,117.61,958.19,118.15,960.28,118.15z"/>
<path class="st2" d="M989.79,120.25c-2.56,0-4.76-0.58-6.61-1.73c-1.85-1.15-3.27-2.8-4.28-4.94s-1.51-4.68-1.51-7.63
c0-2.96,0.5-5.51,1.49-7.65s2.42-3.78,4.27-4.93c1.85-1.15,4.06-1.72,6.64-1.72c2.59,0,4.81,0.59,6.65,1.76
c1.84,1.17,3.25,2.87,4.23,5.09c0.98,2.23,1.47,4.91,1.47,8.05h-2.26v-0.7c-0.1-3.9-1.02-6.89-2.75-8.97
c-1.73-2.08-4.18-3.12-7.34-3.12c-3.21,0-5.7,1.07-7.48,3.2s-2.66,5.13-2.66,9s0.89,6.86,2.66,9s4.27,3.2,7.48,3.2
c2.24,0,4.21-0.53,5.9-1.58c1.69-1.05,3.04-2.57,4.04-4.54l1.76,1c-1.1,2.31-2.67,4.09-4.69,5.34
C994.76,119.63,992.43,120.25,989.79,120.25z M978.75,106.55v-2.11h22.08v2.11H978.75z"/>
<path class="st2" d="M1006.9,119.5V92.4h1.96v5.52h0.15v21.58H1006.9z M1027.02,119.5v-14.35c0-1.94-0.2-3.62-0.59-5.03
c-0.39-1.41-0.97-2.58-1.74-3.51c-0.77-0.93-1.7-1.62-2.8-2.07s-2.35-0.68-3.75-0.68c-1.66,0-3.07,0.29-4.23,0.87
c-1.16,0.58-2.1,1.37-2.82,2.37c-0.72,1-1.25,2.16-1.58,3.46c-0.33,1.3-0.5,2.68-0.5,4.14l-1.96,0.05c0-3.09,0.51-5.59,1.53-7.49
c1.02-1.9,2.37-3.28,4.05-4.15c1.68-0.87,3.52-1.3,5.51-1.3c1.37,0,2.63,0.19,3.78,0.58c1.15,0.38,2.17,0.94,3.06,1.67
c0.89,0.73,1.65,1.62,2.27,2.67c0.62,1.05,1.09,2.25,1.41,3.6c0.32,1.35,0.48,2.82,0.48,4.43v14.75H1027.02z"/>
<path class="st2" d="M1032.64,94.51V92.4h17.01v2.11H1032.64z M1049.66,119.5c-1.36,0.3-2.69,0.41-4.01,0.34s-2.5-0.39-3.54-0.95
c-1.04-0.56-1.82-1.41-2.33-2.55c-0.4-0.89-0.63-1.79-0.68-2.7c-0.05-0.91-0.08-1.95-0.08-3.12V84.88h2.11v25.64
c0,1.17,0.01,2.12,0.04,2.84c0.02,0.72,0.2,1.4,0.51,2.03c0.6,1.2,1.56,1.94,2.86,2.22c1.3,0.28,3.01,0.24,5.12-0.11V119.5z"/>
<path class="st2" d="M1064.71,119.5V83.37h2.01l15.36,33.57l15.21-33.57h2.11v36.08h-2.11V88.44l-14.05,31.06h-2.36l-14.05-31.06
v31.06H1064.71z"/>
<path class="st2" d="M1117.3,120.25c-2.56,0-4.76-0.58-6.61-1.73s-3.27-2.8-4.28-4.94c-1-2.14-1.5-4.68-1.5-7.63
c0-2.96,0.5-5.51,1.49-7.65c0.99-2.14,2.42-3.78,4.26-4.93c1.85-1.15,4.06-1.72,6.64-1.72c2.59,0,4.81,0.59,6.65,1.76
c1.84,1.17,3.25,2.87,4.23,5.09c0.98,2.23,1.47,4.91,1.47,8.05h-2.26v-0.7c-0.1-3.9-1.02-6.89-2.75-8.97
c-1.73-2.08-4.18-3.12-7.34-3.12c-3.21,0-5.7,1.07-7.48,3.2c-1.77,2.13-2.66,5.13-2.66,9s0.89,6.86,2.66,9
c1.77,2.13,4.27,3.2,7.48,3.2c2.24,0,4.21-0.53,5.9-1.58c1.69-1.05,3.04-2.57,4.04-4.54l1.76,1c-1.1,2.31-2.67,4.09-4.69,5.34
C1122.28,119.63,1119.94,120.25,1117.3,120.25z M1106.26,106.55v-2.11h22.08v2.11H1106.26z"/>
<path class="st2" d="M1134.41,119.5V92.4h1.96v5.52h0.15v21.58H1134.41z M1150.52,119.5l0.05-18.32c0-2.34-0.65-4.2-1.94-5.57
c-1.3-1.37-2.97-2.06-5.03-2.06c-2.11,0-3.81,0.72-5.12,2.16c-1.3,1.44-1.96,3.36-1.96,5.77l-1.86-0.8c0-1.74,0.39-3.29,1.18-4.65
c0.79-1.36,1.87-2.43,3.25-3.21s2.96-1.17,4.75-1.17c1.52,0,2.95,0.32,4.29,0.97c1.34,0.64,2.42,1.64,3.25,3
c0.83,1.35,1.24,3.1,1.24,5.22l-0.05,18.67H1150.52z M1166.58,119.5l0.05-18.67c0-2.26-0.66-4.04-1.98-5.33
c-1.32-1.3-2.95-1.94-4.89-1.94c-1.07,0-2.15,0.24-3.25,0.73c-1.1,0.49-2.02,1.3-2.76,2.46s-1.12,2.73-1.12,4.74h-1.86
c-0.08-1.99,0.26-3.72,1.04-5.19c0.78-1.47,1.87-2.61,3.27-3.42s3.01-1.22,4.82-1.22c2.56,0,4.67,0.8,6.34,2.41
c1.66,1.61,2.5,3.81,2.5,6.62l-0.05,18.82H1166.58z"/>
<path class="st2" d="M1186.15,120.25c-2.68,0-4.93-0.61-6.77-1.83c-1.84-1.22-3.24-2.91-4.19-5.07c-0.95-2.16-1.43-4.64-1.43-7.45
c0-2.84,0.48-5.34,1.46-7.48c0.97-2.14,2.38-3.81,4.23-4.99c1.85-1.19,4.09-1.78,6.71-1.78c2.69,0,4.96,0.61,6.8,1.82
s3.23,2.89,4.18,5.03c0.95,2.14,1.42,4.61,1.42,7.4c0,2.86-0.48,5.37-1.43,7.52c-0.95,2.15-2.35,3.83-4.2,5.03
C1191.07,119.65,1188.81,120.25,1186.15,120.25z M1186.15,118.15c3.41,0,5.96-1.13,7.63-3.4c1.67-2.27,2.51-5.21,2.51-8.84
c0-3.7-0.84-6.65-2.52-8.84c-1.68-2.2-4.22-3.3-7.62-3.3c-2.29,0-4.19,0.52-5.68,1.56s-2.61,2.47-3.35,4.29
c-0.74,1.82-1.1,3.92-1.1,6.3c0,3.68,0.86,6.64,2.57,8.88C1180.3,117.03,1182.82,118.15,1186.15,118.15z"/>
<path class="st2" d="M1204.07,119.5V92.4h1.96v6.47l-0.65-0.85c0.28-0.74,0.64-1.42,1.08-2.06c0.43-0.64,0.86-1.15,1.28-1.56
c0.74-0.72,1.61-1.27,2.62-1.64s2.03-0.59,3.05-0.65c1.02-0.06,1.92,0.04,2.71,0.29v2.01c-1.12-0.25-2.31-0.3-3.56-0.14
c-1.25,0.16-2.43,0.74-3.51,1.74c-0.95,0.89-1.63,1.91-2.02,3.06c-0.39,1.15-0.63,2.35-0.71,3.58c-0.08,1.23-0.13,2.4-0.13,3.5
v13.35H1204.07z"/>
<path class="st2" d="M1224.94,131.54l5.27-14.15l0.05,4.22l-11.89-29.21h2.31l10.64,26.3h-1.61l9.54-26.3h2.26l-14.2,39.14H1224.94
z"/>
<path class="st2" d="M1266.54,119.5V85.48h-13.25v-2.11h28.6v2.11h-13.25v34.02H1266.54z"/>
<path class="st2" d="M1285.91,119.5V83.37h1.96v21.33h0.15v14.8H1285.91z M1306.04,119.5v-14.35c0-1.94-0.2-3.62-0.59-5.03
c-0.39-1.41-0.97-2.58-1.74-3.51c-0.77-0.93-1.7-1.62-2.8-2.07s-2.35-0.68-3.75-0.68c-1.66,0-3.07,0.29-4.23,0.87
c-1.16,0.58-2.1,1.37-2.82,2.37c-0.72,1-1.25,2.16-1.58,3.46c-0.33,1.3-0.5,2.68-0.5,4.14l-1.96,0.05c0-3.09,0.51-5.59,1.53-7.49
c1.02-1.9,2.37-3.28,4.05-4.15c1.68-0.87,3.52-1.3,5.51-1.3c1.37,0,2.63,0.19,3.78,0.58c1.15,0.38,2.17,0.94,3.06,1.67
c0.89,0.73,1.65,1.62,2.27,2.67c0.62,1.05,1.09,2.25,1.41,3.6c0.32,1.35,0.48,2.82,0.48,4.43v14.75H1306.04z"/>
<path class="st2" d="M1322.55,120.25c-2.11,0-3.86-0.37-5.24-1.1c-1.39-0.74-2.43-1.71-3.11-2.91c-0.69-1.2-1.03-2.51-1.03-3.91
c0-1.59,0.35-2.91,1.04-3.95c0.69-1.05,1.6-1.87,2.72-2.47s2.33-1.04,3.61-1.3c1.57-0.32,3.26-0.6,5.06-0.84
c1.8-0.24,3.46-0.45,4.98-0.61c1.52-0.17,2.66-0.3,3.41-0.4l-0.75,0.5c0.08-3.19-0.5-5.57-1.74-7.14
c-1.25-1.56-3.44-2.35-6.59-2.35c-2.28,0-4.1,0.51-5.48,1.52s-2.35,2.56-2.9,4.65l-2.31-0.6c0.6-2.51,1.84-4.42,3.7-5.72
c1.87-1.3,4.23-1.96,7.09-1.96c2.51,0,4.6,0.51,6.27,1.53c1.67,1.02,2.81,2.4,3.41,4.14c0.23,0.67,0.4,1.48,0.5,2.43
c0.1,0.95,0.15,1.88,0.15,2.79v16.96h-1.96v-7.43l1,0.05c-0.72,2.58-2.15,4.58-4.29,6
C1327.96,119.54,1325.44,120.25,1322.55,120.25z M1322.45,118.15c1.96,0,3.68-0.35,5.18-1.05c1.5-0.7,2.71-1.72,3.64-3.05
c0.93-1.33,1.52-2.92,1.77-4.78c0.13-0.94,0.2-1.94,0.2-3.01s0-1.84,0-2.31l1.1,0.85c-0.89,0.08-2.11,0.2-3.68,0.34
c-1.56,0.14-3.22,0.33-4.96,0.55c-1.74,0.23-3.31,0.51-4.72,0.87c-0.82,0.2-1.67,0.52-2.53,0.95c-0.87,0.44-1.6,1.05-2.2,1.83
c-0.59,0.79-0.89,1.8-0.89,3.04c0,0.87,0.22,1.75,0.65,2.63c0.44,0.89,1.17,1.63,2.2,2.23
C1319.25,117.84,1320.66,118.15,1322.45,118.15z"/>
<path class="st2" d="M1338.86,94.51V92.4h17.01v2.11H1338.86z M1355.87,119.5c-1.35,0.3-2.69,0.41-4.01,0.34s-2.5-0.39-3.54-0.95
c-1.04-0.56-1.82-1.41-2.33-2.55c-0.4-0.89-0.63-1.79-0.68-2.7c-0.05-0.91-0.08-1.95-0.08-3.12V84.88h2.11v25.64
c0,1.17,0.01,2.12,0.04,2.84c0.03,0.72,0.2,1.4,0.51,2.03c0.6,1.2,1.56,1.94,2.86,2.22c1.3,0.28,3.01,0.24,5.12-0.11V119.5z"/>
<path class="st2" d="M1378.75,119.5l-10.34-36.13h2.16l9.28,32.32l9.23-32.32h2.21l9.28,32.32l9.23-32.32h2.21l-10.34,36.13h-2.21
l-9.28-32.27l-9.23,32.27H1378.75z"/>
<path class="st2" d="M1424.42,120.25c-2.68,0-4.93-0.61-6.77-1.83c-1.84-1.22-3.24-2.91-4.19-5.07c-0.95-2.16-1.43-4.64-1.43-7.45
c0-2.84,0.49-5.34,1.46-7.48c0.97-2.14,2.38-3.81,4.23-4.99c1.85-1.19,4.08-1.78,6.71-1.78c2.69,0,4.96,0.61,6.8,1.82
c1.84,1.21,3.23,2.89,4.18,5.03c0.94,2.14,1.42,4.61,1.42,7.4c0,2.86-0.48,5.37-1.43,7.52s-2.35,3.83-4.2,5.03
C1429.33,119.65,1427.08,120.25,1424.42,120.25z M1424.42,118.15c3.41,0,5.96-1.13,7.63-3.4c1.67-2.27,2.51-5.21,2.51-8.84
c0-3.7-0.84-6.65-2.52-8.84c-1.68-2.2-4.22-3.3-7.62-3.3c-2.29,0-4.19,0.52-5.68,1.56s-2.61,2.47-3.35,4.29s-1.1,3.92-1.1,6.3
c0,3.68,0.86,6.64,2.57,8.88C1418.57,117.03,1421.09,118.15,1424.42,118.15z"/>
<path class="st2" d="M1442.33,119.5V92.4h1.96v6.47l-0.65-0.85c0.28-0.74,0.64-1.42,1.08-2.06s0.86-1.15,1.28-1.56
c0.74-0.72,1.61-1.27,2.62-1.64c1.01-0.38,2.03-0.59,3.05-0.65s1.92,0.04,2.71,0.29v2.01c-1.12-0.25-2.31-0.3-3.56-0.14
c-1.25,0.16-2.43,0.74-3.51,1.74c-0.95,0.89-1.63,1.91-2.02,3.06c-0.39,1.15-0.63,2.35-0.71,3.58s-0.12,2.4-0.12,3.5v13.35H1442.33
z"/>
<path class="st2" d="M1459.14,119.5V83.37h2.11v22.08l13.55-13.05h3.16l-14.15,13.55l16.31,13.55h-3.61l-15.25-13.05v13.05H1459.14
z"/>
<path class="st2" d="M1492.01,120.2c-3.06,0-5.6-0.64-7.63-1.92c-2.02-1.28-3.28-3.05-3.76-5.31l2.16-0.4
c0.45,1.67,1.51,3.01,3.17,4.01c1.66,1,3.7,1.51,6.11,1.51c2.46,0,4.41-0.53,5.86-1.58c1.45-1.05,2.17-2.48,2.17-4.29
c0-0.99-0.22-1.79-0.65-2.42c-0.44-0.63-1.28-1.2-2.55-1.72s-3.12-1.11-5.58-1.78c-2.58-0.7-4.58-1.38-6.01-2.04
c-1.43-0.66-2.43-1.4-3.01-2.23s-0.87-1.84-0.87-3.05c0-1.44,0.43-2.71,1.28-3.81c0.85-1.1,2.03-1.97,3.54-2.58
s3.25-0.93,5.22-0.93c1.99,0,3.79,0.33,5.38,0.99c1.6,0.66,2.88,1.58,3.86,2.76c0.98,1.18,1.54,2.54,1.69,4.08l-2.16,0.4
c-0.32-1.89-1.28-3.38-2.9-4.48s-3.62-1.64-6.03-1.64c-2.26-0.03-4.1,0.43-5.53,1.38c-1.43,0.95-2.15,2.2-2.15,3.74
c0,0.85,0.24,1.58,0.73,2.2c0.49,0.61,1.33,1.17,2.53,1.67c1.2,0.5,2.88,1.02,5.02,1.56c2.71,0.69,4.82,1.38,6.33,2.08
c1.51,0.7,2.58,1.52,3.19,2.46c0.61,0.94,0.92,2.1,0.92,3.49c0,2.46-0.92,4.39-2.76,5.78S1495.22,120.2,1492.01,120.2z"/>
<path class="st2" d="M995.68,179.72v-36.13h2.11v34.02h17.97v2.11H995.68z"/>
<path class="st2" d="M1020.52,146.8v-3.21h2.11v3.21H1020.52z M1020.52,179.72v-27.1h2.11v27.1H1020.52z"/>
<path class="st2" d="M1030.16,179.72v-36.13h2.11v22.08l13.55-13.05h3.16l-14.15,13.55l16.31,13.55h-3.61l-15.25-13.05v13.05
H1030.16z"/>
<path class="st2" d="M1064.03,180.47c-2.56,0-4.76-0.58-6.61-1.73s-3.27-2.8-4.28-4.94c-1-2.14-1.5-4.68-1.5-7.63
c0-2.96,0.5-5.51,1.49-7.65c0.99-2.14,2.42-3.78,4.26-4.93c1.85-1.15,4.06-1.72,6.64-1.72c2.59,0,4.81,0.59,6.65,1.76
s3.25,2.87,4.23,5.09c0.98,2.23,1.47,4.91,1.47,8.05h-2.26v-0.7c-0.1-3.9-1.02-6.89-2.75-8.97c-1.73-2.08-4.18-3.12-7.34-3.12
c-3.21,0-5.7,1.07-7.48,3.2c-1.77,2.13-2.66,5.13-2.66,9c0,3.86,0.89,6.86,2.66,9c1.77,2.13,4.27,3.2,7.48,3.2
c2.24,0,4.21-0.53,5.9-1.58s3.04-2.57,4.04-4.54l1.76,1c-1.1,2.31-2.67,4.09-4.69,5.34
C1069.01,179.85,1066.68,180.47,1064.03,180.47z M1052.99,166.77v-2.11h22.08v2.11H1052.99z"/>
<path class="st2" d="M1091.93,179.72v-36.13h2.11v17.01h21.88v-17.01h2.11v36.13h-2.11v-17.01h-21.88v17.01H1091.93z"/>
<path class="st2" d="M1135.54,180.32c-1.37,0-2.63-0.19-3.78-0.58s-2.17-0.94-3.06-1.67c-0.9-0.73-1.65-1.62-2.27-2.67
c-0.62-1.05-1.09-2.25-1.4-3.6c-0.32-1.35-0.48-2.82-0.48-4.43v-14.75h2.11v14.35c0,1.92,0.2,3.6,0.59,5.02
c0.39,1.42,0.97,2.6,1.74,3.53c0.77,0.93,1.7,1.62,2.8,2.07c1.1,0.45,2.35,0.68,3.75,0.68c1.66,0,3.07-0.29,4.23-0.87
c1.16-0.58,2.1-1.37,2.82-2.37c0.72-1,1.25-2.16,1.58-3.46c0.33-1.3,0.5-2.69,0.5-4.14l1.96-0.05c0,3.09-0.51,5.59-1.53,7.49
c-1.02,1.9-2.37,3.28-4.05,4.15C1139.37,179.89,1137.53,180.32,1135.54,180.32z M1144.83,179.72v-5.52h-0.15v-21.58h2.11v27.1
H1144.83z"/>
<path class="st2" d="M1153.81,179.72v-27.1h1.96v5.52h0.15v21.58H1153.81z M1169.92,179.72l0.05-18.32c0-2.34-0.65-4.2-1.95-5.57
c-1.3-1.37-2.97-2.06-5.03-2.06c-2.11,0-3.81,0.72-5.12,2.16c-1.3,1.44-1.96,3.36-1.96,5.77l-1.86-0.8c0-1.74,0.39-3.29,1.18-4.65
s1.87-2.43,3.25-3.21c1.38-0.78,2.96-1.17,4.75-1.17c1.52,0,2.95,0.32,4.29,0.97c1.34,0.64,2.42,1.64,3.25,3
c0.83,1.35,1.24,3.09,1.24,5.22l-0.05,18.67H1169.92z M1185.98,179.72l0.05-18.67c0-2.26-0.66-4.04-1.98-5.33
c-1.32-1.3-2.95-1.94-4.89-1.94c-1.07,0-2.15,0.24-3.25,0.73s-2.02,1.3-2.76,2.46c-0.75,1.15-1.12,2.74-1.12,4.74h-1.86
c-0.08-1.99,0.26-3.72,1.04-5.19c0.78-1.47,1.87-2.61,3.27-3.42c1.41-0.81,3.01-1.22,4.82-1.22c2.56,0,4.67,0.8,6.33,2.41
c1.66,1.61,2.5,3.81,2.5,6.62l-0.05,18.82H1185.98z"/>
<path class="st2" d="M1202.54,180.47c-2.11,0-3.86-0.37-5.24-1.1c-1.39-0.74-2.43-1.71-3.11-2.91c-0.69-1.2-1.03-2.51-1.03-3.91
c0-1.59,0.35-2.91,1.04-3.95c0.69-1.04,1.6-1.87,2.72-2.47s2.33-1.04,3.61-1.3c1.57-0.32,3.26-0.6,5.06-0.84
c1.8-0.24,3.46-0.45,4.98-0.61c1.52-0.17,2.66-0.3,3.41-0.4l-0.75,0.5c0.08-3.2-0.5-5.57-1.74-7.14s-3.44-2.35-6.59-2.35
c-2.28,0-4.1,0.51-5.48,1.52s-2.35,2.56-2.9,4.65l-2.31-0.6c0.6-2.51,1.84-4.42,3.7-5.72s4.23-1.96,7.09-1.96
c2.51,0,4.6,0.51,6.27,1.53c1.67,1.02,2.81,2.4,3.41,4.14c0.23,0.67,0.4,1.48,0.5,2.43c0.1,0.95,0.15,1.88,0.15,2.79v16.96h-1.96
v-7.43l1,0.05c-0.72,2.58-2.15,4.58-4.29,6C1207.95,179.76,1205.43,180.47,1202.54,180.47z M1202.44,178.37
c1.96,0,3.68-0.35,5.18-1.05c1.5-0.7,2.71-1.72,3.64-3.05c0.93-1.33,1.52-2.92,1.77-4.78c0.13-0.94,0.2-1.94,0.2-3.01
s0-1.84,0-2.31l1.1,0.85c-0.89,0.08-2.11,0.2-3.68,0.34c-1.56,0.14-3.22,0.33-4.96,0.55c-1.74,0.23-3.31,0.51-4.72,0.87
c-0.82,0.2-1.67,0.52-2.53,0.95c-0.87,0.44-1.6,1.05-2.2,1.83s-0.89,1.8-0.89,3.04c0,0.87,0.22,1.75,0.65,2.63
c0.44,0.89,1.17,1.63,2.2,2.23C1199.24,178.06,1200.65,178.37,1202.44,178.37z"/>
<path class="st2" d="M1221.86,179.72v-27.1h1.96v5.52h0.15v21.58H1221.86z M1241.98,179.72v-14.35c0-1.94-0.2-3.62-0.59-5.03
c-0.39-1.41-0.97-2.58-1.74-3.51c-0.77-0.93-1.7-1.62-2.8-2.07c-1.1-0.45-2.35-0.68-3.75-0.68c-1.66,0-3.07,0.29-4.23,0.87
c-1.16,0.58-2.1,1.37-2.82,2.37c-0.72,1-1.25,2.16-1.58,3.46c-0.33,1.3-0.5,2.69-0.5,4.14l-1.96,0.05c0-3.09,0.51-5.59,1.53-7.49
c1.02-1.9,2.37-3.28,4.05-4.15c1.68-0.87,3.52-1.3,5.51-1.3c1.37,0,2.63,0.19,3.78,0.58c1.15,0.38,2.17,0.94,3.06,1.67
c0.89,0.73,1.65,1.62,2.27,2.67c0.62,1.05,1.09,2.25,1.41,3.6c0.32,1.35,0.48,2.82,0.48,4.43v14.75H1241.98z"/>
<path class="st2" d="M1260.65,179.72v-36.13h2.01l15.36,33.57l15.21-33.57h2.11v36.08h-2.11v-31.01l-14.05,31.06h-2.36
l-14.05-31.06v31.06H1260.65z"/>
<path class="st2" d="M1313.24,180.47c-2.56,0-4.76-0.58-6.61-1.73c-1.85-1.15-3.27-2.8-4.28-4.94c-1-2.14-1.51-4.68-1.51-7.63
c0-2.96,0.5-5.51,1.49-7.65c1-2.14,2.42-3.78,4.27-4.93c1.85-1.15,4.06-1.72,6.64-1.72c2.59,0,4.81,0.59,6.65,1.76
c1.84,1.17,3.25,2.87,4.23,5.09c0.98,2.23,1.47,4.91,1.47,8.05h-2.26v-0.7c-0.1-3.9-1.02-6.89-2.75-8.97
c-1.73-2.08-4.18-3.12-7.34-3.12c-3.21,0-5.7,1.07-7.48,3.2s-2.66,5.13-2.66,9c0,3.86,0.89,6.86,2.66,9s4.27,3.2,7.48,3.2
c2.24,0,4.21-0.53,5.9-1.58s3.04-2.57,4.04-4.54l1.76,1c-1.1,2.31-2.67,4.09-4.69,5.34
C1318.21,179.85,1315.88,180.47,1313.24,180.47z M1302.2,166.77v-2.11h22.08v2.11H1302.2z"/>
<path class="st2" d="M1330.35,179.72v-27.1h1.96v5.52h0.15v21.58H1330.35z M1346.46,179.72l0.05-18.32c0-2.34-0.65-4.2-1.94-5.57
c-1.3-1.37-2.97-2.06-5.03-2.06c-2.11,0-3.81,0.72-5.12,2.16c-1.3,1.44-1.96,3.36-1.96,5.77l-1.86-0.8c0-1.74,0.39-3.29,1.18-4.65
s1.87-2.43,3.25-3.21c1.38-0.78,2.96-1.17,4.75-1.17c1.52,0,2.95,0.32,4.29,0.97c1.34,0.64,2.42,1.64,3.25,3
c0.83,1.35,1.24,3.09,1.24,5.22l-0.05,18.67H1346.46z M1362.52,179.72l0.05-18.67c0-2.26-0.66-4.04-1.98-5.33
c-1.32-1.3-2.95-1.94-4.89-1.94c-1.07,0-2.15,0.24-3.25,0.73c-1.1,0.49-2.02,1.3-2.76,2.46s-1.12,2.74-1.12,4.74h-1.86
c-0.08-1.99,0.26-3.72,1.04-5.19c0.78-1.47,1.87-2.61,3.27-3.42c1.41-0.81,3.01-1.22,4.82-1.22c2.56,0,4.67,0.8,6.34,2.41
c1.66,1.61,2.5,3.81,2.5,6.62l-0.05,18.82H1362.52z"/>
<path class="st2" d="M1382.09,180.47c-2.68,0-4.93-0.61-6.77-1.83s-3.24-2.91-4.19-5.07c-0.95-2.16-1.43-4.64-1.43-7.45
c0-2.84,0.48-5.34,1.46-7.48c0.97-2.14,2.38-3.8,4.23-4.99c1.85-1.19,4.09-1.78,6.71-1.78c2.69,0,4.96,0.61,6.8,1.82
s3.23,2.89,4.18,5.03c0.95,2.14,1.42,4.61,1.42,7.4c0,2.86-0.48,5.37-1.43,7.51c-0.95,2.15-2.35,3.83-4.2,5.03
C1387,179.87,1384.75,180.47,1382.09,180.47z M1382.09,178.37c3.41,0,5.96-1.13,7.63-3.4s2.51-5.21,2.51-8.84
c0-3.7-0.84-6.64-2.52-8.84c-1.68-2.2-4.22-3.3-7.62-3.3c-2.29,0-4.19,0.52-5.68,1.56c-1.5,1.04-2.61,2.47-3.35,4.29
c-0.74,1.82-1.1,3.92-1.1,6.3c0,3.68,0.86,6.64,2.57,8.88C1376.24,177.25,1378.76,178.37,1382.09,178.37z"/>
<path class="st2" d="M1400,179.72v-27.1h1.96v6.47l-0.65-0.85c0.28-0.74,0.64-1.42,1.08-2.06c0.43-0.63,0.86-1.15,1.28-1.56
c0.74-0.72,1.61-1.27,2.62-1.64c1.01-0.38,2.03-0.59,3.05-0.65c1.02-0.06,1.92,0.04,2.71,0.29v2.01c-1.12-0.25-2.31-0.3-3.56-0.14
c-1.25,0.16-2.43,0.74-3.51,1.74c-0.95,0.89-1.63,1.91-2.02,3.06c-0.39,1.15-0.63,2.35-0.71,3.58c-0.08,1.23-0.13,2.4-0.13,3.5
v13.35H1400z"/>
<path class="st2" d="M1420.88,191.76l5.27-14.15l0.05,4.21l-11.89-29.21h2.31l10.64,26.3h-1.61l9.54-26.3h2.26l-14.2,39.14H1420.88
z"/>
</g>
<g>
<g>
<path class="st2" d="M283.59,142.33c0.82-1.5,3.64-2.46,5.65-2.69c9.56-1.09,15.67-9.18,14.1-19.11
c-1.47-9.32-10.16-14.89-19.51-12.51c-8.51,2.18-12.43,8.07-12.08,18.17c0.24,6.71-4.3,14.03-10.37,15.67
c-2.06,0.55-5.57,0.21-6.7-1.15c-3.56-4.27-6.29-9.22-9.44-14.06c7.79-7.12,13.73-15.34,16.46-25.47
c0.92-3.42,2.06-5.36,5.97-6.43c8.68-2.37,13.02-12.08,10.05-20.68c-2.75-7.95-12.56-12.68-20.56-9.9
c-8.49,2.94-13.12,11.83-9.53,20.25c2.04,4.77,1.98,9.27-0.42,12.91c-3.91,5.94-8.99,11.13-13.67,16.55
c-1.9,2.22-3.91,2.1-6.71,0.89c-14.49-6.28-29.48-7.49-44.02-0.96c-5.69,2.55-9.09,1.21-12.8-2.32c-2.23-2.13-4.63-4.26-6.27-6.82
c-4.04-6.27-7.51-12.31-4.17-20.72s-2.32-17.63-10.54-19.97c-8.32-2.37-17.64,2.66-20.03,10.81c-2.61,8.84,2.06,17.34,11.26,19.85
c2.92,0.79,3.96,2.22,4.58,5.04c1.94,8.74,6.24,16.25,12.77,22.45c1.53,1.47,2.92,3.09,4.32,4.58c-2.91,4.34-6.09,8.01-8.03,12.24
c-2.45,5.36-5.92,4.66-9.69,2.55c-6.78-3.83-9.86-9.57-9.31-17.65c0.61-9.23-6.21-16.17-15.25-16.44
c-8.8-0.26-16.23,6.72-16.47,15.49c-0.25,8.95,6.31,16.16,15.46,16.28c3.21,0.03,4.35,1.38,5.57,3.85
c3.71,7.54,9.51,12.78,17.65,15.3c1.98,0.61,3.91,1.43,6.79,2.51c-3.46,5.27-6.39,10.04-9.63,14.6c-2.33,3.29-5.42,3.99-9.54,2.88
c-9.28-2.48-17.6,2.39-20.06,11.18c-2.2,7.94,2.49,16.76,10.26,19.26c8.83,2.83,16.84-1.33,20.49-10.61
c0.58-1.47,1.21-3.1,2.32-4.15c4.19-3.96,8.58-7.7,12.86-11.5c24.89,36.49,73.84,32.67,96.09-0.54c3.15,3.01,6.53,5.78,9.35,9.05
c2.75,3.19,5.07,6.78,7.3,10.38c4.53,7.31,13.1,10.18,20.9,6.77c7.1-3.1,10.91-11.7,8.52-19.25c-2.39-7.59-11.2-13.45-18.99-11.05
c-6.36,1.96-9.73-0.57-12.79-4.99c-2.67-3.85-4.95-7.97-7.56-12.23c1.52-0.55,2.34-0.91,3.21-1.13
C271.3,157.02,278.65,151.38,283.59,142.33z M287.62,114.55c5.21-0.02,8.86,3.3,8.96,8.17c0.12,5.18-3.12,8.84-8,9.07
c-5.25,0.24-9.1-3.28-9.22-8.42C279.26,118.6,283.03,114.56,287.62,114.55z M262.44,70.74c4.96-0.13,8.97,3.49,9.1,8.19
c0.13,4.96-3.84,9.34-8.59,9.49c-4.65,0.14-9.03-4.18-9.15-9.04C253.7,74.62,257.44,70.86,262.44,70.74z M144.09,87.8
c-4.8-0.05-8.7-3.91-8.72-8.62c-0.03-4.69,3.92-8.52,8.8-8.55c5.05-0.01,8.69,3.62,8.56,8.56
C152.58,84.26,148.97,87.86,144.09,87.8z M118.94,131.61c-5.23,0.11-8.86-3.21-8.92-8.16c-0.05-4.79,4.22-9.11,9.02-9.1
c4.42,0.01,8.25,3.91,8.3,8.42C127.38,127.68,123.75,131.51,118.94,131.61z M124.96,202.12c-5.13,0.23-8.73-3.11-8.85-8.21
c-0.12-5.08,3.38-8.76,8.32-8.75c4.56,0.02,8.32,3.71,8.42,8.26C132.95,198.19,129.58,201.91,124.96,202.12z M237.19,162.84
c-8.1,11.67-19.86,16.43-32.82,17.11c-14.48-0.66-25.81-5.71-34.32-16.72c-2.43-3.13-2.41-5.44,0.05-8.52
c17.66-22.19,51.63-21.27,67.44,2.11C238.46,158.18,238.2,161.38,237.19,162.84z M282.26,185.08c4.85-0.03,8.73,3.77,8.73,8.58
c0,4.88-3.87,8.71-8.72,8.6c-4.85-0.11-8.49-3.98-8.39-8.91C273.97,188.65,277.54,185.12,282.26,185.08z"/>
<path class="st2" d="M212.76,171.11c-5.31,4.28-13.32,4.22-18.65-0.13c-5.67-4.63-7.02-12.7-3.21-19.18
c3.69-6.26,10.56-9,17.81-6.53c-4.14,3.5-5.56,7.18-1.64,10.95c3.48,3.36,6.6,1.42,9.27-1.62
C219.24,160.01,217.62,167.2,212.76,171.11z"/>
<path class="st2" d="M263.24,75.51c-2.8-0.18-4.86,2.16-4.65,4.26c0.2,1.96,2.41,3.76,4.98,3.34c1.76-0.36,3.06-1.81,3.19-3.45
C266.92,77.7,265.41,75.81,263.24,75.51z M264.53,78.18c-0.38,0-0.69-0.32-0.69-0.7c0-0.38,0.31-0.7,0.69-0.7
c0.39,0,0.7,0.32,0.7,0.7C265.23,77.87,264.93,78.18,264.53,78.18z"/>
<path class="st2" d="M116.96,119.24c-1.65,0.7-2.64,2.4-2.43,4.12c0.19,1.6,1.37,2.94,2.97,3.37c3.12,0.07,5.33-1.99,5.37-3.69
C122.9,121.22,120.43,118.84,116.96,119.24z M117.99,121.1c-0.38,0-0.7-0.31-0.7-0.69c0-0.38,0.32-0.7,0.7-0.7
c0.38,0,0.7,0.32,0.7,0.7C118.69,120.79,118.37,121.1,117.99,121.1z"/>
<path class="st2" d="M289.45,119.63c-3.11-0.38-5.39,1.83-5.36,3.6c0.02,1.71,2.2,3.75,5.15,3.5c1.48-0.48,2.52-1.82,2.61-3.35
C291.95,121.76,290.98,120.24,289.45,119.63z M290.73,123.54c-0.38,0-0.7-0.31-0.7-0.7c0-0.38,0.32-0.69,0.7-0.69
c0.38,0,0.7,0.31,0.7,0.69C291.43,123.23,291.11,123.54,290.73,123.54z"/>
<path class="st2" d="M142.88,75.67c-1.78,0.24-3.13,1.67-3.23,3.3c-0.12,1.89,1.46,3.69,3.57,3.8c3.11,0.21,5.3-2.01,5.24-3.73
C148.4,77.3,146.02,75.19,142.88,75.67z M141.54,78.28c-0.38,0-0.69-0.31-0.69-0.7c0-0.38,0.31-0.69,0.69-0.69
c0.39,0,0.7,0.31,0.7,0.69C142.24,77.98,141.94,78.28,141.54,78.28z"/>
<path class="st2" d="M283,190.25c-2.23-0.35-4.1,1.32-4.17,3.06c-0.07,1.77,1.77,3.6,4.07,3.33c1.7-0.04,3.07-1.42,3.12-3.11
C286.08,191.83,284.73,190.36,283,190.25z M284.58,193.08c-0.39,0-0.7-0.31-0.7-0.69c0-0.39,0.31-0.7,0.7-0.7
c0.38,0,0.69,0.31,0.69,0.7C285.27,192.77,284.96,193.08,284.58,193.08z"/>
<path class="st2" d="M122.37,190.01c-1.72,0.68-2.72,2.42-2.46,4.14c0.23,1.51,1.4,2.75,2.94,3.11c2.55,0.09,4.46-1.72,4.55-3.44
C127.5,191.9,125.31,189.65,122.37,190.01z M123.63,191.91c-0.38,0-0.69-0.31-0.69-0.7c0-0.38,0.31-0.69,0.69-0.69
c0.39,0,0.7,0.31,0.7,0.69C124.33,191.6,124.03,191.91,123.63,191.91z"/>
</g>
<g>
<path class="st2" d="M383.47,127.79v49.7h-9.97v-21.15h-21.29v21.15h-9.97v-49.7h9.97v20.44h21.29v-20.44H383.47z"/>
<path class="st2" d="M393.97,131.68c-1.16-1.11-1.74-2.5-1.74-4.17c0-1.66,0.58-3.05,1.74-4.17c1.16-1.11,2.62-1.67,4.38-1.67
c1.76,0,3.22,0.56,4.38,1.67c1.16,1.12,1.74,2.5,1.74,4.17c0,1.66-0.58,3.05-1.74,4.17c-1.16,1.12-2.62,1.67-4.38,1.67
C396.59,133.35,395.13,132.79,393.97,131.68z"/>
<path class="st2" d="M547.84,131.68c-1.16-1.11-1.74-2.5-1.74-4.17c0-1.66,0.58-3.05,1.74-4.17c1.16-1.11,2.62-1.67,4.38-1.67
c1.76,0,3.22,0.56,4.38,1.67c1.16,1.12,1.74,2.5,1.74,4.17c0,1.66-0.58,3.05-1.74,4.17c-1.16,1.12-2.62,1.67-4.38,1.67
C550.46,133.35,549,132.79,547.84,131.68z"/>
<rect x="393.3" y="138.05" class="st2" width="9.97" height="39.45"/>
<path class="st2" d="M446.34,141.93c2.89,2.97,4.34,7.11,4.34,12.42v23.14h-9.97v-21.79c0-3.13-0.78-5.54-2.35-7.23
c-1.57-1.68-3.7-2.53-6.41-2.53c-2.75,0-4.93,0.84-6.52,2.53c-1.59,1.69-2.39,4.09-2.39,7.23v21.79h-9.97v-39.45h9.97v4.91
c1.33-1.71,3.03-3.05,5.09-4.02c2.06-0.97,4.33-1.46,6.8-1.46C439.65,137.48,443.45,138.96,446.34,141.93z"/>
<path class="st2" d="M459.98,147.02c1.59-3.09,3.76-5.46,6.51-7.12c2.75-1.66,5.81-2.49,9.19-2.49c2.56,0,5.01,0.56,7.33,1.67
c2.33,1.12,4.18,2.6,5.55,4.45V124.8h10.11v52.69h-10.11v-5.84c-1.23,1.95-2.97,3.51-5.2,4.7c-2.23,1.19-4.82,1.78-7.76,1.78
c-3.32,0-6.36-0.85-9.11-2.56c-2.75-1.71-4.93-4.12-6.51-7.23c-1.59-3.11-2.39-6.68-2.39-10.72
C457.59,153.64,458.39,150.11,459.98,147.02z M487.21,151.54c-0.95-1.73-2.23-3.06-3.84-3.99c-1.61-0.93-3.35-1.39-5.2-1.39
c-1.85,0-3.56,0.45-5.13,1.35c-1.57,0.9-2.84,2.22-3.81,3.95c-0.97,1.73-1.46,3.79-1.46,6.16c0,2.37,0.49,4.45,1.46,6.23
c0.97,1.78,2.25,3.15,3.84,4.09c1.59,0.95,3.29,1.42,5.09,1.42c1.85,0,3.58-0.46,5.2-1.39c1.61-0.93,2.89-2.25,3.84-3.99
c0.95-1.73,1.42-3.81,1.42-6.23C488.64,155.35,488.16,153.27,487.21,151.54z"/>
<path class="st2" d="M514.84,176.39c-2.56-1.16-4.59-2.74-6.09-4.73c-1.5-1.99-2.31-4.2-2.46-6.62h10.04
c0.19,1.52,0.94,2.78,2.24,3.77c1.31,1,2.93,1.5,4.88,1.5c1.9,0,3.38-0.38,4.45-1.14c1.07-0.76,1.6-1.73,1.6-2.92
c0-1.28-0.65-2.24-1.96-2.88c-1.31-0.64-3.38-1.34-6.23-2.1c-2.94-0.71-5.35-1.45-7.23-2.21c-1.88-0.76-3.49-1.92-4.84-3.49
c-1.35-1.57-2.03-3.68-2.03-6.34c0-2.18,0.63-4.18,1.89-5.98c1.26-1.8,3.06-3.23,5.41-4.27c2.35-1.04,5.11-1.57,8.3-1.57
c4.7,0,8.45,1.17,11.25,3.52c2.8,2.35,4.34,5.52,4.63,9.51h-9.54c-0.14-1.57-0.8-2.81-1.96-3.74c-1.16-0.93-2.72-1.39-4.66-1.39
c-1.8,0-3.19,0.33-4.17,1c-0.97,0.66-1.46,1.59-1.46,2.78c0,1.33,0.66,2.34,1.99,3.03c1.33,0.69,3.39,1.39,6.19,2.1
c2.85,0.71,5.2,1.45,7.05,2.21c1.85,0.76,3.45,1.93,4.81,3.52c1.35,1.59,2.05,3.69,2.1,6.3c0,2.28-0.63,4.32-1.89,6.12
c-1.26,1.8-3.06,3.22-5.41,4.24c-2.35,1.02-5.09,1.53-8.22,1.53C520.3,178.13,517.4,177.55,514.84,176.39z"/>
<rect x="547.23" y="138.05" class="st2" width="9.97" height="39.45"/>
<path class="st2" d="M590.24,139.15c2.23,1.16,3.99,2.67,5.27,4.52v-5.62h10.04v39.73c0,3.65-0.74,6.92-2.21,9.79
c-1.47,2.87-3.68,5.15-6.62,6.84c-2.94,1.68-6.5,2.53-10.68,2.53c-5.6,0-10.19-1.31-13.78-3.92c-3.58-2.61-5.61-6.17-6.09-10.68
h9.9c0.52,1.8,1.65,3.24,3.38,4.31c1.73,1.07,3.83,1.6,6.3,1.6c2.89,0,5.24-0.87,7.05-2.6c1.8-1.73,2.71-4.36,2.71-7.87v-6.12
c-1.28,1.85-3.05,3.39-5.3,4.63c-2.26,1.23-4.83,1.85-7.73,1.85c-3.32,0-6.36-0.85-9.11-2.56c-2.75-1.71-4.93-4.12-6.52-7.23
c-1.59-3.11-2.39-6.68-2.39-10.72c0-3.99,0.79-7.52,2.39-10.61c1.59-3.09,3.75-5.46,6.48-7.12c2.73-1.66,5.78-2.49,9.15-2.49
C585.42,137.41,588.01,137.99,590.24,139.15z M594.09,151.54c-0.95-1.73-2.23-3.06-3.85-3.99c-1.61-0.93-3.35-1.39-5.2-1.39
c-1.85,0-3.56,0.45-5.13,1.35c-1.57,0.9-2.84,2.22-3.81,3.95c-0.97,1.73-1.46,3.79-1.46,6.16c0,2.37,0.49,4.45,1.46,6.23
c0.97,1.78,2.25,3.15,3.85,4.09c1.59,0.95,3.29,1.42,5.09,1.42c1.85,0,3.58-0.46,5.2-1.39c1.61-0.93,2.89-2.25,3.85-3.99
c0.95-1.73,1.42-3.81,1.42-6.23C595.51,155.35,595.03,153.27,594.09,151.54z"/>
<path class="st2" d="M645.49,139.44c2.33,1.31,4.14,3.23,5.45,5.77c1.31,2.54,1.96,5.59,1.96,9.15v23.14h-9.97v-21.79
c0-3.13-0.78-5.54-2.35-7.23c-1.57-1.68-3.7-2.53-6.41-2.53c-2.75,0-4.93,0.84-6.52,2.53c-1.59,1.69-2.39,4.09-2.39,7.23v21.79
h-9.97V124.8h9.97v18.16c1.28-1.71,2.99-3.05,5.13-4.02c2.14-0.97,4.51-1.46,7.12-1.46C640.51,137.48,643.17,138.13,645.49,139.44
z"/>
<path class="st2" d="M673.97,146.24v19.08c0,1.33,0.32,2.29,0.96,2.88c0.64,0.59,1.72,0.89,3.24,0.89h4.63v8.4h-6.27
c-8.4,0-12.6-4.08-12.6-12.25v-19.01h-4.7v-8.19h4.7v-9.75h10.04v9.75h8.83v8.19H673.97z"/>
</g>
</g>
<g>
<defs>
<rect id="SVGID_00000142868261346849394960000000696206683628523147_" y="-0.63" width="1572" height="274.27"/>
</defs>
<clipPath id="SVGID_00000022529371134204594770000016760592095366508674_">
<use xlink:href="#SVGID_00000142868261346849394960000000696206683628523147_" style="overflow:visible;"/>
</clipPath>
<g style="opacity:0.1;clip-path:url(#SVGID_00000022529371134204594770000016760592095366508674_);">
<g>
<g>
<polygon class="st4" points="1223.46,207.5 1398.13,271.54 1221.09,290.75 "/>
<g class="st5">
<path d="M1150.25,94.8c-0.06-0.14-0.2-0.24-0.36-0.25c-0.24-0.02-0.45,0.16-0.47,0.4l11.38,97.26
c-0.02,0.24,0.16,0.45,0.4,0.47c0.24,0.02,0.45-0.16,0.47-0.4l-11.38-97.26C1150.29,94.95,1150.28,94.87,1150.25,94.8z"/>
</g>
<g class="st6">
<path d="M1250.24,31.89c-0.09-0.2-0.32-0.3-0.54-0.23l-122,142.47c-0.23,0.08-0.35,0.33-0.28,0.56
c0.07,0.23,0.33,0.35,0.56,0.28l122-142.47c0.23-0.08,0.35-0.33,0.28-0.56C1250.25,31.92,1250.25,31.91,1250.24,31.89z"/>
</g>
<g class="st7">
<path d="M1250.24,31.89c-0.08-0.17-0.26-0.27-0.45-0.25l-99.99,62.91c-0.24,0.03-0.41,0.25-0.38,0.49
c0.03,0.24,0.25,0.41,0.49,0.38l99.99-62.91c0.24-0.03,0.41-0.25,0.38-0.49C1250.27,31.98,1250.26,31.94,1250.24,31.89z"/>
</g>
<g class="st8">
<path d="M1186.09,266.15c-0.08-0.17-0.25-0.27-0.45-0.25c-0.24,0.03-0.42,0.25-0.39,0.49l-18.15,56.38
c0.03,0.24,0.24,0.42,0.49,0.39c0.24-0.03,0.41-0.24,0.39-0.49l18.15-56.38C1186.12,266.24,1186.11,266.19,1186.09,266.15z"/>
</g>
<g class="st9">
<path d="M1186.09,266.15c-0.1-0.21-0.35-0.31-0.56-0.22c-0.23,0.09-0.33,0.35-0.24,0.57l11.33,96.24
c0.09,0.23,0.35,0.33,0.57,0.24c0.23-0.09,0.33-0.35,0.24-0.57l-11.33-96.24C1186.1,266.16,1186.09,266.16,1186.09,266.15z"/>
</g>
<g class="st10">
<path d="M1302.41,175.25c-0.04-0.08-0.1-0.15-0.18-0.19L1250.07,31.7c-0.21-0.12-0.48-0.05-0.6,0.16
c-0.12,0.21-0.05,0.48,0.16,0.6l52.17,143.35c0.21,0.12,0.48,0.05,0.6-0.16C1302.47,175.53,1302.47,175.37,1302.41,175.25z"/>
</g>
<g class="st11">
<path d="M1209.47,165.87c-0.1-0.22-0.36-0.31-0.58-0.21l-47.85,26.2c-0.22,0.1-0.32,0.36-0.22,0.58l0,0
c0.1,0.22,0.36,0.31,0.58,0.21l47.85-26.2C1209.48,166.35,1209.58,166.09,1209.47,165.87L1209.47,165.87z"/>
</g>
<g class="st12">
<path d="M1186.09,266.15c-0.01-0.01-0.01-0.02-0.02-0.03l-16.16-36.38c-0.12-0.21-0.39-0.28-0.6-0.16
c-0.21,0.12-0.26,0.4-0.16,0.6l16.16,36.38c0.12,0.21,0.39,0.28,0.6,0.16C1186.11,266.6,1186.19,266.35,1186.09,266.15z"/>
</g>
<g class="st13">
<path d="M1186.09,266.15c-0.03-0.05-0.06-0.1-0.11-0.15l-40.24-34.93c-0.18-0.16-0.46-0.14-0.62,0.04
c-0.16,0.18-0.14,0.46,0.04,0.62l40.24,34.93c0.18,0.16,0.46,0.14,0.62-0.04C1186.14,266.49,1186.16,266.3,1186.09,266.15z"/>
</g>
<g class="st14">
<path d="M1209.47,165.87c-0.04-0.08-0.1-0.15-0.18-0.2c-0.21-0.12-0.48-0.04-0.6,0.17l-23.38,100.28
c-0.12,0.21-0.04,0.48,0.17,0.6c0.21,0.12,0.48,0.04,0.6-0.17l23.38-100.28C1209.53,166.14,1209.53,165.99,1209.47,165.87z"/>
</g>
<g class="st15">
<path d="M1197.42,362.39c-0.07-0.16-0.24-0.26-0.42-0.25c-0.24,0.01-0.43,0.22-0.42,0.46l24.41-21.21
c0.01,0.24,0.22,0.43,0.46,0.42c0.24-0.01,0.43-0.22,0.42-0.46l-24.41,21.21C1197.46,362.5,1197.44,362.44,1197.42,362.39z"/>
</g>
<g class="st16">
<path d="M1302.41,175.25c0-0.01,0-0.01-0.01-0.02c-0.11-0.22-0.38-0.3-0.59-0.19l-132.48,54.53c-0.22,0.11-0.27,0.38-0.19,0.59
c0.11,0.22,0.38,0.3,0.59,0.19l132.48-54.53C1302.43,175.71,1302.51,175.46,1302.41,175.25z"/>
</g>
<g class="st17">
<path d="M1302.41,175.25c-0.01-0.01-0.01-0.02-0.02-0.04c-0.12-0.21-0.39-0.28-0.6-0.16l-92.94-9.38
c-0.21,0.12-0.28,0.38-0.16,0.6c0.12,0.21,0.39,0.28,0.6,0.16l92.94,9.38C1302.43,175.7,1302.51,175.45,1302.41,175.25z"/>
</g>
<g class="st18">
<path d="M1299.37,64.28c-0.03-0.06-0.07-0.11-0.13-0.16c-0.19-0.15-0.47-0.12-0.62,0.08l3.04,110.97
c-0.15,0.19-0.11,0.47,0.08,0.62c0.19,0.15,0.47,0.12,0.62-0.08l-3.04-110.97C1299.42,64.6,1299.44,64.42,1299.37,64.28z"/>
</g>
<g class="st19">
<path d="M1299.37,64.28c-0.02-0.03-0.04-0.07-0.06-0.1c-0.16-0.18-0.44-0.21-0.62-0.05l-89.89,101.59
c-0.18,0.16-0.2,0.43-0.05,0.62c0.16,0.18,0.44,0.21,0.62,0.05l89.89-101.59C1299.41,64.67,1299.45,64.46,1299.37,64.28z"/>
</g>
<g class="st20">
<path d="M1339.23,159.8c-0.09-0.2-0.33-0.3-0.54-0.23l-129.76,6.07c-0.23,0.08-0.35,0.33-0.27,0.56
c0.06,0.22,0.33,0.35,0.56,0.27l129.76-6.07c0.23-0.08,0.35-0.33,0.27-0.56C1339.25,159.83,1339.24,159.81,1339.23,159.8z"/>
</g>
<g class="st21">
<path d="M1315.89,430.57c-0.08-0.17-0.27-0.28-0.46-0.25l-94.06-89.4c-0.24,0.04-0.4,0.26-0.37,0.5
c0.04,0.24,0.26,0.4,0.5,0.37l94.06,89.4c0.24-0.04,0.41-0.26,0.37-0.5C1315.92,430.65,1315.91,430.61,1315.89,430.57z"/>
</g>
<g class="st22">
<path d="M1414.38-19.24c-0.02-0.05-0.05-0.09-0.1-0.13c-0.18-0.17-0.45-0.16-0.62,0.02l-75.14,179.04
c-0.17,0.18-0.16,0.45,0.02,0.62c0.18,0.17,0.45,0.16,0.62-0.02l75.14-179.04C1414.42-18.88,1414.45-19.08,1414.38-19.24z"/>
</g>
<g class="st23">
<path d="M1339.23,159.8c-0.01-0.02-0.02-0.04-0.03-0.06c-0.14-0.2-0.41-0.25-0.61-0.12l-115.38,47.51
c-0.2,0.14-0.25,0.41-0.12,0.61c0.14,0.2,0.41,0.25,0.61,0.12l115.38-47.51C1339.26,160.23,1339.32,159.99,1339.23,159.8z"/>
</g>
<g class="st24">
<path d="M1570.13,127.23c-0.08-0.17-0.27-0.28-0.47-0.25L1298.9,64.03c-0.24,0.04-0.4,0.26-0.36,0.5
c0.04,0.24,0.26,0.4,0.5,0.36l270.76,62.95c0.24-0.04,0.4-0.26,0.36-0.5C1570.16,127.3,1570.14,127.26,1570.13,127.23z"/>
</g>
<g class="st25">
<path d="M1299.37,64.28c-0.07-0.15-0.23-0.26-0.41-0.25c-0.24,0.01-0.43,0.21-0.43,0.45l19.81,129.02
c0.01,0.24,0.21,0.43,0.45,0.43c0.24-0.01,0.43-0.21,0.43-0.45l-19.81-129.02C1299.41,64.39,1299.39,64.33,1299.37,64.28z"/>
</g>
<g class="st26">
<path d="M1412.78,202.53c-0.02-0.04-0.05-0.08-0.08-0.12L1299.29,64.16c-0.17-0.17-0.45-0.18-0.62-0.01
c-0.17,0.17-0.18,0.45-0.01,0.62l113.41,138.25c0.17,0.17,0.45,0.18,0.62,0.01C1412.82,202.9,1412.85,202.7,1412.78,202.53z"/>
</g>
<g class="st27">
<path d="M1221.49,290.56c-0.08-0.18-0.28-0.28-0.48-0.25c-0.24,0.05-0.39,0.28-0.35,0.52l94.41,140.02
c0.05,0.24,0.28,0.39,0.52,0.35c0.24-0.05,0.39-0.28,0.35-0.52l-94.41-140.02C1221.51,290.63,1221.5,290.59,1221.49,290.56z"/>
</g>
<g class="st28">
<path d="M1221.49,290.56c-0.06-0.12-0.16-0.21-0.3-0.24c-0.24-0.05-0.47,0.09-0.52,0.33l1.6,122.5
c-0.06,0.24,0.09,0.47,0.33,0.53c0.24,0.05,0.47-0.09,0.53-0.33l-1.6-122.5C1221.54,290.74,1221.53,290.64,1221.49,290.56z"/>
</g>
<g class="st29">
<path d="M1262.86,318.75c-0.07-0.15-0.23-0.26-0.4-0.25c-0.24,0-0.44,0.2-0.43,0.45l53.03,111.82c0,0.24,0.2,0.44,0.45,0.43
c0.24,0,0.44-0.2,0.43-0.45l-53.03-111.82C1262.9,318.87,1262.89,318.81,1262.86,318.75z"/>
</g>
<g class="st30">
<path d="M1262.86,318.75c-0.04-0.08-0.1-0.15-0.19-0.2c-0.21-0.12-0.48-0.04-0.6,0.18l-39.78,94.31
c-0.11,0.21-0.03,0.48,0.18,0.6c0.21,0.11,0.48,0.04,0.59-0.18l39.78-94.31C1262.92,319.02,1262.92,318.87,1262.86,318.75z"/>
</g>
<g class="st31">
<path d="M1570.13,127.23c-0.1-0.22-0.36-0.31-0.58-0.21l-230.89,32.57c-0.22,0.1-0.32,0.36-0.22,0.58l0,0
c0.1,0.22,0.36,0.31,0.58,0.21l230.89-32.57C1570.13,127.71,1570.23,127.45,1570.13,127.23L1570.13,127.23z"/>
</g>
<g class="st32">
<path d="M1339.23,159.8c-0.06-0.14-0.19-0.24-0.36-0.25c-0.24-0.02-0.46,0.15-0.48,0.4l-20.05,33.5
c-0.02,0.24,0.15,0.46,0.39,0.48c0.24,0.02,0.46-0.15,0.48-0.4l20.05-33.5C1339.28,159.95,1339.27,159.87,1339.23,159.8z"/>
</g>
<g class="st33">
<path d="M1339.23,159.8c-0.08-0.17-0.26-0.27-0.45-0.25c-0.24,0.03-0.41,0.25-0.38,0.49l59.29,111.55
c0.03,0.24,0.25,0.41,0.49,0.38c0.24-0.03,0.41-0.25,0.38-0.49l-59.29-111.55C1339.27,159.88,1339.25,159.84,1339.23,159.8z"/>
</g>
<g class="st34">
<path d="M1221.49,290.56c-0.03-0.06-0.07-0.11-0.12-0.16l-76.78-78.25c-0.19-0.15-0.47-0.12-0.62,0.07
c-0.15,0.19-0.12,0.47,0.07,0.62l76.78,78.25c0.19,0.15,0.47,0.12,0.62-0.07C1221.54,290.89,1221.55,290.71,1221.49,290.56z"/>
</g>
<g class="st35">
<path d="M1331.87,304.53c-0.06-0.12-0.17-0.22-0.32-0.24c-0.24-0.05-0.47,0.11-0.51,0.35l-15.98,126.05
c-0.05,0.24,0.11,0.47,0.35,0.51c0.24,0.05,0.47-0.11,0.51-0.35l15.98-126.05C1331.93,304.7,1331.91,304.61,1331.87,304.53z"/>
</g>
<g class="st36">
<path d="M1331.87,304.53c-0.03-0.07-0.08-0.13-0.15-0.18c-0.2-0.14-0.47-0.09-0.61,0.12L1222.32,413
c-0.14,0.2-0.08,0.47,0.12,0.61c0.2,0.14,0.47,0.09,0.61-0.12l108.79-108.53C1331.93,304.83,1331.94,304.66,1331.87,304.53z"/>
</g>
<g class="st37">
<path d="M1422.95,366.77c-0.06-0.13-0.19-0.23-0.35-0.25l-199.87,46.29c-0.24-0.03-0.46,0.15-0.49,0.39
c-0.03,0.24,0.14,0.46,0.39,0.49l199.87-46.29c0.24,0.03,0.46-0.15,0.49-0.39C1423,366.92,1422.99,366.84,1422.95,366.77z"/>
</g>
<g class="st38">
<path d="M1319.18,193.3c-0.07-0.15-0.23-0.26-0.4-0.25l-174.48,19c-0.24,0-0.44,0.2-0.43,0.45c0.01,0.24,0.2,0.44,0.45,0.43
l174.48-19c0.24,0,0.44-0.2,0.43-0.45C1319.22,193.42,1319.21,193.36,1319.18,193.3z"/>
</g>
<g class="st39">
<path d="M1398.52,271.35c-0.07-0.14-0.2-0.24-0.37-0.25l-174.67-64.04c-0.24-0.02-0.45,0.16-0.47,0.41
c-0.02,0.24,0.16,0.45,0.41,0.47l174.67,64.04c0.24,0.02,0.45-0.16,0.47-0.41C1398.57,271.5,1398.55,271.42,1398.52,271.35z"/>
</g>
<g class="st40">
<path d="M1319.18,193.3c-0.03-0.07-0.09-0.14-0.16-0.18c-0.21-0.13-0.48-0.07-0.61,0.13l-97.7,97.25
c-0.13,0.21-0.07,0.48,0.13,0.61c0.21,0.13,0.48,0.07,0.61-0.13l97.7-97.25C1319.24,193.6,1319.24,193.44,1319.18,193.3z"/>
</g>
<g class="st41">
<path d="M1331.87,304.53c-0.02-0.04-0.04-0.07-0.07-0.11l-108.02-97.22c-0.16-0.18-0.44-0.2-0.62-0.03
c-0.18,0.16-0.19,0.44-0.03,0.62l108.02,97.22c0.16,0.18,0.44,0.2,0.62,0.03C1331.92,304.91,1331.95,304.7,1331.87,304.53z"/>
</g>
<g class="st42">
<path d="M1319.18,193.3c-0.05-0.11-0.15-0.2-0.28-0.24c-0.23-0.07-0.48,0.07-0.54,0.3l-56.32,125.45
c-0.07,0.23,0.07,0.48,0.3,0.55c0.23,0.07,0.48-0.07,0.54-0.3l56.32-125.45C1319.24,193.51,1319.23,193.4,1319.18,193.3z"/>
</g>
<g class="st43">
<path d="M1495.96,150.44c-0.02-0.05-0.06-0.09-0.1-0.13c-0.18-0.17-0.46-0.16-0.62,0.02l-97.44,120.92
c-0.17,0.18-0.16,0.46,0.02,0.62c0.18,0.17,0.46,0.16,0.62-0.02l97.44-120.92C1496.01,150.79,1496.03,150.6,1495.96,150.44z"/>
</g>
<g class="st44">
<path d="M1570.13,127.23c-0.03-0.06-0.07-0.11-0.12-0.15c-0.19-0.16-0.46-0.13-0.62,0.06l-157.35,75.31
c-0.16,0.19-0.13,0.47,0.06,0.62c0.19,0.15,0.46,0.13,0.62-0.06l157.35-75.31C1570.18,127.56,1570.2,127.38,1570.13,127.23z"/>
</g>
<g class="st45">
<path d="M1319.18,193.3c-0.06-0.13-0.19-0.23-0.34-0.25c-0.24-0.03-0.46,0.14-0.49,0.38l12.69,111.22
c-0.03,0.24,0.14,0.46,0.38,0.49c0.24,0.03,0.46-0.14,0.49-0.38l-12.69-111.22C1319.23,193.46,1319.22,193.38,1319.18,193.3z"
/>
</g>
<g class="st46">
<path d="M1398.52,271.35c-0.05-0.1-0.14-0.19-0.25-0.23c-0.23-0.08-0.48,0.04-0.56,0.27l-66.65,33.17
c-0.08,0.23,0.04,0.48,0.27,0.56c0.23,0.08,0.48-0.04,0.56-0.27l66.65-33.17C1398.58,271.57,1398.57,271.46,1398.52,271.35z"/>
</g>
<g class="st47">
<path d="M1422.95,366.77c-0.01-0.02-0.03-0.05-0.04-0.07l-91.08-62.24c-0.14-0.2-0.42-0.24-0.61-0.1
c-0.2,0.14-0.24,0.41-0.1,0.61l91.08,62.24c0.14,0.2,0.42,0.24,0.61,0.1C1422.99,367.19,1423.04,366.96,1422.95,366.77z"/>
</g>
<g class="st48">
<path d="M1150.25,94.8c-0.02-0.04-0.04-0.08-0.08-0.11c-0.17-0.18-0.44-0.19-0.62-0.02l-22.01,79.56
c-0.18,0.17-0.19,0.45-0.02,0.62c0.17,0.18,0.44,0.19,0.62,0.02l22.01-79.56C1150.29,95.18,1150.33,94.97,1150.25,94.8z"/>
</g>
<g class="st48">
<path d="M1161.63,192.07C1161.63,192.06,1161.63,192.06,1161.63,192.07l-33.39-17.71c-0.11-0.22-0.37-0.31-0.59-0.2
c-0.22,0.11-0.28,0.38-0.2,0.59l33.38,17.7c0.11,0.22,0.37,0.31,0.59,0.2C1161.64,192.54,1161.73,192.28,1161.63,192.07z"/>
</g>
<g class="st48">
<path d="M1197.42,362.39c-0.06-0.13-0.18-0.22-0.33-0.25l-29.48-39.86c-0.24-0.04-0.46,0.13-0.5,0.37
c-0.04,0.24,0.13,0.47,0.37,0.5l29.48,39.86c0.24,0.04,0.46-0.13,0.5-0.37C1197.47,362.55,1197.45,362.47,1197.42,362.39z"/>
</g>
<g class="st48">
<path d="M1161.63,192.07c-0.08-0.17-0.26-0.27-0.45-0.25c-0.24,0.03-0.41,0.25-0.38,0.49l24.46,74.08
c0.03,0.24,0.25,0.41,0.49,0.38c0.24-0.03,0.41-0.25,0.38-0.49l-24.46-74.08C1161.66,192.15,1161.65,192.11,1161.63,192.07z"/>
</g>
<g class="st48">
<path d="M1169.93,229.77c-0.06-0.12-0.17-0.22-0.32-0.25c-0.24-0.04-0.47,0.11-0.52,0.35l-24.08,1.44
c-0.04,0.24,0.11,0.47,0.35,0.51c0.24,0.04,0.47-0.11,0.52-0.35l24.08-1.44C1169.98,229.95,1169.97,229.85,1169.93,229.77z"/>
</g>
<g class="st48">
<path d="M1209.47,165.87c-0.1-0.22-0.36-0.31-0.58-0.21l-39.54,63.9c-0.22,0.1-0.32,0.36-0.22,0.58l0,0.01
c0.1,0.22,0.36,0.31,0.58,0.22l39.54-63.9C1209.48,166.36,1209.58,166.1,1209.47,165.87
C1209.48,165.88,1209.48,165.87,1209.47,165.87z"/>
</g>
<g class="st48">
<path d="M1209.47,165.87c-0.01-0.02-0.02-0.04-0.04-0.06c-0.14-0.2-0.41-0.25-0.61-0.11l-63.62,65.35
c-0.2,0.14-0.26,0.41-0.11,0.61c0.14,0.2,0.41,0.25,0.61,0.11l63.62-65.35C1209.51,166.29,1209.56,166.06,1209.47,165.87z"/>
</g>
<g class="st48">
<path d="M1299.37,64.28c-0.08-0.17-0.27-0.28-0.47-0.25c-0.24,0.04-0.4,0.27-0.36,0.51l39.87,95.52
c0.05,0.24,0.27,0.4,0.51,0.36c0.24-0.04,0.4-0.27,0.36-0.51l-39.87-95.52C1299.4,64.35,1299.38,64.32,1299.37,64.28z"/>
</g>
<g class="st48">
<path d="M1414.38-19.24c-0.1-0.21-0.34-0.31-0.56-0.22l-115.01,83.52c-0.23,0.09-0.34,0.34-0.25,0.57
c0.07,0.22,0.34,0.34,0.57,0.25l115.01-83.52c0.23-0.09,0.34-0.34,0.25-0.57C1414.38-19.22,1414.38-19.23,1414.38-19.24z"/>
</g>
<g class="st48">
<path d="M1315.89,430.57c-0.08-0.16-0.25-0.27-0.44-0.25l-92.81-17.52c-0.24,0.02-0.42,0.24-0.39,0.48
c0.02,0.24,0.24,0.42,0.48,0.39l92.81,17.52c0.24-0.02,0.42-0.24,0.4-0.48C1315.93,430.67,1315.91,430.62,1315.89,430.57z"/>
</g>
<g class="st48">
<path d="M1223.86,207.31c-0.06-0.12-0.17-0.22-0.31-0.25l-79.15,5c-0.24-0.05-0.47,0.11-0.52,0.35
c-0.04,0.24,0.11,0.47,0.35,0.52l79.15-5c0.24,0.05,0.47-0.11,0.52-0.35C1223.91,207.49,1223.9,207.39,1223.86,207.31z"/>
</g>
<g class="st48">
<path d="M1412.78,202.53c-0.04-0.09-0.11-0.16-0.2-0.21l-73.54-42.73c-0.22-0.11-0.48-0.02-0.59,0.2
c-0.11,0.22-0.02,0.48,0.2,0.59l73.54,42.73c0.22,0.11,0.48,0.02,0.59-0.2C1412.83,202.79,1412.83,202.65,1412.78,202.53z"/>
</g>
<g class="st48">
<path d="M1221.49,290.56c-0.01-0.01-0.01-0.03-0.02-0.04l2.37-83.25c-0.12-0.21-0.39-0.28-0.6-0.15
c-0.21,0.12-0.27,0.41-0.16,0.6l-2.37,83.25c0.12,0.21,0.39,0.28,0.6,0.16C1221.51,291.01,1221.58,290.76,1221.49,290.56z"/>
</g>
<g class="st48">
<path d="M1570.13,127.23c-0.07-0.14-0.21-0.24-0.38-0.25L1414-19.49c-0.24-0.01-0.45,0.17-0.46,0.42
c-0.01,0.24,0.17,0.45,0.42,0.46l155.75,146.46c0.24,0.01,0.45-0.17,0.46-0.42C1570.17,127.36,1570.16,127.29,1570.13,127.23z"
/>
</g>
<g class="st48">
<path d="M1422.95,366.77c-0.06-0.12-0.17-0.21-0.3-0.24l-107.06,63.81c-0.24-0.05-0.47,0.1-0.52,0.34
c-0.05,0.24,0.1,0.47,0.34,0.52l107.06-63.81c0.24,0.05,0.47-0.1,0.52-0.34C1423.01,366.95,1422.99,366.85,1422.95,366.77z"/>
</g>
<g class="st48">
<path d="M1495.96,150.44c-0.01-0.02-0.02-0.04-0.03-0.06L1414.34-19.3c-0.14-0.2-0.41-0.25-0.61-0.12
c-0.2,0.14-0.25,0.41-0.12,0.61l81.58,169.68c0.14,0.2,0.41,0.25,0.61,0.12C1495.99,150.87,1496.05,150.63,1495.96,150.44z"/>
</g>
<g class="st48">
<path d="M1319.18,193.3c-0.08-0.17-0.27-0.28-0.46-0.25l-95.33,14.01c-0.24,0.03-0.41,0.26-0.37,0.5
c0.03,0.24,0.26,0.41,0.5,0.37l95.32-14.01c0.24-0.04,0.41-0.26,0.37-0.5C1319.21,193.38,1319.2,193.34,1319.18,193.3z"/>
</g>
<g class="st48">
<path d="M1262.86,318.75c-0.01-0.02-0.02-0.05-0.04-0.07l-41.38-28.19c-0.14-0.2-0.41-0.25-0.61-0.1
c-0.2,0.14-0.24,0.41-0.1,0.61l41.38,28.19c0.14,0.2,0.41,0.25,0.61,0.11C1262.9,319.17,1262.95,318.94,1262.86,318.75z"/>
</g>
<g class="st48">
<path d="M1398.52,271.35c-0.02-0.04-0.04-0.07-0.07-0.11c-0.16-0.18-0.44-0.2-0.62-0.03l-177.04,19.2
c-0.18,0.16-0.2,0.44-0.03,0.62c0.16,0.18,0.44,0.2,0.62,0.03l177.04-19.2C1398.57,271.74,1398.6,271.53,1398.52,271.35z"/>
</g>
<g class="st48">
<path d="M1331.87,304.53c-0.04-0.08-0.11-0.16-0.19-0.2l-110.39-13.97c-0.22-0.11-0.48-0.03-0.59,0.19
c-0.11,0.22-0.03,0.48,0.18,0.59l110.39,13.97c0.22,0.11,0.48,0.03,0.59-0.18C1331.93,304.79,1331.93,304.65,1331.87,304.53z"
/>
</g>
<g class="st48">
<path d="M1398.52,271.36c-0.01-0.02-0.02-0.04-0.04-0.06l-79.34-78.05c-0.14-0.2-0.41-0.25-0.61-0.11
c-0.2,0.14-0.25,0.42-0.11,0.61l79.34,78.05c0.14,0.2,0.41,0.25,0.61,0.11C1398.55,271.78,1398.61,271.55,1398.52,271.36z"/>
</g>
<g class="st48">
<path d="M1570.13,127.23c-0.02-0.03-0.04-0.07-0.06-0.09c-0.16-0.19-0.43-0.21-0.62-0.06l-74.17,23.21
c-0.19,0.15-0.21,0.44-0.06,0.62c0.16,0.19,0.43,0.21,0.62,0.06l74.17-23.21C1570.17,127.62,1570.21,127.4,1570.13,127.23z"/>
</g>
<g class="st48">
<path d="M1331.87,304.53c-0.1-0.22-0.36-0.31-0.58-0.21l-69.01,14.22c-0.22,0.1-0.32,0.36-0.21,0.58l0,0
c0.1,0.22,0.36,0.31,0.58,0.21l69.01-14.22C1331.88,305.01,1331.99,304.74,1331.87,304.53z"/>
</g>
<g class="st48">
<path d="M1412.78,202.53c-0.09-0.18-0.29-0.29-0.5-0.24l-93.59-9.23c-0.24,0.06-0.38,0.3-0.32,0.53
c0.06,0.24,0.3,0.38,0.53,0.32l93.59,9.23c0.24-0.06,0.38-0.3,0.32-0.53C1412.8,202.59,1412.79,202.56,1412.78,202.53z"/>
</g>
<g class="st48">
<path d="M1412.78,202.53c-0.01-0.02-0.02-0.04-0.03-0.06c-0.13-0.2-0.41-0.26-0.61-0.12l-14.25,68.82
c-0.2,0.14-0.25,0.41-0.12,0.61c0.13,0.2,0.41,0.26,0.61,0.12l14.25-68.82C1412.8,202.96,1412.87,202.73,1412.78,202.53z"/>
</g>
<g class="st48">
<path d="M1495.96,150.44c-0.04-0.08-0.1-0.15-0.18-0.19c-0.21-0.12-0.48-0.05-0.6,0.16L1412,202.5
c-0.12,0.21-0.05,0.48,0.16,0.6c0.21,0.12,0.48,0.05,0.6-0.16l83.18-52.1C1496.02,150.72,1496.02,150.56,1495.96,150.44z"/>
</g>
<path d="M1129.08,173.41c-0.63-0.68-1.69-0.73-2.37-0.1c-0.68,0.63-0.73,1.69-0.1,2.37c0.63,0.68,1.69,0.73,2.37,0.1
C1129.67,175.16,1129.71,174.1,1129.08,173.41z"/>
<path d="M1151.09,93.86c-0.63-0.68-1.69-0.73-2.37-0.1c-0.68,0.63-0.73,1.69-0.1,2.37c0.63,0.68,1.69,0.73,2.37,0.1
C1151.67,95.6,1151.72,94.54,1151.09,93.86z"/>
<path d="M1168.78,321.58c-0.63-0.68-1.69-0.73-2.37-0.1c-0.68,0.63-0.73,1.69-0.1,2.37c0.63,0.68,1.69,0.73,2.37,0.1
C1169.36,323.32,1169.41,322.26,1168.78,321.58z"/>
<path d="M1162.47,191.12c-0.63-0.68-1.69-0.73-2.37-0.1c-0.68,0.63-0.73,1.69-0.1,2.37c0.63,0.68,1.69,0.73,2.37,0.1
S1163.1,191.8,1162.47,191.12z"/>
<path d="M1251.08,30.95c-0.63-0.68-1.69-0.73-2.37-0.1c-0.68,0.63-0.73,1.69-0.1,2.37c0.63,0.68,1.69,0.73,2.37,0.1
C1251.66,32.69,1251.71,31.63,1251.08,30.95z"/>
<path d="M1186.93,265.2c-0.63-0.68-1.69-0.73-2.37-0.1c-0.68,0.63-0.73,1.69-0.1,2.37c0.63,0.68,1.69,0.73,2.37,0.1
C1187.51,266.95,1187.56,265.88,1186.93,265.2z"/>
<path d="M1198.26,361.44c-0.63-0.68-1.69-0.73-2.37-0.1c-0.68,0.63-0.73,1.69-0.1,2.37c0.63,0.68,1.69,0.73,2.37,0.1
C1198.84,363.19,1198.89,362.12,1198.26,361.44z"/>
<path d="M1303.25,174.3c-0.63-0.68-1.69-0.73-2.37-0.1c-0.68,0.63-0.73,1.69-0.1,2.37c0.63,0.68,1.69,0.73,2.37,0.1
C1303.83,176.04,1303.88,174.98,1303.25,174.3z"/>
<path d="M1170.77,228.82c-0.63-0.68-1.69-0.73-2.37-0.1c-0.68,0.63-0.73,1.69-0.1,2.37c0.63,0.68,1.69,0.73,2.37,0.1
C1171.35,230.57,1171.4,229.51,1170.77,228.82z"/>
<path d="M1146.69,230.27c-0.63-0.68-1.69-0.73-2.37-0.1s-0.73,1.69-0.1,2.37c0.63,0.68,1.69,0.73,2.37,0.1
C1147.27,232.01,1147.32,230.95,1146.69,230.27z"/>
<path d="M1210.31,164.92c-0.63-0.68-1.69-0.73-2.37-0.1s-0.73,1.69-0.1,2.37c0.63,0.68,1.69,0.73,2.37,0.1
C1210.9,166.67,1210.94,165.61,1210.31,164.92z"/>
<path d="M1222.67,340.23c-0.63-0.68-1.69-0.73-2.37-0.1c-0.68,0.63-0.73,1.69-0.1,2.37c0.63,0.68,1.69,0.73,2.37,0.1
C1223.25,341.98,1223.3,340.91,1222.67,340.23z"/>
<path d="M1300.21,63.33c-0.63-0.68-1.69-0.73-2.37-0.1c-0.68,0.63-0.73,1.69-0.1,2.37c0.63,0.68,1.69,0.73,2.37,0.1
C1300.79,65.08,1300.84,64.02,1300.21,63.33z"/>
<path d="M1340.08,158.85c-0.63-0.68-1.69-0.73-2.37-0.1c-0.68,0.63-0.73,1.69-0.1,2.37c0.63,0.68,1.69,0.73,2.37,0.1
C1340.66,160.6,1340.7,159.54,1340.08,158.85z"/>
<path d="M1316.73,429.63c-0.63-0.68-1.69-0.73-2.37-0.1c-0.68,0.63-0.73,1.69-0.1,2.37c0.63,0.68,1.69,0.73,2.37,0.1
C1317.31,431.37,1317.36,430.31,1316.73,429.63z"/>
<path d="M1223.92,412.11c-0.63-0.68-1.69-0.73-2.37-0.1c-0.68,0.63-0.73,1.69-0.1,2.37c0.63,0.68,1.69,0.73,2.37,0.1
C1224.5,413.86,1224.55,412.79,1223.92,412.11z"/>
<path d="M1415.22-20.19c-0.63-0.68-1.69-0.73-2.37-0.1c-0.68,0.63-0.73,1.69-0.1,2.37c0.63,0.68,1.69,0.73,2.37,0.1
C1415.8-18.44,1415.84-19.5,1415.22-20.19z"/>
<path d="M1224.7,206.36c-0.63-0.68-1.69-0.73-2.37-0.1c-0.68,0.63-0.73,1.69-0.1,2.37c0.63,0.68,1.69,0.73,2.37,0.1
C1225.28,208.11,1225.32,207.05,1224.7,206.36z"/>
<path d="M1145.54,211.36c-0.63-0.68-1.69-0.73-2.37-0.1c-0.68,0.63-0.73,1.69-0.1,2.37c0.63,0.68,1.69,0.73,2.37,0.1
C1146.12,213.11,1146.17,212.04,1145.54,211.36z"/>
<path d="M1222.33,289.61c-0.63-0.68-1.69-0.73-2.37-0.1c-0.68,0.63-0.73,1.69-0.1,2.37c0.63,0.68,1.69,0.73,2.37,0.1
C1222.91,291.36,1222.95,290.29,1222.33,289.61z"/>
<path d="M1263.7,317.8c-0.63-0.68-1.69-0.73-2.37-0.1c-0.68,0.63-0.73,1.69-0.1,2.37c0.63,0.68,1.69,0.73,2.37,0.1
C1264.29,319.55,1264.33,318.49,1263.7,317.8z"/>
<path d="M1570.97,126.28c-0.63-0.68-1.69-0.73-2.37-0.1c-0.68,0.63-0.73,1.69-0.1,2.37c0.63,0.68,1.69,0.73,2.37,0.1
S1571.59,126.96,1570.97,126.28z"/>
<path d="M1320.02,192.36c-0.63-0.68-1.69-0.73-2.37-0.1c-0.68,0.63-0.73,1.69-0.1,2.37c0.63,0.68,1.69,0.73,2.37,0.1
C1320.6,194.1,1320.65,193.04,1320.02,192.36z"/>
<path d="M1399.36,270.41c-0.63-0.68-1.69-0.73-2.37-0.1c-0.68,0.63-0.73,1.69-0.1,2.37c0.63,0.68,1.69,0.73,2.37,0.1
C1399.94,272.15,1399.99,271.09,1399.36,270.41z"/>
<path d="M1496.8,149.49c-0.63-0.68-1.69-0.73-2.37-0.1c-0.68,0.63-0.73,1.69-0.1,2.37c0.63,0.68,1.69,0.73,2.37,0.1
C1497.38,151.24,1497.43,150.17,1496.8,149.49z"/>
<path d="M1413.62,201.58c-0.63-0.68-1.69-0.73-2.37-0.1c-0.68,0.63-0.73,1.69-0.1,2.37c0.63,0.68,1.69,0.73,2.37,0.1
C1414.2,203.33,1414.24,202.27,1413.62,201.58z"/>
<path d="M1332.71,303.58c-0.63-0.68-1.69-0.73-2.37-0.1c-0.68,0.63-0.73,1.69-0.1,2.37c0.63,0.68,1.69,0.73,2.37,0.1
C1333.29,305.33,1333.34,304.26,1332.71,303.58z"/>
<path d="M1423.79,365.82c-0.63-0.68-1.69-0.73-2.37-0.1c-0.68,0.63-0.73,1.69-0.1,2.37c0.63,0.68,1.69,0.73,2.37,0.1
C1424.37,367.57,1424.42,366.5,1423.79,365.82z"/>
</g>
<g class="st48">
<polygon points="1414.78,-20.04 1414.13,-19.84 1249.73,31.61 1249.68,31.91 1222.53,207.39 1223.51,207.55 1250.61,32.37
1413.78,-18.69 1412.2,202.41 1413.19,202.42 "/>
</g>
<g class="st49">
<polygon points="1221.71,291.41 1221.21,290.56 1167.73,322.24 1168.23,323.09 "/>
</g>
<g class="st49">
<rect x="1218.83" y="322.32" transform="matrix(0.9474 -0.3201 0.3201 0.9474 -36.1558 425.678)" width="115.78" height="0.99"/>
</g>
<g class="st50">
<path d="M1398.26,271.62l-212.96-5.01l-0.32-0.01l-16.18-37.74l229.56,41.77L1398.26,271.62z M1185.64,265.63l206.33,4.85
l-221.52-40.3L1185.64,265.63z"/>
</g>
<g class="st51">
<polygon points="1414.57,-18.97 1413.99,-19.77 1298.65,63.55 1250.44,31.57 1249.9,32.4 1298.68,64.75 1298.95,64.55 "/>
</g>
<polygon class="st52" points="1414.38,-19.24 1298.53,64.48 1249.46,31.86 "/>
<polygon class="st53" points="1197.43,362.41 1185.64,265.63 1167.59,323.15 "/>
<polygon class="st48" points="1315.59,430.33 1331.22,304.36 1422.99,367.01 "/>
<polygon class="st48" points="1412.78,202.53 1318.35,193.45 1339.23,159.8 "/>
<polygon class="st52" points="1127.45,174.75 1161.23,192.25 1149.41,94.96 "/>
<polyline class="st52" points="1149.41,94.96 1127.45,174.75 1161.23,192.25 "/>
<polyline class="st52" points="1149.41,94.96 1127.45,174.75 1161.23,192.25 "/>
</g>
<path class="st54" d="M1316.63,432c0.51-0.47,0.67-1.19,0.45-1.81l104.23-62.12c0.01,0.01,0.01,0.01,0.01,0.02
c0.63,0.68,1.69,0.73,2.37,0.1c0.68-0.63,0.73-1.69,0.1-2.37c-0.62-0.68-1.66-0.72-2.34-0.12l-88.41-60.42
c0.12-0.33,0.13-0.68,0.04-1.01l63.73-31.72c0.03,0.04,0.04,0.09,0.08,0.13c0.63,0.68,1.69,0.73,2.37,0.1
c0.63-0.58,0.69-1.52,0.21-2.2l80.31-99.67l88.66-42.43c0.02,0.02,0.03,0.05,0.05,0.08c0.63,0.68,1.69,0.73,2.37,0.1
c0.68-0.63,0.73-1.69,0.1-2.37c-0.58-0.63-1.51-0.69-2.19-0.22L1415.39-18.19c0.38-0.62,0.35-1.43-0.17-2
c-0.63-0.68-1.69-0.73-2.37-0.1c-0.3,0.27-0.45,0.64-0.51,1.01L1251.2,31.14c-0.04-0.06-0.07-0.14-0.12-0.2
c-0.63-0.68-1.69-0.73-2.37-0.1c-0.5,0.46-0.65,1.16-0.45,1.77L1151,93.79c-0.63-0.61-1.63-0.64-2.29-0.04
c-0.68,0.63-0.73,1.69-0.1,2.37c0.12,0.13,0.26,0.21,0.4,0.29l-11.51,41.82l-9.58,34.63c-0.43-0.02-0.87,0.13-1.21,0.44
c-0.68,0.63-0.73,1.69-0.1,2.37c0.63,0.68,1.69,0.73,2.37,0.1c0.05-0.05,0.08-0.11,0.12-0.17l30.54,16.19
c-0.15,0.54-0.05,1.13,0.35,1.57c0.35,0.39,0.84,0.55,1.33,0.52l5.07,15.37l-0.41,0.42l-20.13,2.19
c-0.07-0.19-0.17-0.37-0.32-0.53c-0.63-0.68-1.69-0.73-2.37-0.1c-0.68,0.63-0.73,1.69-0.1,2.37c0.54,0.58,1.38,0.67,2.04,0.31
l8.41,8.57l-7.25,7.45c-0.61-0.34-1.4-0.29-1.94,0.21c-0.68,0.63-0.73,1.69-0.1,2.37c0.57,0.62,1.47,0.69,2.15,0.24l37.82,32.83
c-0.29,0.6-0.21,1.34,0.27,1.86c0.11,0.12,0.24,0.19,0.37,0.27l-17.16,53.31c-0.44-0.03-0.9,0.11-1.25,0.43
c-0.68,0.63-0.73,1.69-0.1,2.37c0.51,0.56,1.3,0.67,1.95,0.36l27.54,37.25c-0.58,0.64-0.61,1.61-0.02,2.26
c0.63,0.68,1.69,0.73,2.37,0.1c0.6-0.55,0.68-1.43,0.27-2.1l21.96-19.08c0.28,0.23,0.61,0.38,0.96,0.4l0.89,68.61
c-0.25,0.07-0.49,0.18-0.69,0.36c-0.68,0.63-0.73,1.69-0.1,2.37c0.63,0.68,1.69,0.73,2.37,0.1c0.17-0.15,0.29-0.33,0.38-0.52
l89.64,16.92c0.03,0.36,0.15,0.73,0.41,1.02C1314.88,432.58,1315.95,432.63,1316.63,432z M1147.12,231.41l14.29-0.86l14.4,14.67
l8.16,19.03l-37.01-32.13C1147.07,231.9,1147.12,231.66,1147.12,231.41z M1330.87,303.16c-0.1,0.04-0.19,0.11-0.28,0.17
l-23.9-21.51l21.48-2.33L1330.87,303.16z M1413.54,201.53c-0.11-0.1-0.22-0.18-0.35-0.25l0.37-51.41l76-10.72l4.91,10.21
c-0.01,0.01-0.03,0.01-0.04,0.03c-0.51,0.46-0.66,1.16-0.45,1.77L1413.54,201.53z M1567.47,127.31l-77.39,10.92l-15.65-32.55
L1567.47,127.31z M1489.18,138.35l-75.61,10.67l0.41-57.4l59.36,13.8L1489.18,138.35z M1222.8,342.29
c0.17-0.26,0.3-0.54,0.31-0.84l25.8-8.72l4.47,6.63l-9.63,22.84L1222.8,342.29z M1329.99,303.96c-0.02,0.05-0.04,0.09-0.06,0.14
l-57.08-7.22l5.38-11.97l27.3-2.96L1329.99,303.96z M1251.08,33.18l46.33,30.73c-0.17,0.48-0.11,1.01,0.18,1.46l-24.99,28.24
l-21.88-60.13c0.09-0.05,0.19-0.09,0.27-0.16C1251.02,33.28,1251.04,33.23,1251.08,33.18z M1292.92,396.55l-21.27-31.55l8.3-8.28
l18.3,38.59L1292.92,396.55z M1298.61,396.09l14.24,30.02l-19.43-28.82L1298.61,396.09z M1324.11,243.95l-23.62-8.66l18.03-40.17
c0,0,0.01,0,0.01,0L1324.11,243.95z M1224.02,204.25l8.42-0.92l-7.66,3.16c-0.03-0.04-0.04-0.09-0.08-0.13
c-0.26-0.28-0.59-0.42-0.93-0.49L1224.02,204.25z M1337.82,158.68c-0.04,0.03-0.09,0.04-0.12,0.07c-0.27,0.25-0.41,0.56-0.48,0.89
l-23.03,1.08l-14.25-92.82L1337.82,158.68z M1236.42,202.9l59.09-6.44l-67.58,9.93L1236.42,202.9z M1282.73,228.78l-57.53-21.09
l92.06-13.53c0.02,0.05,0.06,0.1,0.09,0.15L1282.73,228.78z M1314.79,170.36l3.32,21.62c-0.16,0.07-0.32,0.15-0.46,0.28
c-0.29,0.27-0.44,0.62-0.5,0.98l-77.82,8.48l61.37-25.26c0.03,0.04,0.04,0.09,0.07,0.12c0.63,0.68,1.69,0.73,2.37,0.1
c0.49-0.45,0.64-1.12,0.46-1.71L1314.79,170.36z M1315.62,170.01l21.9-9.02c0.03,0.04,0.04,0.09,0.08,0.13
c0.02,0.02,0.05,0.03,0.07,0.05l-18.4,30.74c-0.1-0.03-0.19-0.06-0.3-0.07L1315.62,170.01z M1315.49,169.14l-1.16-7.58l22.09-1.03
L1315.49,169.14z M1317.43,195.45l-17.75,39.55l-16.06-5.89L1317.43,195.45z M1224.16,203.36l5.4-34.88l70.31,7.09l-64.51,26.57
L1224.16,203.36z M1302.31,173.81l-0.32-11.68l11.46-0.54l1.21,7.88l-11.51,4.74C1302.9,174,1302.61,173.86,1302.31,173.81z
M1301.96,161.29l-2.49-90.64l13.84,90.11L1301.96,161.29z M1371.57,80.93l-70.94-16.49c0-0.2-0.03-0.39-0.1-0.57l112.41-81.63
c0.01,0.01,0.03,0.02,0.04,0.03L1371.57,80.93z M1371.25,81.69l-19.49,46.44l-51.44-62.71c0.04-0.06,0.1-0.12,0.14-0.19
L1371.25,81.69z M1351.38,129.05l-12.3,29.3c-0.15-0.02-0.3-0.02-0.46,0l-38.12-91.33L1351.38,129.05z M1351.97,129.77
l20.54,25.04l-32.14,4.53c-0.07-0.17-0.16-0.35-0.3-0.49c-0.07-0.08-0.16-0.12-0.24-0.18L1351.97,129.77z M1373.14,155.57
l37.36,45.55l-70.07-40.72c0.02-0.08,0.05-0.15,0.05-0.23L1373.14,155.57z M1277.22,285.02l-5.28,11.75l-47.56-6.02
L1277.22,285.02z M1271.59,297.55l-8.87,19.75c-0.48-0.07-0.98,0.04-1.37,0.38l-38.62-26.32L1271.59,297.55z M1280.15,355.3
l-13.54-28.55l62.44-21.1l0.95-0.2c0.01,0.03,0.03,0.05,0.05,0.08L1280.15,355.3z M1266.19,325.85l-2.67-5.62
c0.03-0.02,0.06-0.03,0.08-0.05c0.34-0.31,0.52-0.72,0.54-1.15l56.99-11.75L1266.19,325.85z M1265.42,326.11l-6.42,2.17l3.25-7.69
c0.18,0.02,0.36,0.01,0.54-0.02L1265.42,326.11z M1272.49,297.67l56.09,7.1l-0.83,0.28l-63.81,13.15
c-0.07-0.14-0.13-0.28-0.24-0.39c-0.05-0.06-0.13-0.09-0.19-0.14L1272.49,297.67z M1330.84,306.26l-10.65,83.98l-21.15,4.9
l-18.5-39l50.1-49.98C1330.71,306.19,1330.77,306.23,1330.84,306.26z M1271.16,364.28l-16.82-24.95l4.15-9.84l7.36-2.49l13.7,28.9
L1271.16,364.28z M1253.75,338.47l-4.05-6l7.68-2.59L1253.75,338.47z M1249.13,331.61l-26.52-39.33l38.29,26.09
c-0.2,0.57-0.11,1.22,0.32,1.7c0.06,0.07,0.15,0.1,0.22,0.16l-3.55,8.42L1249.13,331.61z M1220.29,289.31l-21.96-22.38l23.05,0.54
l-0.62,21.66C1220.59,289.16,1220.44,289.22,1220.29,289.31z M1197.33,265.91l-8.97-9.14l5.22-22.38l28.6,5.2l-0.77,26.88
L1197.33,265.91z M1171.14,230.31l2.34,0.43l5.41,16.39l-2.12-2.16l-5.98-13.94C1170.97,230.81,1171.09,230.57,1171.14,230.31z
M1174.45,230.91l18.29,3.33l-5.09,21.81l-7.39-7.53L1174.45,230.91z M1185.6,264.68l-4.72-14.28l6.54,6.66l-1.78,7.62
C1185.63,264.68,1185.62,264.68,1185.6,264.68z M1196.08,265.88l-8.86-0.21c-0.07-0.17-0.15-0.33-0.28-0.47
c-0.13-0.15-0.3-0.25-0.46-0.34l1.65-7.09L1196.08,265.88z M1179.47,251.24l-1.74-4.07l1.79,1.83l3.02,9.15L1179.47,251.24z
M1171.93,229.44l0.99-0.41l0.21,0.62L1171.93,229.44z M1173.72,228.71l22.47-9.25l-3.22,13.81l-18.87-3.43L1173.72,228.71z
M1197.15,219.07l25.17-10.36c0.21,0.2,0.46,0.32,0.72,0.39l-0.84,29.5l-28.4-5.17L1197.15,219.07z M1197.39,218.02l2-8.57
l22.24-1.4L1197.39,218.02z M1199.59,208.58l0.4-1.71l23-2.51l-0.25,1.64c-0.15,0.07-0.29,0.14-0.42,0.25
c-0.27,0.25-0.42,0.58-0.48,0.91L1199.59,208.58z M1200.2,205.97l8.92-38.25c0.39-0.01,0.78-0.14,1.09-0.42
c0.22-0.2,0.35-0.46,0.44-0.72l17.92,1.81l-5.43,35.09L1200.2,205.97z M1214.34,166.23l14.67-0.69l-0.33,2.13L1214.34,166.23z
M1230.01,165.5l66.88-3.13l4.25,11.66c-0.09,0.05-0.19,0.09-0.27,0.16c-0.22,0.2-0.35,0.46-0.44,0.72l-70.78-7.14L1230.01,165.5z
M1297.61,162.34l3.7-0.17l0.31,11.19L1297.61,162.34z M1297.31,161.51l-24.37-66.96l25.29-28.58c0.14,0.07,0.29,0.1,0.45,0.13
l2.61,95.23L1297.31,161.51z M1413.77-17.4L1413,90.56l-40.62-9.44l41.35-98.53C1413.75-17.41,1413.76-17.41,1413.77-17.4z
M1412.99,91.39l-0.41,57.77l-39.05,5.51l-21.18-25.82l19.71-46.97L1412.99,91.39z M1412.57,150.01l-0.37,51.07
c-0.18,0.02-0.34,0.05-0.51,0.13l-37.55-45.77L1412.57,150.01z M1319.61,194.91l76.58,75.33l-0.73-0.13l-70.45-25.83l-5.62-49.23
C1319.47,195.02,1319.54,194.96,1319.61,194.91z M1320.08,391.11l-4.82,38.01c-0.03,0-0.06,0.01-0.09,0.01l-15.76-33.24
L1320.08,391.11z M1313.43,428.43l-30.27-28.77l9.4-2.18L1313.43,428.43z M1282.45,398.98l-22.97-21.83l11.59-11.56l21.01,31.16
L1282.45,398.98z M1281.82,399.13l-57.67,13.36c-0.01-0.02-0.02-0.04-0.03-0.05l34.98-34.9L1281.82,399.13z M1223.95,411.37
l20.25-48l14.27,13.56L1223.95,411.37z M1244.42,362.84l9.54-22.62l16.61,24.64l-11.72,11.69L1244.42,362.84z M1221.54,292.35
c0.04-0.01,0.08-0.01,0.13-0.02l26.67,39.56l-25.5,8.62c-0.06-0.09-0.09-0.19-0.16-0.27c-0.15-0.16-0.33-0.29-0.52-0.38
L1221.54,292.35z M1191.07,308.41l-4.77-40.52c0.19-0.07,0.37-0.17,0.53-0.32c0.27-0.25,0.41-0.57,0.48-0.91l9.76,0.23
l22.58,23.02c-0.29,0.49-0.3,1.08-0.04,1.59L1191.07,308.41z M1172.09,226.52l0.56,1.69l-1.8,0.74c-0.03-0.04-0.04-0.09-0.07-0.12
c-0.02-0.02-0.05-0.03-0.07-0.05L1172.09,226.52z M1172.68,225.56l9.29-15.02l16.54-1.04l-2.08,8.91l-22.99,9.46L1172.68,225.56z
M1172.39,224.66l-4.37-13.23l13.07-0.82L1172.39,224.66z M1167.74,210.59l-0.07-0.2l14.52-1.58l-0.56,0.9L1167.74,210.59z
M1183.11,208.71l15.99-1.74l-0.39,1.67l-16.19,1.02L1183.11,208.71z M1183.69,207.76l24.43-39.47l-8.81,37.77L1183.69,207.76z
M1233.01,139.67l-3.87,25.03l-18.46,0.86c-0.05-0.15-0.13-0.27-0.21-0.41L1233.01,139.67z M1234.22,138.3l38.2-43.17l24.17,66.42
l-66.45,3.11L1234.22,138.3z M1234.47,136.7l15.84-102.36l21.78,59.85L1234.47,136.7z M1414.75-16.43l58.14,120.92l-58.91-13.69
L1414.75-16.43z M1358.48,197.82l37.99,71.49l-76.25-75.01c0.05-0.08,0.08-0.16,0.11-0.24L1358.48,197.82z M1313.98,430.05
l-89.26-16.85l57.82-13.39l31.59,30.03C1314.08,429.91,1314.01,429.97,1313.98,430.05z M1223.09,411.17l-0.89-68.34
c0.08-0.04,0.16-0.09,0.24-0.14l21.09,20.04L1223.09,411.17z M1220.03,342.22l-21.96,19.08c-0.24-0.2-0.52-0.32-0.82-0.37
l-6.06-51.45l29.13-17.26c0.11,0.06,0.23,0.08,0.35,0.12l0.62,47.37c-0.36,0.03-0.72,0.15-1,0.41
C1219.69,340.68,1219.62,341.55,1220.03,342.22z M1196.31,361.08l-4.05-5.48l4.13,5.43
C1196.36,361.05,1196.33,361.07,1196.31,361.08z M1162.57,230.48l5.33-0.32c0.04,0.33,0.15,0.66,0.4,0.93
c0.39,0.43,0.95,0.58,1.48,0.5l5.07,11.41L1162.57,230.48z M1162.37,230.28l-7.64-7.78l10.61-10.9l1.79-0.11l4.67,14.13
l-1.72,2.78c-0.56-0.19-1.21-0.1-1.68,0.33c-0.36,0.33-0.53,0.78-0.53,1.23L1162.37,230.28z M1166.23,210.69l0.15-0.15l0.42-0.05
l0.05,0.16L1166.23,210.69z M1167.38,209.5l39.47-40.54l-24.08,38.91l-15.38,1.68L1167.38,209.5z M1162.44,191.09
c-0.27-0.28-0.61-0.43-0.96-0.48l-5.64-48.17l93.24-108.88c0.11,0.06,0.22,0.08,0.33,0.11l-16.15,104.4l-23.43,26.48
c-0.6-0.3-1.35-0.22-1.88,0.26c-0.46,0.43-0.61,1.05-0.48,1.62L1162.44,191.09z M1247.89,33.78l-92.17,107.64l-5.25-44.88
c0.19-0.07,0.36-0.17,0.52-0.31c0.5-0.46,0.65-1.16,0.45-1.77L1247.89,33.78z M1415.26-17.38l153.06,143.93
c-0.02,0.04-0.05,0.06-0.07,0.1l-94.27-21.92L1415.26-17.38z M1564.78,128.54l-67.81,21.22c-0.06-0.09-0.09-0.19-0.16-0.27
c-0.4-0.44-0.99-0.59-1.54-0.49l-4.8-9.98L1564.78,128.54z M1479.11,170.34l-60.63,29.02l75.2-47.09L1479.11,170.34z
M1411.74,204.26l-13.59,65.61c-0.14,0-0.28,0.03-0.42,0.06l-38.27-72.01l51.29,5.06c0.05,0.32,0.16,0.62,0.39,0.88
C1411.31,204.04,1411.52,204.17,1411.74,204.26z M1396.3,272.1l-63.51,31.61c-0.03-0.04-0.04-0.09-0.08-0.13
c-0.27-0.3-0.63-0.45-1-0.5l-2.7-23.68L1396.3,272.1z M1154.12,223.12l7.09,7.23l-14.12,0.85c-0.03-0.22-0.1-0.43-0.22-0.63
L1154.12,223.12z M1154.12,221.88l-8.39-8.55c0.09-0.16,0.15-0.33,0.19-0.51l18.13-1.15L1154.12,221.88z M1165.03,210.68
l-0.08,0.09l-1.7,0.11L1165.03,210.68z M1167.07,208.58l-4.94-14.95c0.07-0.05,0.16-0.07,0.23-0.14c0.46-0.43,0.61-1.05,0.47-1.62
l43.9-24.04L1167.07,208.58z M1480.94,169.46l13.94-17.3c0.59,0.26,1.3,0.17,1.81-0.3c0.39-0.35,0.55-0.85,0.52-1.33l67.27-21.05
L1480.94,169.46z M1477.95,171.78l-79.15,98.22c-0.01-0.01-0.02,0-0.04-0.01l13.59-65.61c0.41,0.01,0.83-0.13,1.16-0.43
c0.45-0.41,0.6-1.01,0.48-1.57L1477.95,171.78z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

File diff suppressed because it is too large Load Diff
+437
View File
@@ -0,0 +1,437 @@
# hindsight-litellm
Universal LLM memory integration via LiteLLM. Add persistent memory to any LLM application with just a few lines of code.
## Features
- **Universal LLM Support** - Works with 100+ LLM providers via LiteLLM (OpenAI, Anthropic, Groq, Azure, AWS Bedrock, Google Vertex AI, and more)
- **Simple Integration** - Just configure, enable, and use `hindsight_litellm.completion()`
- **Automatic Memory Injection** - Relevant memories are injected into prompts before LLM calls
- **Automatic Conversation Storage** - Conversations are stored to Hindsight for future recall
- **Two Memory Modes** - Choose between `reflect` (synthesized context) or `recall` (raw memory retrieval)
- **Direct Memory APIs** - Query, synthesize, and store memories manually
- **Native Client Wrappers** - Alternative wrappers for OpenAI and Anthropic SDKs
- **Debug Mode** - Inspect exactly what memories are being injected
## Installation
```bash
pip install hindsight-litellm
```
## Quick Start
```python
import hindsight_litellm
# Configure and enable memory integration
hindsight_litellm.configure(
hindsight_api_url="http://localhost:8888",
bank_id="my-agent",
)
hindsight_litellm.enable()
# Use the convenience wrapper - memory is automatically injected and stored
response = hindsight_litellm.completion(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "What did we discuss about AI?"}]
)
```
## How It Works
Here's what happens under the hood when you call `completion()`:
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ 1. YOUR CODE │
│ ───────────────────────────────────────────────────────────────────────── │
│ response = hindsight_litellm.completion( │
│ model="gpt-4o-mini", │
│ messages=[{"role": "user", "content": "Help me with my Python project"}]│
│ ) │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ 2. MEMORY RETRIEVAL (before LLM call) │
│ ───────────────────────────────────────────────────────────────────────── │
│ # hindsight_litellm queries Hindsight for relevant memories │
│ │
│ # If use_reflect=False (default) - raw memories: │
│ memories = hindsight.recall(query="Help me with my Python project") │
│ # Returns: ["User prefers pytest", "User is building a FastAPI app", ...] │
│ │
│ # If use_reflect=True - synthesized context: │
│ context = hindsight.reflect(query="Help me with my Python project") │
│ # Returns: "The user is an experienced Python developer working on..." │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ 3. PROMPT INJECTION │
│ ───────────────────────────────────────────────────────────────────────── │
│ # Memories are injected into the system message: │
│ │
│ messages = [ │
│ {"role": "system", "content": """ │
│ # Relevant Memories │
│ 1. [WORLD] User prefers pytest for testing │
│ 2. [WORLD] User is building a FastAPI app │
│ 3. [OPINION] User likes type hints │
│ """}, │
│ {"role": "user", "content": "Help me with my Python project"} │
│ ] │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ 4. LLM CALL │
│ ───────────────────────────────────────────────────────────────────────── │
│ # The enriched prompt is sent to the LLM │
│ response = litellm.completion(model="gpt-4o-mini", messages=messages) │
│ │
│ # LLM now has context and can give personalized responses like: │
│ # "Since you're working on your FastAPI app, here's how to add tests │
│ # with pytest..." │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ 5. CONVERSATION STORAGE (after LLM call) │
│ ───────────────────────────────────────────────────────────────────────── │
│ # The conversation is stored to Hindsight for future recall │
│ hindsight.retain( │
│ content="User: Help me with my Python project\n" │
│ "Assistant: Since you're working on FastAPI..." │
│ ) │
│ # Hindsight extracts facts: "User asked about Python project help" │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ 6. RESPONSE RETURNED │
│ ───────────────────────────────────────────────────────────────────────── │
│ # You receive the response as normal │
│ print(response.choices[0].message.content) │
└─────────────────────────────────────────────────────────────────────────────┘
```
The memory injection and storage happen automatically - you just use `completion()` as normal.
## Configuration Options
```python
hindsight_litellm.configure(
# Required
hindsight_api_url="http://localhost:8888", # Hindsight API server URL
bank_id="my-agent", # Memory bank ID
api_key="your-api-key", # Optional API key for authentication
# Optional - Memory behavior
store_conversations=True, # Store conversations after LLM calls
inject_memories=True, # Inject relevant memories into prompts
use_reflect=False, # Use reflect API (synthesized) vs recall (raw memories)
reflect_include_facts=False, # Include source facts with reflect responses
max_memories=None, # Maximum memories to inject (None = unlimited)
max_memory_tokens=4096, # Maximum tokens for memory context
recall_budget="mid", # Recall budget: "low", "mid", "high"
fact_types=["world", "agent"], # Filter fact types to inject
# Optional - Bank Configuration
bank_name="My Agent", # Human-readable display name for the memory bank
background="This agent...", # Instructions guiding what Hindsight should remember (see below)
# Optional - Advanced
injection_mode="system_message", # or "prepend_user"
excluded_models=["gpt-3.5*"], # Exclude certain models
verbose=True, # Enable verbose logging and debug info
)
```
### Bank Configuration: background and bank_name
The `background` and `bank_name` parameters configure the memory bank itself. When provided, `configure()` will automatically create or update the bank with these settings.
- **bank_name**: A human-readable display name for the memory bank. Useful for identifying banks in the Hindsight UI or when managing multiple banks.
- **background**: Instructions that guide Hindsight on what information is important to extract and remember from conversations. This influences memory extraction during the `retain` operation and can affect how the bank's "disposition" (skepticism, literalism, empathy) is calibrated.
```python
# Example: Customer support routing agent
hindsight_litellm.configure(
hindsight_api_url="http://localhost:8888",
bank_id="support-router",
bank_name="Customer Support Router",
background="""This agent routes customer support requests to the appropriate team.
Remember which types of issues should go to which teams (billing, technical, sales).
Track customer preferences for communication channels and past issue resolutions.
Note any escalation patterns or VIP customers who need special handling.""",
)
```
### Memory Modes: Reflect vs Recall
- **Recall mode** (`use_reflect=False`, default): Retrieves raw memory facts and injects them as a numbered list. Best when you need precise, individual memories.
- **Reflect mode** (`use_reflect=True`): Synthesizes memories into a coherent context paragraph. Best for natural, conversational memory context.
```python
# Recall mode - raw memories
hindsight_litellm.configure(
bank_id="my-agent",
use_reflect=False, # Default
)
# Injects: "1. [WORLD] User prefers Python\n2. [OPINION] User dislikes Java..."
# Reflect mode - synthesized context
hindsight_litellm.configure(
bank_id="my-agent",
use_reflect=True,
)
# Injects: "Based on previous conversations, the user is a Python developer who..."
```
## Multi-Provider Support
Works with any LiteLLM-supported provider:
```python
import hindsight_litellm
hindsight_litellm.configure(
hindsight_api_url="http://localhost:8888",
bank_id="my-agent",
)
hindsight_litellm.enable()
# OpenAI
hindsight_litellm.completion(model="gpt-4o", messages=[...])
# Anthropic
hindsight_litellm.completion(model="claude-3-5-sonnet-20241022", messages=[...])
# Groq
hindsight_litellm.completion(model="groq/llama-3.1-70b-versatile", messages=[...])
# Azure OpenAI
hindsight_litellm.completion(model="azure/gpt-4", messages=[...])
# AWS Bedrock
hindsight_litellm.completion(model="bedrock/anthropic.claude-3", messages=[...])
# Google Vertex AI
hindsight_litellm.completion(model="vertex_ai/gemini-pro", messages=[...])
```
## Direct Memory APIs
### Recall - Query raw memories
```python
from hindsight_litellm import configure, recall
configure(bank_id="my-agent", hindsight_api_url="http://localhost:8888")
# Query memories
memories = recall("what projects am I working on?", budget="mid")
for m in memories:
print(f"- [{m.fact_type}] {m.text}")
# Output:
# - [world] User is building a FastAPI project
# - [opinion] User prefers Python over JavaScript
```
### Reflect - Get synthesized context
```python
from hindsight_litellm import configure, reflect
configure(bank_id="my-agent", hindsight_api_url="http://localhost:8888")
# Get synthesized memory context
result = reflect("what do you know about the user's preferences?")
print(result.text)
# Output:
# "Based on our conversations, the user prefers Python for backend development..."
```
### Retain - Store memories
```python
from hindsight_litellm import configure, retain
configure(bank_id="my-agent", hindsight_api_url="http://localhost:8888")
# Store a memory
result = retain(
content="User mentioned they're working on a machine learning project",
context="Discussion about current projects",
)
print(f"Retained successfully: {result.success}, items: {result.items_count}")
```
### Async APIs
```python
from hindsight_litellm import arecall, areflect, aretain
# Async versions of all memory APIs
memories = await arecall("what do you know about me?")
context = await areflect("summarize user preferences")
result = await aretain(content="New information to remember")
```
## Native Client Wrappers
Alternative to LiteLLM callbacks for direct SDK integration:
### OpenAI Wrapper
```python
from openai import OpenAI
from hindsight_litellm import wrap_openai
client = OpenAI()
wrapped = wrap_openai(
client,
bank_id="my-agent",
hindsight_api_url="http://localhost:8888",
)
response = wrapped.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": "What do you know about me?"}]
)
```
### Anthropic Wrapper
```python
from anthropic import Anthropic
from hindsight_litellm import wrap_anthropic
client = Anthropic()
wrapped = wrap_anthropic(
client,
bank_id="my-agent",
hindsight_api_url="http://localhost:8888",
)
response = wrapped.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello!"}]
)
```
## Debug Mode
When `verbose=True`, you can inspect exactly what memories are being injected:
```python
from hindsight_litellm import configure, enable, completion, get_last_injection_debug
configure(
bank_id="my-agent",
hindsight_api_url="http://localhost:8888",
verbose=True,
use_reflect=True,
)
enable()
response = completion(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "What's my favorite color?"}]
)
# Inspect what was injected
debug = get_last_injection_debug()
if debug:
print(f"Mode: {debug.mode}") # "reflect" or "recall"
print(f"Injected: {debug.injected}") # True/False
print(f"Results: {debug.results_count}")
print(f"Memory context:\n{debug.memory_context}")
if debug.error:
print(f"Error: {debug.error}")
```
## Context Manager
```python
from hindsight_litellm import hindsight_memory
import litellm
with hindsight_memory(bank_id="user-123"):
response = litellm.completion(model="gpt-4", messages=[...])
# Memory integration automatically disabled after context
```
## Disabling and Cleanup
```python
from hindsight_litellm import disable, cleanup
# Temporarily disable memory integration
disable()
# Clean up all resources (call when shutting down)
cleanup()
```
## API Reference
### Main Functions
| Function | Description |
|----------|-------------|
| `configure(...)` | Configure global Hindsight settings |
| `enable()` | Enable memory integration with LiteLLM |
| `disable()` | Disable memory integration |
| `is_enabled()` | Check if memory integration is enabled |
| `cleanup()` | Clean up all resources |
### Configuration Functions
| Function | Description |
|----------|-------------|
| `get_config()` | Get current configuration |
| `is_configured()` | Check if Hindsight is configured |
| `reset_config()` | Reset configuration to defaults |
### Memory Functions
| Function | Description |
|----------|-------------|
| `recall(query, ...)` | Synchronously query raw memories |
| `arecall(query, ...)` | Asynchronously query raw memories |
| `reflect(query, ...)` | Synchronously get synthesized memory context |
| `areflect(query, ...)` | Asynchronously get synthesized memory context |
| `retain(content, ...)` | Synchronously store a memory |
| `aretain(content, ...)` | Asynchronously store a memory |
### Debug Functions
| Function | Description |
|----------|-------------|
| `get_last_injection_debug()` | Get debug info from last memory injection |
| `clear_injection_debug()` | Clear stored debug info |
### Client Wrappers
| Function | Description |
|----------|-------------|
| `wrap_openai(client, ...)` | Wrap OpenAI client with memory |
| `wrap_anthropic(client, ...)` | Wrap Anthropic client with memory |
## Requirements
- Python >= 3.10
- litellm >= 1.40.0
- A running Hindsight API server
## License
MIT
@@ -0,0 +1,817 @@
"""Hindsight-LiteLLM: Universal LLM memory integration via LiteLLM.
This package provides automatic memory integration for any LLM provider
supported by LiteLLM (100+ providers including OpenAI, Anthropic, Groq,
Azure, AWS Bedrock, Google Vertex AI, and more).
Features:
- Automatic memory injection before LLM calls
- Automatic conversation storage after LLM calls
- Works with any LiteLLM-supported provider
- Zero code changes to existing LiteLLM usage
- Multi-user support via separate bank_ids
- Document grouping for conversation threading
- Direct recall API for manual memory queries
- Native client wrappers for OpenAI and Anthropic
Basic usage:
>>> from hindsight_litellm import configure, enable
>>>
>>> # Configure Hindsight integration
>>> configure(
... hindsight_api_url="http://localhost:8888",
... bank_id="user-123", # Use separate bank_ids for multi-user support
... store_conversations=True,
... inject_memories=True,
... )
>>>
>>> # Enable memory integration
>>> enable()
>>>
>>> # Now use LiteLLM as normal - memory integration is automatic
>>> import litellm
>>> response = litellm.completion(
... model="gpt-4",
... messages=[{"role": "user", "content": "What did we discuss about AI?"}]
... )
Direct recall API:
>>> from hindsight_litellm import configure, recall
>>> configure(bank_id="my-agent", hindsight_api_url="http://localhost:8888")
>>>
>>> # Query memories directly
>>> memories = recall("what projects am I working on?")
>>> for m in memories:
... print(f"- [{m.fact_type}] {m.text}")
Native client wrappers:
>>> from openai import OpenAI
>>> from hindsight_litellm import wrap_openai
>>>
>>> client = OpenAI()
>>> wrapped = wrap_openai(client, bank_id="user-123")
>>>
>>> response = wrapped.chat.completions.create(
... model="gpt-4",
... messages=[{"role": "user", "content": "Hello!"}]
... )
Works with any LiteLLM-supported provider:
>>> # OpenAI
>>> litellm.completion(model="gpt-4", messages=[...])
>>>
>>> # Anthropic
>>> litellm.completion(model="claude-3-opus-20240229", messages=[...])
>>>
>>> # Groq
>>> litellm.completion(model="groq/llama-3.1-70b-versatile", messages=[...])
>>>
>>> # Azure OpenAI
>>> litellm.completion(model="azure/gpt-4", messages=[...])
>>>
>>> # AWS Bedrock
>>> litellm.completion(model="bedrock/anthropic.claude-3", messages=[...])
>>>
>>> # Google Vertex AI
>>> litellm.completion(model="vertex_ai/gemini-pro", messages=[...])
Context manager usage:
>>> from hindsight_litellm import hindsight_memory
>>>
>>> with hindsight_memory(bank_id="user-123"):
... response = litellm.completion(model="gpt-4", messages=[...])
>>> # Memory integration automatically disabled after context
Configuration options:
- hindsight_api_url: URL of your Hindsight API server
- bank_id: Memory bank ID for memory operations (required). For multi-user
support, use different bank_ids per user (e.g., f"user-{user_id}")
- api_key: Optional API key for Hindsight authentication
- store_conversations: Whether to store conversations (default: True)
- inject_memories: Whether to inject relevant memories (default: True)
- injection_mode: How to inject memories (system_message or prepend_user)
- max_memories: Maximum number of memories to inject (None = unlimited)
- recall_budget: Budget for memory recall (low, mid, high)
- excluded_models: List of model patterns to exclude from interception
- verbose: Enable verbose logging
- bank_name: Display name for the memory bank
- background: Instructions that help Hindsight understand what to remember
Background example:
>>> configure(
... bank_id="routing-agent",
... background="This agent routes customer requests to support channels. "
... "Remember which types of issues should go to which channels.",
... )
"""
from contextlib import contextmanager
from dataclasses import dataclass
from typing import Optional, List, Any
import litellm
from .config import (
configure,
get_config,
is_configured,
reset_config,
HindsightConfig,
MemoryInjectionMode,
)
from .callbacks import (
HindsightCallback,
get_callback,
cleanup_callback,
)
from .wrappers import (
recall,
arecall,
RecallResult,
RecallResponse,
RecallDebugInfo,
reflect,
areflect,
ReflectResult,
ReflectDebugInfo,
retain,
aretain,
RetainResult,
RetainDebugInfo,
wrap_openai,
wrap_anthropic,
HindsightOpenAI,
HindsightAnthropic,
)
__version__ = "0.1.0"
# Track whether we've registered with LiteLLM
_enabled = False
# Store original functions for restoration
_original_completion = None
_original_acompletion = None
@dataclass
class InjectionDebugInfo:
"""Debug information from a memory injection operation.
This is populated when verbose=True in the config and can be retrieved
via get_last_injection_debug() after a completion() call.
Attributes:
mode: The injection mode used ("reflect" or "recall")
query: The user query used for memory lookup
bank_id: The bank ID used
memory_context: The formatted memory context that was injected
reflect_text: The raw reflect text (when mode="reflect")
reflect_facts: The facts used to generate the reflect response (when reflect_include_facts=True)
recall_results: The raw recall results (when mode="recall")
results_count: Number of memories/results found
injected: Whether memories were actually injected into the prompt
error: Error message if injection failed (None on success)
"""
mode: str # "reflect" or "recall"
query: str
bank_id: str
memory_context: str # The formatted context that was injected
reflect_text: Optional[str] = None # Raw reflect response text
reflect_facts: Optional[List[dict]] = None # Facts used by reflect (when reflect_include_facts=True)
recall_results: Optional[List[dict]] = None # Raw recall results
results_count: int = 0
injected: bool = False
error: Optional[str] = None # Error message if injection failed
# Store the last injection debug info (populated when verbose=True)
_last_injection_debug: Optional[InjectionDebugInfo] = None
def get_last_injection_debug() -> Optional[InjectionDebugInfo]:
"""Get debug info from the last memory injection operation.
When verbose=True in the config, this returns information about
what memories were injected into the last completion() call.
Returns:
InjectionDebugInfo if verbose mode captured injection info, None otherwise
Example:
>>> from hindsight_litellm import configure, enable, completion, get_last_injection_debug
>>> configure(bank_id="my-agent", verbose=True, use_reflect=True)
>>> enable()
>>> response = completion(model="gpt-4o-mini", messages=[...])
>>> debug = get_last_injection_debug()
>>> if debug:
... print(f"Injected {debug.results_count} memories via {debug.mode}")
... print(f"Reflect text: {debug.reflect_text}")
"""
return _last_injection_debug
def clear_injection_debug() -> None:
"""Clear the stored injection debug info."""
global _last_injection_debug
_last_injection_debug = None
def _inject_memories(messages: List[dict]) -> List[dict]:
"""Inject memories into messages list.
Returns the modified messages list with memories injected into the system message.
Uses reflect API when config.use_reflect=True, otherwise uses recall API.
When verbose=True in config, stores debug info retrievable via get_last_injection_debug().
"""
global _last_injection_debug
import logging
# Clear previous debug info
_last_injection_debug = None
if not is_configured():
return messages
config = get_config()
if not config or not config.enabled or not config.inject_memories:
return messages
if not messages:
return messages
# Extract user query from last user message
user_query = None
for msg in reversed(messages):
if msg.get("role") == "user":
content = msg.get("content")
if isinstance(content, str):
user_query = content
break
if not user_query:
return messages
try:
from hindsight_client import Hindsight
# Use bank_id directly (no entity scoping)
bank_id = config.bank_id
# Track debug info
mode = "reflect" if config.use_reflect else "recall"
reflect_text = None
reflect_facts = None
recall_results = None
results_count = 0
memory_context = ""
# Create client
client = Hindsight(base_url=config.hindsight_api_url, timeout=30.0)
# Use reflect API if use_reflect is enabled
if config.use_reflect:
# If reflect_include_facts is enabled, use the API directly to include facts
if config.reflect_include_facts:
from hindsight_client_api.models import reflect_request, reflect_include_options
request_obj = reflect_request.ReflectRequest(
query=user_query,
budget=config.recall_budget or "mid",
include=reflect_include_options.ReflectIncludeOptions(facts={}),
)
import asyncio
try:
loop = asyncio.get_event_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
result = loop.run_until_complete(client._api.reflect(bank_id, request_obj))
# Extract facts from based_on
if hasattr(result, 'based_on') and result.based_on:
reflect_facts = [
{
"text": f.text if hasattr(f, 'text') else str(f),
"type": getattr(f, 'type', None),
"context": getattr(f, 'context', None),
}
for f in result.based_on
]
else:
result = client.reflect(
bank_id=bank_id,
query=user_query,
budget=config.recall_budget or "mid",
)
reflect_text = result.text if hasattr(result, 'text') else str(result)
if not reflect_text:
# Store debug info for empty result
if config.verbose:
_last_injection_debug = InjectionDebugInfo(
mode=mode,
query=user_query,
bank_id=bank_id,
memory_context="",
reflect_text="",
reflect_facts=reflect_facts,
results_count=0,
injected=False,
)
return messages
results_count = 1 # reflect returns a single synthesized response
memory_context = (
"# Relevant Context from Memory\n"
f"{reflect_text}"
)
else:
# Use recall API (original behavior)
result = client.recall(
bank_id=bank_id,
query=user_query,
budget=config.recall_budget or "mid",
max_tokens=config.max_memory_tokens or 4096,
types=config.fact_types,
)
# client.recall() returns a list directly, not an object with .results
if isinstance(result, list):
results = result
elif hasattr(result, 'results'):
results = result.results
else:
results = []
# Convert to dicts for debug info
recall_results = [
{
"text": r.text if hasattr(r, 'text') else str(r),
"type": getattr(r, 'type', 'world'),
}
for r in results
]
if not results:
# Store debug info for empty result
if config.verbose:
_last_injection_debug = InjectionDebugInfo(
mode=mode,
query=user_query,
bank_id=bank_id,
memory_context="",
recall_results=[],
results_count=0,
injected=False,
)
return messages
# Format memories (apply limit if set, otherwise use all)
results_to_use = results[:config.max_memories] if config.max_memories else results
memory_lines = []
for i, r in enumerate(results_to_use, 1):
text = r.text if hasattr(r, 'text') else str(r)
fact_type = getattr(r, 'type', 'world')
if text:
type_label = fact_type.upper() if fact_type else "MEMORY"
memory_lines.append(f"{i}. [{type_label}] {text}")
if not memory_lines:
if config.verbose:
_last_injection_debug = InjectionDebugInfo(
mode=mode,
query=user_query,
bank_id=bank_id,
memory_context="",
recall_results=recall_results,
results_count=0,
injected=False,
)
return messages
results_count = len(memory_lines)
memory_context = (
"# Relevant Memories\n"
"The following information from memory may be relevant:\n\n"
+ "\n".join(memory_lines)
)
# Inject into messages
updated_messages = list(messages)
# Find existing system message or create new one
found_system = False
for i, msg in enumerate(updated_messages):
if msg.get("role") == "system":
existing_content = msg.get("content", "")
updated_messages[i] = {
**msg,
"content": f"{existing_content}\n\n{memory_context}"
}
found_system = True
break
if not found_system:
updated_messages.insert(0, {
"role": "system",
"content": memory_context
})
# Store debug info when verbose
if config.verbose:
_last_injection_debug = InjectionDebugInfo(
mode=mode,
query=user_query,
bank_id=bank_id,
memory_context=memory_context,
reflect_text=reflect_text,
reflect_facts=reflect_facts,
recall_results=recall_results,
results_count=results_count,
injected=True,
)
logger = logging.getLogger("hindsight_litellm")
logger.info(f"Injected memories using {mode} into prompt")
return updated_messages
except ImportError as e:
if config.verbose:
logging.getLogger("hindsight_litellm").warning(
f"hindsight_client not installed: {e}. Install with: pip install hindsight-client"
)
_last_injection_debug = InjectionDebugInfo(
mode="reflect" if config.use_reflect else "recall",
query=user_query or "",
bank_id=config.bank_id or "",
memory_context="",
results_count=0,
injected=False,
error=f"hindsight_client not installed: {e}",
)
return messages
except Exception as e:
# Always set debug info on error when verbose mode is on
if config.verbose:
logging.getLogger("hindsight_litellm").warning(f"Failed to inject memories: {e}")
_last_injection_debug = InjectionDebugInfo(
mode="reflect" if config.use_reflect else "recall",
query=user_query or "",
bank_id=config.bank_id or "",
memory_context="",
results_count=0,
injected=False,
error=str(e),
)
return messages
def _wrapped_completion(*args, **kwargs):
"""Wrapper for litellm.completion that injects memories before the call."""
# Inject memories into messages
if "messages" in kwargs:
kwargs["messages"] = _inject_memories(kwargs["messages"])
elif args and len(args) > 1:
# messages might be second positional arg after model
args = list(args)
if isinstance(args[1], list):
args[1] = _inject_memories(args[1])
args = tuple(args)
# Call original
return _original_completion(*args, **kwargs)
async def _wrapped_acompletion(*args, **kwargs):
"""Wrapper for litellm.acompletion that injects memories before the call."""
# Inject memories into messages
if "messages" in kwargs:
kwargs["messages"] = _inject_memories(kwargs["messages"])
elif args and len(args) > 1:
args = list(args)
if isinstance(args[1], list):
args[1] = _inject_memories(args[1])
args = tuple(args)
# Call original
return await _original_acompletion(*args, **kwargs)
def enable() -> None:
"""Enable Hindsight memory integration with LiteLLM.
This monkeypatches LiteLLM functions to:
1. Inject relevant memories into prompts before LLM calls
2. Store conversations to Hindsight after successful LLM calls
Must be called after configure() to take effect.
Example:
>>> from hindsight_litellm import configure, enable
>>> configure(bank_id="my-agent", hindsight_api_url="http://localhost:8888")
>>> enable()
>>>
>>> # Now all LiteLLM calls will have memory integration
>>> import litellm
>>> response = litellm.completion(model="gpt-4", messages=[...])
"""
global _enabled, _original_completion, _original_acompletion
if _enabled:
return # Already enabled
if not is_configured():
raise RuntimeError(
"Hindsight not configured. Call configure() before enable()."
)
# Store original functions and monkeypatch for memory injection
_original_completion = litellm.completion
_original_acompletion = litellm.acompletion
litellm.completion = _wrapped_completion
litellm.acompletion = _wrapped_acompletion
# Get or create the callback instance for storing conversations
callback = get_callback()
# Register callback using litellm.callbacks for conversation storage
if callback not in litellm.callbacks:
litellm.callbacks.append(callback)
_enabled = True
config = get_config()
if config and config.verbose:
print(f"Hindsight memory enabled for bank: {config.bank_id}")
def disable() -> None:
"""Disable Hindsight memory integration with LiteLLM.
This restores the original LiteLLM functions and removes callbacks,
stopping memory injection and conversation storage.
Example:
>>> from hindsight_litellm import disable
>>> disable() # Stop memory integration
"""
global _enabled, _original_completion, _original_acompletion
if not _enabled:
return # Already disabled
# Restore original functions
if _original_completion is not None:
litellm.completion = _original_completion
_original_completion = None
if _original_acompletion is not None:
litellm.acompletion = _original_acompletion
_original_acompletion = None
# Remove callback from litellm.callbacks
callback = get_callback()
if callback in litellm.callbacks:
litellm.callbacks.remove(callback)
_enabled = False
config = get_config()
if config and config.verbose:
print("Hindsight memory disabled")
def is_enabled() -> bool:
"""Check if Hindsight memory integration is currently enabled.
Returns:
True if enable() has been called and not subsequently disabled
"""
return _enabled
def cleanup() -> None:
"""Clean up all Hindsight resources.
This disables the integration and closes any open connections.
Call this when shutting down your application.
Example:
>>> from hindsight_litellm import cleanup
>>> cleanup() # Clean up when done
"""
disable()
cleanup_callback()
reset_config()
# =============================================================================
# Convenience wrappers - use hindsight_litellm.completion() directly
# =============================================================================
def completion(*args, **kwargs):
"""Call LiteLLM completion with Hindsight memory integration.
This is a convenience wrapper that delegates to litellm.completion().
Memory injection and storage happen automatically if configured and enabled.
Args:
*args: Positional arguments passed to litellm.completion()
**kwargs: Keyword arguments passed to litellm.completion()
Returns:
LiteLLM ModelResponse object
Example:
>>> import hindsight_litellm
>>>
>>> hindsight_litellm.configure(
... hindsight_api_url="http://localhost:8888",
... bank_id="my-agent",
... )
>>> hindsight_litellm.enable()
>>>
>>> # Use directly - no need to import litellm separately
>>> response = hindsight_litellm.completion(
... model="gpt-4o-mini",
... messages=[{"role": "user", "content": "Hello!"}]
... )
"""
return litellm.completion(*args, **kwargs)
async def acompletion(*args, **kwargs):
"""Call LiteLLM async completion with Hindsight memory integration.
This is a convenience wrapper that delegates to litellm.acompletion().
Memory injection and storage happen automatically if configured and enabled.
Args:
*args: Positional arguments passed to litellm.acompletion()
**kwargs: Keyword arguments passed to litellm.acompletion()
Returns:
LiteLLM ModelResponse object
Example:
>>> import hindsight_litellm
>>> import asyncio
>>>
>>> hindsight_litellm.configure(
... hindsight_api_url="http://localhost:8888",
... bank_id="my-agent",
... )
>>> hindsight_litellm.enable()
>>>
>>> async def main():
... response = await hindsight_litellm.acompletion(
... model="gpt-4o-mini",
... messages=[{"role": "user", "content": "Hello!"}]
... )
... return response
>>>
>>> asyncio.run(main())
"""
return await litellm.acompletion(*args, **kwargs)
@contextmanager
def hindsight_memory(
hindsight_api_url: str = "http://localhost:8888",
bank_id: Optional[str] = None,
api_key: Optional[str] = None,
store_conversations: bool = True,
inject_memories: bool = True,
injection_mode: MemoryInjectionMode = MemoryInjectionMode.SYSTEM_MESSAGE,
max_memories: Optional[int] = None,
max_memory_tokens: int = 4096,
recall_budget: str = "mid",
fact_types: Optional[List[str]] = None,
document_id: Optional[str] = None,
excluded_models: Optional[List[str]] = None,
verbose: bool = False,
bank_name: Optional[str] = None,
background: Optional[str] = None,
):
"""Context manager for temporary Hindsight memory integration.
Use this to enable memory integration for a specific block of code,
automatically cleaning up afterwards.
Args:
hindsight_api_url: URL of the Hindsight API server
bank_id: Memory bank ID for memory operations (required). For multi-user
support, use different bank_ids per user (e.g., f"user-{user_id}")
api_key: Optional API key for Hindsight authentication
store_conversations: Whether to store conversations
inject_memories: Whether to inject relevant memories
injection_mode: How to inject memories
max_memories: Maximum number of memories to inject (None = unlimited)
max_memory_tokens: Maximum tokens for memory context
recall_budget: Budget for memory recall (low, mid, high)
fact_types: List of fact types to filter (world, agent, opinion, observation)
document_id: Optional document ID for grouping conversations
excluded_models: List of model patterns to exclude
verbose: Enable verbose logging
bank_name: Optional display name for the memory bank
background: Optional background/instructions for memory extraction
Example:
>>> from hindsight_litellm import hindsight_memory
>>> import litellm
>>>
>>> with hindsight_memory(bank_id="user-123"):
... response = litellm.completion(model="gpt-4", messages=[...])
>>> # Memory integration automatically disabled after context
"""
# Save previous state
was_enabled = is_enabled()
previous_config = get_config()
try:
# Configure and enable
configure(
hindsight_api_url=hindsight_api_url,
bank_id=bank_id,
api_key=api_key,
store_conversations=store_conversations,
inject_memories=inject_memories,
injection_mode=injection_mode,
max_memories=max_memories,
max_memory_tokens=max_memory_tokens,
recall_budget=recall_budget,
fact_types=fact_types,
document_id=document_id,
excluded_models=excluded_models,
verbose=verbose,
bank_name=bank_name,
background=background,
)
enable()
yield
finally:
# Restore previous state
disable()
if previous_config:
configure(
hindsight_api_url=previous_config.hindsight_api_url,
bank_id=previous_config.bank_id,
api_key=previous_config.api_key,
store_conversations=previous_config.store_conversations,
inject_memories=previous_config.inject_memories,
injection_mode=previous_config.injection_mode,
max_memories=previous_config.max_memories,
max_memory_tokens=previous_config.max_memory_tokens,
recall_budget=previous_config.recall_budget,
fact_types=previous_config.fact_types,
document_id=previous_config.document_id,
excluded_models=previous_config.excluded_models,
verbose=previous_config.verbose,
bank_name=previous_config.bank_name,
background=previous_config.background,
)
if was_enabled:
enable()
else:
reset_config()
__all__ = [
# Main API
"configure",
"enable",
"disable",
"is_enabled",
"cleanup",
"hindsight_memory",
# LLM completion wrappers (convenience)
"completion",
"acompletion",
# Direct memory APIs
"recall",
"arecall",
"RecallResult",
"reflect",
"areflect",
"ReflectResult",
"retain",
"aretain",
"RetainResult",
# Native client wrappers
"wrap_openai",
"wrap_anthropic",
"HindsightOpenAI",
"HindsightAnthropic",
# Configuration
"get_config",
"is_configured",
"reset_config",
"HindsightConfig",
"MemoryInjectionMode",
# Injection debug (verbose mode)
"get_last_injection_debug",
"clear_injection_debug",
"InjectionDebugInfo",
# Callback (for advanced usage)
"HindsightCallback",
"get_callback",
"cleanup_callback",
]
@@ -0,0 +1,640 @@
"""LiteLLM callback handlers for Hindsight memory integration.
This module implements LiteLLM's CustomLogger interface to intercept
LLM calls and integrate with Hindsight for memory injection and storage.
Uses direct HTTP calls via requests/httpx to avoid async event loop conflicts
when the hindsight_client's async methods are called from LiteLLM callbacks.
"""
import logging
import fnmatch
import hashlib
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional, Set
import asyncio
import threading
import concurrent.futures
from litellm.integrations.custom_logger import CustomLogger
from litellm.types.utils import ModelResponse
from .config import get_config, is_configured, HindsightConfig, MemoryInjectionMode
# Use requests for sync HTTP calls to avoid async event loop issues
try:
import requests
HAS_REQUESTS = True
except ImportError:
HAS_REQUESTS = False
try:
import httpx
HAS_HTTPX = True
except ImportError:
HAS_HTTPX = False
logger = logging.getLogger(__name__)
# Thread pool for running async operations in background
_executor = concurrent.futures.ThreadPoolExecutor(max_workers=4, thread_name_prefix="hindsight-")
class HindsightCallback(CustomLogger):
"""LiteLLM custom logger that integrates with Hindsight memory system.
This callback handler:
1. Injects relevant memories into prompts before LLM calls
2. Stores conversations to Hindsight after successful LLM calls
Features:
- Works with 100+ LLM providers via LiteLLM
- Deduplication to avoid storing duplicate conversations
- Configurable memory injection modes
- Support for entity observations in recall
Usage:
>>> from hindsight_litellm import configure, enable
>>> configure(bank_id="my-agent", hindsight_api_url="http://localhost:8888")
>>> enable()
>>>
>>> # Now all LiteLLM calls will have memory integration
>>> import litellm
>>> response = litellm.completion(
... model="gpt-4",
... messages=[{"role": "user", "content": "What did we discuss?"}]
... )
"""
def __init__(self):
"""Initialize the Hindsight callback handler."""
super().__init__()
self._http_session = None
self._http_lock = threading.Lock()
# Track recently stored conversation hashes for deduplication
self._recent_hashes: Set[str] = set()
self._max_hash_cache = 1000
def _get_http_session(self):
"""Get or create a requests Session (thread-safe)."""
if self._http_session is None:
with self._http_lock:
if self._http_session is None:
if HAS_REQUESTS:
self._http_session = requests.Session()
elif HAS_HTTPX:
self._http_session = httpx.Client(timeout=30.0)
else:
raise RuntimeError(
"Neither 'requests' nor 'httpx' is installed. "
"Please install one: pip install requests"
)
return self._http_session
def _http_post(self, url: str, json_data: dict, config: HindsightConfig) -> Optional[dict]:
"""Make a synchronous HTTP POST request."""
try:
session = self._get_http_session()
headers = {"Content-Type": "application/json"}
if config.api_key:
headers["Authorization"] = f"Bearer {config.api_key}"
if HAS_REQUESTS:
response = session.post(url, json=json_data, headers=headers, timeout=30)
response.raise_for_status()
return response.json()
elif HAS_HTTPX:
response = session.post(url, json=json_data, headers=headers)
response.raise_for_status()
return response.json()
except Exception as e:
if config.verbose:
logger.warning(f"HTTP POST failed: {e}")
return None
def _should_skip_model(self, model: str, config: HindsightConfig) -> bool:
"""Check if this model should be excluded from interception."""
for pattern in config.excluded_models:
if fnmatch.fnmatch(model.lower(), pattern.lower()):
return True
return False
def _extract_user_query(self, messages: List[Dict[str, Any]]) -> Optional[str]:
"""Extract the user's query from the last user message."""
for msg in reversed(messages):
role = msg.get("role", "")
if role == "user":
content = msg.get("content")
if isinstance(content, str):
return content
elif isinstance(content, list):
# Handle structured content (e.g., vision messages)
text_parts = []
for item in content:
if isinstance(item, dict) and item.get("type") == "text":
text_parts.append(item.get("text", ""))
if text_parts:
return " ".join(text_parts)
return None
def _compute_conversation_hash(
self,
user_input: str,
assistant_output: str,
) -> str:
"""Compute a hash for deduplication."""
content = f"{user_input.strip().lower()}|{assistant_output.strip().lower()}"
return hashlib.md5(content.encode()).hexdigest()[:16]
def _is_duplicate(self, conv_hash: str) -> bool:
"""Check if this conversation was recently stored."""
if conv_hash in self._recent_hashes:
return True
# Add to cache, evict oldest if full
self._recent_hashes.add(conv_hash)
if len(self._recent_hashes) > self._max_hash_cache:
# Remove oldest (arbitrary since set, but good enough)
self._recent_hashes.pop()
return False
def _format_memories(
self,
results: List[Any],
config: HindsightConfig
) -> str:
"""Format memory recall results into a context string.
Results can be RecallResult objects (with .text, .type attributes)
or dicts (with get() method).
"""
if not results:
return ""
# Apply limit if set, otherwise use all results
results_to_use = results[:config.max_memories] if config.max_memories else results
memory_lines = []
for i, result in enumerate(results_to_use, 1):
# Handle both RecallResult objects and dicts
if hasattr(result, 'text'):
text = result.text or ""
fact_type = getattr(result, 'type', 'world') or "world"
weight = getattr(result, 'weight', 0.0) or 0.0
else:
text = result.get("text", "")
fact_type = result.get("type", result.get("fact_type", "world"))
weight = result.get("weight", 0.0)
if text:
# Include metadata for context
type_label = fact_type.upper() if fact_type else "MEMORY"
line = f"{i}. [{type_label}] {text}"
if weight > 0 and config.verbose:
line += f" (relevance: {weight:.2f})"
memory_lines.append(line)
if not memory_lines:
return ""
return (
"# Relevant Memories\n"
"The following information from memory may be relevant:\n\n"
+ "\n".join(memory_lines)
)
def _inject_memories_into_messages(
self,
messages: List[Dict[str, Any]],
memory_context: str,
config: HindsightConfig,
) -> List[Dict[str, Any]]:
"""Inject memory context into the messages list."""
if not memory_context:
return messages
updated_messages = list(messages) # Make a copy
if config.injection_mode == MemoryInjectionMode.SYSTEM_MESSAGE:
# Find existing system message or create new one
for i, msg in enumerate(updated_messages):
if msg.get("role") == "system":
# Append to existing system message
existing_content = msg.get("content", "")
updated_messages[i] = {
**msg,
"content": f"{existing_content}\n\n{memory_context}"
}
return updated_messages
# No system message found, prepend one
updated_messages.insert(0, {
"role": "system",
"content": memory_context
})
elif config.injection_mode == MemoryInjectionMode.PREPEND_USER:
# Find the last user message and prepend context
for i in range(len(updated_messages) - 1, -1, -1):
if updated_messages[i].get("role") == "user":
original_content = updated_messages[i].get("content", "")
if isinstance(original_content, str):
updated_messages[i] = {
**updated_messages[i],
"content": f"{memory_context}\n\n---\n\n{original_content}"
}
break
return updated_messages
def _get_bank_id(self, config: HindsightConfig) -> str:
"""Get the bank_id for API calls."""
return config.bank_id
def _recall_memories_sync(
self,
query: str,
config: HindsightConfig
) -> List[Dict[str, Any]]:
"""Recall relevant memories from Hindsight (sync) using direct HTTP."""
try:
bank_id = self._get_bank_id(config)
url = f"{config.hindsight_api_url}/v1/default/banks/{bank_id}/memories/recall"
request_data = {
"query": query,
"budget": config.recall_budget or "mid",
"max_tokens": config.max_memory_tokens or 4096,
}
if config.fact_types:
request_data["types"] = config.fact_types
response = self._http_post(url, request_data, config)
if response and "results" in response:
return response["results"]
return []
except Exception as e:
if config.verbose:
logger.warning(f"Failed to recall memories: {e}")
return []
async def _recall_memories_async(
self,
query: str,
config: HindsightConfig
) -> List[Any]:
"""Recall relevant memories from Hindsight (async).
Uses thread pool executor with sync HTTP to avoid event loop conflicts.
"""
try:
loop = asyncio.get_running_loop()
results = await loop.run_in_executor(
_executor,
self._recall_memories_sync,
query,
config
)
return results if isinstance(results, list) else []
except Exception as e:
if config.verbose:
logger.warning(f"Failed to recall memories: {e}")
return []
def _store_conversation_sync(
self,
messages: List[Dict[str, Any]],
response: ModelResponse,
model: str,
config: HindsightConfig,
) -> None:
"""Store the conversation to Hindsight (sync) using direct HTTP.
By default, stores the full conversation history passed to the LLM.
Each message is stored as a separate item, all linked by document_id.
Hindsight will process the document as a whole for memory extraction.
"""
try:
# Extract assistant response from the LLM response
assistant_output = ""
if response.choices and len(response.choices) > 0:
choice = response.choices[0]
if hasattr(choice, "message") and choice.message:
assistant_output = choice.message.content or ""
if not assistant_output:
return
# Build conversation items - each message becomes a separate item
# All linked by document_id for Hindsight to process together
items = []
for msg in messages:
role = msg.get("role", "").upper()
content = msg.get("content", "")
# Skip system messages - they're instructions, not conversation
if role == "SYSTEM":
continue
# Skip if this looks like our injected memory context
if isinstance(content, str) and content.startswith("# Relevant Memories"):
continue
# Handle structured content (e.g., vision messages)
if isinstance(content, list):
text_parts = []
for item in content:
if isinstance(item, dict) and item.get("type") == "text":
text_parts.append(item.get("text", ""))
content = " ".join(text_parts)
if content:
# Map roles to clearer labels
label = "USER" if role == "USER" else "ASSISTANT"
items.append(f"{label}: {content}")
# Add the new assistant response
items.append(f"ASSISTANT: {assistant_output}")
if not items:
return
# Use last user message for deduplication hash
user_input = self._extract_user_query(messages) or ""
# Deduplication check
conv_hash = self._compute_conversation_hash(user_input, assistant_output)
if self._is_duplicate(conv_hash):
if config.verbose:
logger.debug(f"Skipping duplicate conversation: {conv_hash}")
return
# Build the full conversation as a single item for now
# (Future: could store each message as separate item in same document)
conversation_text = "\n\n".join(items)
# Build metadata
metadata = {
"source": "litellm",
"model": model,
}
# Add token usage if available
if hasattr(response, "usage") and response.usage:
if hasattr(response.usage, "total_tokens"):
metadata["tokens"] = str(response.usage.total_tokens)
bank_id = self._get_bank_id(config)
url = f"{config.hindsight_api_url}/v1/default/banks/{bank_id}/memories"
request_data = {
"items": [
{
"content": conversation_text,
"context": f"conversation:litellm:{model}",
"metadata": metadata,
"document_id": config.document_id, # Group by document
}
],
}
self._http_post(url, request_data, config)
if config.verbose:
logger.info(f"Stored conversation to Hindsight bank: {config.bank_id}")
except Exception as e:
if config.verbose:
logger.warning(f"Failed to store conversation: {e}")
async def _store_conversation_async(
self,
messages: List[Dict[str, Any]],
response: ModelResponse,
model: str,
config: HindsightConfig,
) -> None:
"""Store the conversation to Hindsight (async).
Uses thread pool executor with sync HTTP to avoid event loop conflicts.
"""
try:
loop = asyncio.get_running_loop()
await loop.run_in_executor(
_executor,
self._store_conversation_sync,
messages,
response,
model,
config
)
except Exception as e:
if config.verbose:
logger.warning(f"Failed to store conversation: {e}")
# ========== LiteLLM CustomLogger Interface ==========
def log_pre_api_call(
self,
model: str,
messages: List[Dict[str, Any]],
kwargs: Dict[str, Any],
) -> None:
"""Called before making the API call (sync).
This is where we inject memories into the messages.
"""
if not is_configured():
return
config = get_config()
if not config or not config.enabled or not config.inject_memories:
return
if self._should_skip_model(model, config):
return
# Extract user query
user_query = self._extract_user_query(messages)
if not user_query:
return
# Recall relevant memories
memories = self._recall_memories_sync(user_query, config)
if not memories:
return
# Format and inject memories
memory_context = self._format_memories(memories, config)
updated_messages = self._inject_memories_into_messages(
messages, memory_context, config
)
# Modify messages list IN-PLACE (don't just reassign kwargs)
messages.clear()
messages.extend(updated_messages)
if config.verbose:
logger.info(f"Injected {len(memories)} memories into prompt")
async def async_log_pre_api_call(
self,
model: str,
messages: List[Dict[str, Any]],
kwargs: Dict[str, Any],
) -> None:
"""Called before making the API call (async).
This is where we inject memories into the messages.
"""
if not is_configured():
return
config = get_config()
if not config or not config.enabled or not config.inject_memories:
return
if self._should_skip_model(model, config):
return
# Extract user query
user_query = self._extract_user_query(messages)
if not user_query:
return
# Recall relevant memories
memories = await self._recall_memories_async(user_query, config)
if not memories:
return
# Format and inject memories
memory_context = self._format_memories(memories, config)
updated_messages = self._inject_memories_into_messages(
messages, memory_context, config
)
# Modify messages list IN-PLACE (don't just reassign kwargs)
messages.clear()
messages.extend(updated_messages)
if config.verbose:
logger.info(f"Injected {len(memories)} memories into prompt")
def log_success_event(
self,
kwargs: Dict[str, Any],
response_obj: Any,
start_time: float,
end_time: float,
) -> None:
"""Called after successful API call (sync).
This is where we store the conversation.
"""
if not is_configured():
return
config = get_config()
if not config or not config.enabled or not config.store_conversations:
return
model = kwargs.get("model", "unknown")
if self._should_skip_model(model, config):
return
messages = kwargs.get("messages", [])
if not messages:
return
# Store the conversation
self._store_conversation_sync(messages, response_obj, model, config)
async def async_log_success_event(
self,
kwargs: Dict[str, Any],
response_obj: Any,
start_time: float,
end_time: float,
) -> None:
"""Called after successful API call (async).
This is where we store the conversation.
"""
if not is_configured():
return
config = get_config()
if not config or not config.enabled or not config.store_conversations:
return
model = kwargs.get("model", "unknown")
if self._should_skip_model(model, config):
return
messages = kwargs.get("messages", [])
if not messages:
return
# Store the conversation
await self._store_conversation_async(messages, response_obj, model, config)
def log_failure_event(
self,
kwargs: Dict[str, Any],
response_obj: Any,
start_time: float,
end_time: float,
) -> None:
"""Called after failed API call (sync)."""
# We don't store failed conversations
pass
async def async_log_failure_event(
self,
kwargs: Dict[str, Any],
response_obj: Any,
start_time: float,
end_time: float,
) -> None:
"""Called after failed API call (async)."""
# We don't store failed conversations
pass
def close(self) -> None:
"""Clean up resources."""
with self._http_lock:
if self._http_session is not None:
try:
if HAS_REQUESTS:
self._http_session.close()
elif HAS_HTTPX:
self._http_session.close()
except Exception:
pass
self._http_session = None
self._recent_hashes.clear()
# Global callback instance
_callback: Optional[HindsightCallback] = None
def get_callback() -> HindsightCallback:
"""Get the global callback instance, creating it if necessary."""
global _callback
if _callback is None:
_callback = HindsightCallback()
return _callback
def cleanup_callback() -> None:
"""Clean up the global callback instance."""
global _callback
if _callback is not None:
_callback.close()
_callback = None
@@ -0,0 +1,232 @@
"""Global configuration for Hindsight-LiteLLM integration."""
from typing import Optional, List
from dataclasses import dataclass, field
from enum import Enum
class MemoryInjectionMode(str, Enum):
"""How memories should be injected into the prompt."""
SYSTEM_MESSAGE = "system_message" # Add as system message
PREPEND_USER = "prepend_user" # Prepend to user message
DISABLED = "disabled" # Don't inject memories
@dataclass
class HindsightConfig:
"""Configuration for Hindsight integration with LiteLLM.
Attributes:
hindsight_api_url: URL of the Hindsight API server
bank_id: Memory bank ID for memory operations (required). For multi-user
support, use different bank_ids per user (e.g., f"user-{user_id}")
api_key: Optional API key for Hindsight authentication
store_conversations: Whether to store conversations to Hindsight
inject_memories: Whether to inject relevant memories into prompts
injection_mode: How to inject memories (system_message or prepend_user)
max_memories: Maximum number of memories to inject
max_memory_tokens: Maximum tokens for injected memory context
recall_budget: Budget level for memory recall (low, mid, high)
fact_types: List of fact types to filter recall (world, agent, opinion, observation)
document_id: Optional document ID for grouping stored conversations
enabled: Master switch to enable/disable Hindsight integration
excluded_models: List of model patterns to exclude from interception
verbose: Enable verbose logging
bank_name: Optional display name for the memory bank
background: Optional background/instructions for memory extraction
use_reflect: Use reflect API instead of recall for memory injection (synthesizes answer)
"""
hindsight_api_url: str = "http://localhost:8888"
bank_id: Optional[str] = None
api_key: Optional[str] = None
store_conversations: bool = True
inject_memories: bool = True
injection_mode: MemoryInjectionMode = MemoryInjectionMode.SYSTEM_MESSAGE
max_memories: Optional[int] = None # None = no limit (use all results from API)
max_memory_tokens: int = 4096
recall_budget: str = "mid" # low, mid, high
fact_types: Optional[List[str]] = None # world, agent, opinion, observation
document_id: Optional[str] = None
enabled: bool = True
excluded_models: List[str] = field(default_factory=list)
verbose: bool = False
bank_name: Optional[str] = None # Display name for the memory bank
background: Optional[str] = None # Background/instructions for memory extraction
use_reflect: bool = False # Use reflect instead of recall for memory injection
reflect_include_facts: bool = False # Include facts used by reflect in debug info
# Global configuration instance
_global_config: Optional[HindsightConfig] = None
def configure(
hindsight_api_url: str = "http://localhost:8888",
bank_id: Optional[str] = None,
api_key: Optional[str] = None,
store_conversations: bool = True,
inject_memories: bool = True,
injection_mode: MemoryInjectionMode = MemoryInjectionMode.SYSTEM_MESSAGE,
max_memories: Optional[int] = None,
max_memory_tokens: int = 4096,
recall_budget: str = "mid",
fact_types: Optional[List[str]] = None,
document_id: Optional[str] = None,
enabled: bool = True,
excluded_models: Optional[List[str]] = None,
verbose: bool = False,
bank_name: Optional[str] = None,
background: Optional[str] = None,
use_reflect: bool = False,
reflect_include_facts: bool = False,
) -> HindsightConfig:
"""Configure global Hindsight integration settings for LiteLLM.
This function sets up the global configuration that will be used by the
LiteLLM callbacks to inject memories and store conversations.
Args:
hindsight_api_url: URL of the Hindsight API server
bank_id: Memory bank ID for memory operations (required). For multi-user
support, use different bank_ids per user (e.g., f"user-{user_id}")
api_key: Optional API key for Hindsight authentication
store_conversations: Whether to store conversations to Hindsight
inject_memories: Whether to inject relevant memories into prompts
injection_mode: How to inject memories into the prompt
max_memories: Maximum number of memories to inject
max_memory_tokens: Maximum tokens for injected memory context
recall_budget: Budget level for memory recall (low, mid, high)
fact_types: List of fact types to filter (world, agent, opinion, observation)
document_id: Optional document ID for grouping stored conversations
enabled: Master switch to enable/disable Hindsight integration
excluded_models: List of model patterns to exclude from interception
verbose: Enable verbose logging
bank_name: Optional display name for the memory bank
background: Optional background/instructions that help Hindsight understand
what information is important to extract and remember from conversations.
This is passed to create_bank() to configure the memory bank.
use_reflect: Use reflect API instead of recall for memory injection.
When True, Hindsight will synthesize a contextual answer based on
memories rather than returning raw memory facts.
reflect_include_facts: When use_reflect=True, include the facts that
were used to generate the reflect response in the debug info.
This is useful for debugging what memories the reflect API used.
Returns:
The configured HindsightConfig instance
Example:
>>> from hindsight_litellm import configure, enable
>>> configure(
... hindsight_api_url="http://localhost:8888",
... bank_id="user-123", # Per-user bank for multi-user support
... store_conversations=True,
... inject_memories=True,
... background="This agent routes customer requests to support channels. "
... "Remember which types of issues should go to which channels.",
... )
>>> enable() # Register callbacks with LiteLLM
"""
global _global_config
_global_config = HindsightConfig(
hindsight_api_url=hindsight_api_url,
bank_id=bank_id,
api_key=api_key,
store_conversations=store_conversations,
inject_memories=inject_memories,
injection_mode=injection_mode,
max_memories=max_memories,
max_memory_tokens=max_memory_tokens,
recall_budget=recall_budget,
fact_types=fact_types,
document_id=document_id,
enabled=enabled,
excluded_models=excluded_models or [],
verbose=verbose,
bank_name=bank_name,
background=background,
use_reflect=use_reflect,
reflect_include_facts=reflect_include_facts,
)
# If background or bank_name is provided, create/update the bank
if bank_id and (background or bank_name):
_create_or_update_bank(
hindsight_api_url=hindsight_api_url,
bank_id=bank_id,
name=bank_name,
background=background,
verbose=verbose,
)
return _global_config
def _create_or_update_bank(
hindsight_api_url: str,
bank_id: str,
name: Optional[str] = None,
background: Optional[str] = None,
verbose: bool = False,
) -> None:
"""Create or update a memory bank with the given configuration.
This is called automatically by configure() when background or bank_name is provided.
"""
try:
from hindsight_client import Hindsight
client = Hindsight(hindsight_api_url)
client.create_bank(
bank_id=bank_id,
name=name,
background=background,
)
if verbose:
import logging
logging.getLogger("hindsight_litellm").info(
f"Created/updated bank '{bank_id}' with background"
)
except ImportError:
if verbose:
import logging
logging.getLogger("hindsight_litellm").warning(
"hindsight_client not installed. Cannot create bank with background. "
"Install with: pip install hindsight-client"
)
except Exception as e:
if verbose:
import logging
logging.getLogger("hindsight_litellm").warning(
f"Failed to create/update bank: {e}"
)
def get_config() -> Optional[HindsightConfig]:
"""Get the current global configuration.
Returns:
The current HindsightConfig instance, or None if not configured
"""
return _global_config
def is_configured() -> bool:
"""Check if Hindsight has been configured.
Returns:
True if configure() has been called with a valid bank_id
"""
return (
_global_config is not None
and _global_config.enabled
and _global_config.bank_id is not None
)
def reset_config() -> None:
"""Reset the global configuration to None."""
global _global_config
_global_config = None
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,59 @@
[project]
name = "hindsight-litellm"
version = "0.1.5"
description = "Universal LLM memory integration via LiteLLM - works with 100+ providers"
readme = "README.md"
requires-python = ">=3.10"
license = { text = "MIT" }
authors = [
{ name = "Vectorize", email = "[email protected]" }
]
keywords = [
"ai",
"memory",
"llm",
"litellm",
"openai",
"anthropic",
"groq",
"langchain",
"agents",
"hindsight",
]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Scientific/Engineering :: Artificial Intelligence",
]
dependencies = [
"litellm>=1.40.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0.0",
"pytest-asyncio>=0.21.0",
"pytest-mock>=3.10.0",
]
[project.urls]
Homepage = "https://github.com/vectorize-io/hindsight"
Documentation = "https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/litellm"
Repository = "https://github.com/vectorize-io/hindsight"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["hindsight_litellm"]
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
@@ -0,0 +1 @@
# Tests for hindsight-litellm
@@ -0,0 +1,471 @@
"""Integration tests for hindsight-litellm."""
import pytest
from unittest.mock import Mock, patch, MagicMock
from typing import List, Dict, Any
from hindsight_litellm import (
configure,
enable,
disable,
is_enabled,
cleanup,
get_config,
is_configured,
reset_config,
HindsightConfig,
MemoryInjectionMode,
)
from hindsight_litellm.callbacks import HindsightCallback, get_callback, cleanup_callback
class TestConfiguration:
"""Tests for configuration management."""
def setup_method(self):
"""Reset config before each test."""
reset_config()
disable()
def teardown_method(self):
"""Clean up after each test."""
cleanup()
def test_configure_creates_config(self):
"""Test that configure creates a config object."""
config = configure(
bank_id="test-agent",
hindsight_api_url="http://localhost:8888",
)
assert config is not None
assert config.bank_id == "test-agent"
assert config.hindsight_api_url == "http://localhost:8888"
assert config.enabled is True
def test_configure_with_all_options(self):
"""Test configure with all options."""
config = configure(
hindsight_api_url="http://custom:9999",
bank_id="custom-agent",
api_key="secret-key",
store_conversations=False,
inject_memories=False,
injection_mode=MemoryInjectionMode.PREPEND_USER,
max_memories=5,
max_memory_tokens=1000,
recall_budget="high",
fact_types=["world", "opinion"],
document_id="doc-123",
enabled=True,
excluded_models=["gpt-3.5*"],
verbose=True,
)
assert config.hindsight_api_url == "http://custom:9999"
assert config.bank_id == "custom-agent"
assert config.api_key == "secret-key"
assert config.store_conversations is False
assert config.inject_memories is False
assert config.injection_mode == MemoryInjectionMode.PREPEND_USER
assert config.max_memories == 5
assert config.max_memory_tokens == 1000
assert config.recall_budget == "high"
assert config.fact_types == ["world", "opinion"]
assert config.document_id == "doc-123"
assert config.excluded_models == ["gpt-3.5*"]
assert config.verbose is True
def test_is_configured_without_bank_id(self):
"""Test is_configured returns False without bank_id."""
configure(hindsight_api_url="http://localhost:8888")
assert is_configured() is False
def test_is_configured_with_bank_id(self):
"""Test is_configured returns True with bank_id."""
configure(bank_id="test-agent")
assert is_configured() is True
def test_reset_config(self):
"""Test reset_config clears the configuration."""
configure(bank_id="test-agent")
assert is_configured() is True
reset_config()
assert get_config() is None
assert is_configured() is False
class TestEnableDisable:
"""Tests for enable/disable functionality."""
def setup_method(self):
"""Reset state before each test."""
cleanup()
def teardown_method(self):
"""Clean up after each test."""
cleanup()
def test_enable_without_config_raises(self):
"""Test enable raises error without configuration."""
with pytest.raises(RuntimeError, match="not configured"):
enable()
def test_enable_registers_callback(self):
"""Test enable registers callback with LiteLLM."""
import litellm
configure(bank_id="test-agent")
enable()
callback = get_callback()
assert callback in litellm.callbacks
assert is_enabled() is True
def test_disable_removes_callback(self):
"""Test disable removes callback from LiteLLM."""
import litellm
configure(bank_id="test-agent")
enable()
assert is_enabled() is True
disable()
callback = get_callback()
assert callback not in litellm.callbacks
assert is_enabled() is False
def test_enable_idempotent(self):
"""Test enable is idempotent (can be called multiple times)."""
import litellm
configure(bank_id="test-agent")
# Enable multiple times
enable()
enable()
enable()
# Should only have one callback
callback = get_callback()
assert litellm.callbacks.count(callback) == 1
class TestCallback:
"""Tests for the HindsightCallback class."""
def setup_method(self):
"""Reset state before each test."""
cleanup()
def teardown_method(self):
"""Clean up after each test."""
cleanup()
def test_extract_user_query_simple(self):
"""Test extracting user query from simple messages."""
callback = HindsightCallback()
messages = [
{"role": "system", "content": "You are helpful."},
{"role": "user", "content": "What is the capital of France?"},
]
query = callback._extract_user_query(messages)
assert query == "What is the capital of France?"
def test_extract_user_query_from_last_user_message(self):
"""Test extracting query from last user message."""
callback = HindsightCallback()
messages = [
{"role": "user", "content": "First question"},
{"role": "assistant", "content": "First answer"},
{"role": "user", "content": "Second question"},
]
query = callback._extract_user_query(messages)
assert query == "Second question"
def test_extract_user_query_structured_content(self):
"""Test extracting query from structured content (vision)."""
callback = HindsightCallback()
messages = [
{
"role": "user",
"content": [
{"type": "text", "text": "What's in this image?"},
{"type": "image_url", "image_url": {"url": "http://example.com/img.png"}},
],
},
]
query = callback._extract_user_query(messages)
assert query == "What's in this image?"
def test_extract_user_query_multiple_text_parts(self):
"""Test extracting query with multiple text parts."""
callback = HindsightCallback()
messages = [
{
"role": "user",
"content": [
{"type": "text", "text": "First part."},
{"type": "text", "text": "Second part."},
],
},
]
query = callback._extract_user_query(messages)
assert query == "First part. Second part."
def test_format_memories(self):
"""Test formatting memories into context string."""
callback = HindsightCallback()
config = HindsightConfig(bank_id="test", max_memories=10, verbose=False)
memories = [
{"text": "User likes Python", "fact_type": "world", "weight": 0.95},
{"text": "User works at Google", "fact_type": "world", "weight": 0.8},
]
formatted = callback._format_memories(memories, config)
assert "Relevant Memories" in formatted
assert "User likes Python" in formatted
assert "User works at Google" in formatted
assert "[WORLD]" in formatted
def test_format_memories_with_verbose(self):
"""Test formatting memories with verbose mode shows weights."""
callback = HindsightCallback()
config = HindsightConfig(bank_id="test", max_memories=10, verbose=True)
memories = [
{"text": "User likes Python", "fact_type": "world", "weight": 0.95},
]
formatted = callback._format_memories(memories, config)
assert "relevance: 0.95" in formatted
def test_inject_memories_as_system_message(self):
"""Test injecting memories as system message."""
callback = HindsightCallback()
config = HindsightConfig(
bank_id="test",
injection_mode=MemoryInjectionMode.SYSTEM_MESSAGE,
)
messages = [
{"role": "user", "content": "Hello"},
]
memory_context = "# Relevant Memories\n1. User is John"
result = callback._inject_memories_into_messages(messages, memory_context, config)
assert len(result) == 2
assert result[0]["role"] == "system"
assert "Relevant Memories" in result[0]["content"]
assert result[1]["role"] == "user"
def test_inject_memories_prepend_to_existing_system(self):
"""Test injecting memories appends to existing system message."""
callback = HindsightCallback()
config = HindsightConfig(
bank_id="test",
injection_mode=MemoryInjectionMode.SYSTEM_MESSAGE,
)
messages = [
{"role": "system", "content": "You are helpful."},
{"role": "user", "content": "Hello"},
]
memory_context = "# Relevant Memories\n1. User is John"
result = callback._inject_memories_into_messages(messages, memory_context, config)
assert len(result) == 2
assert result[0]["role"] == "system"
assert "You are helpful." in result[0]["content"]
assert "Relevant Memories" in result[0]["content"]
def test_inject_memories_prepend_user_mode(self):
"""Test injecting memories in prepend_user mode."""
callback = HindsightCallback()
config = HindsightConfig(
bank_id="test",
injection_mode=MemoryInjectionMode.PREPEND_USER,
)
messages = [
{"role": "user", "content": "What's my name?"},
]
memory_context = "# Relevant Memories\n1. User is John"
result = callback._inject_memories_into_messages(messages, memory_context, config)
assert len(result) == 1
assert result[0]["role"] == "user"
assert "Relevant Memories" in result[0]["content"]
assert "What's my name?" in result[0]["content"]
def test_should_skip_model_exact_match(self):
"""Test model exclusion with exact match."""
callback = HindsightCallback()
config = HindsightConfig(
bank_id="test",
excluded_models=["gpt-3.5-turbo"],
)
assert callback._should_skip_model("gpt-3.5-turbo", config) is True
assert callback._should_skip_model("gpt-4", config) is False
def test_should_skip_model_wildcard(self):
"""Test model exclusion with wildcard pattern."""
callback = HindsightCallback()
config = HindsightConfig(
bank_id="test",
excluded_models=["gpt-3.5*", "claude-instant-*"],
)
assert callback._should_skip_model("gpt-3.5-turbo", config) is True
assert callback._should_skip_model("gpt-3.5-turbo-16k", config) is True
assert callback._should_skip_model("claude-instant-1.2", config) is True
assert callback._should_skip_model("gpt-4", config) is False
assert callback._should_skip_model("claude-3-opus", config) is False
class TestDeduplication:
"""Tests for conversation deduplication."""
def setup_method(self):
"""Reset state before each test."""
cleanup()
def teardown_method(self):
"""Clean up after each test."""
cleanup()
def test_compute_conversation_hash(self):
"""Test computing conversation hash."""
callback = HindsightCallback()
hash1 = callback._compute_conversation_hash("Hello", "Hi there!")
hash2 = callback._compute_conversation_hash("Hello", "Hi there!")
hash3 = callback._compute_conversation_hash("Hello", "Different response")
# Same content should produce same hash
assert hash1 == hash2
# Different content should produce different hash
assert hash1 != hash3
def test_compute_conversation_hash_case_insensitive(self):
"""Test that hash is case insensitive."""
callback = HindsightCallback()
hash1 = callback._compute_conversation_hash("HELLO", "HI THERE!")
hash2 = callback._compute_conversation_hash("hello", "hi there!")
assert hash1 == hash2
def test_is_duplicate_first_time(self):
"""Test first occurrence is not a duplicate."""
callback = HindsightCallback()
result = callback._is_duplicate("abc123")
assert result is False
def test_is_duplicate_second_time(self):
"""Test second occurrence is a duplicate."""
callback = HindsightCallback()
callback._is_duplicate("abc123") # First time
result = callback._is_duplicate("abc123") # Second time
assert result is True
def test_is_duplicate_different_hashes(self):
"""Test different hashes are not duplicates."""
callback = HindsightCallback()
callback._is_duplicate("abc123")
result = callback._is_duplicate("xyz789")
assert result is False
class TestContextManager:
"""Tests for the hindsight_memory context manager."""
def setup_method(self):
"""Reset state before each test."""
cleanup()
def teardown_method(self):
"""Clean up after each test."""
cleanup()
def test_context_manager_enables_and_disables(self):
"""Test context manager enables and disables correctly."""
from hindsight_litellm import hindsight_memory
assert is_enabled() is False
with hindsight_memory(bank_id="test-agent"):
assert is_enabled() is True
assert get_config().bank_id == "test-agent"
assert is_enabled() is False
def test_context_manager_restores_previous_config(self):
"""Test context manager restores previous configuration."""
from hindsight_litellm import hindsight_memory
# Set up initial config
configure(bank_id="original-agent")
enable()
assert get_config().bank_id == "original-agent"
# Use context manager with different config
with hindsight_memory(bank_id="temporary-agent"):
assert get_config().bank_id == "temporary-agent"
# Should restore original config
assert get_config().bank_id == "original-agent"
assert is_enabled() is True
def test_context_manager_with_fact_types(self):
"""Test context manager with fact_types parameter."""
from hindsight_litellm import hindsight_memory
with hindsight_memory(bank_id="test-agent", fact_types=["world", "opinion"]):
config = get_config()
assert config.fact_types == ["world", "opinion"]
class TestFactTypes:
"""Tests for fact_types configuration."""
def setup_method(self):
"""Reset config before each test."""
reset_config()
def teardown_method(self):
"""Clean up after each test."""
cleanup()
def test_configure_with_fact_types(self):
"""Test configuring with fact_types."""
config = configure(
bank_id="test-agent",
fact_types=["world", "agent", "opinion"],
)
assert config.fact_types == ["world", "agent", "opinion"]
def test_configure_without_fact_types(self):
"""Test configuring without fact_types defaults to None."""
config = configure(bank_id="test-agent")
assert config.fact_types is None
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "hindsight-all"
version = "0.1.4"
version = "0.1.5"
description = "All-in-one package for Hindsight - Semantic memory system with personality-driven thinking"
readme = "README.md"
requires-python = ">=3.11"
File diff suppressed because it is too large Load Diff
+9
View File
@@ -0,0 +1,9 @@
{
"name": "hindsight",
"private": true,
"workspaces": [
"hindsight-clients/typescript",
"hindsight-control-plane",
"hindsight-docs"
]
}
+32
View File
@@ -0,0 +1,32 @@
#!/bin/bash
set -e
cd "$(dirname "$0")/../.."
if [ -z "$1" ]; then
echo "Usage: $0 VERSION [--model MODEL]"
echo ""
echo "Generate changelog entry for a release."
echo ""
echo "Examples:"
echo " $0 1.0.5"
echo " $0 v1.0.5"
echo " $0 1.0.5 --model gpt-4o"
exit 1
fi
if [ -z "$OPENAI_API_KEY" ]; then
ENV_FILE=".env"
if [ -f "$ENV_FILE" ]; then
echo "Loading environment from $ENV_FILE"
set -a
source "$ENV_FILE"
set +a
else
echo "Error: OPENAI_API_KEY not set and no .env file found"
exit 1
fi
fi
cd hindsight-dev
uv run generate-changelog "$@"
+2 -5
View File
@@ -13,13 +13,10 @@ if [ ! -f "$ROOT_DIR/.env" ]; then
fi
echo "🔨 Building TypeScript SDK first to ensure it's up to date..."
cd "$ROOT_DIR/hindsight-clients/typescript" || exit 1
npm run build
npm run build -w @vectorize-io/hindsight-client
echo "✅ SDK built successfully"
echo ""
cd "$ROOT_DIR/hindsight-control-plane" || exit 1
echo "🚀 Starting Control Plane (Next.js dev server)..."
if [ -f "$ROOT_DIR/.env" ]; then
echo "📄 Loading environment from $ROOT_DIR/.env"
@@ -33,4 +30,4 @@ fi
export HOSTNAME="${HINDSIGHT_CP_HOSTNAME:-0.0.0.0}"
# Run dev server
npm run dev
npm run dev -w hindsight-control-plane
+2 -13
View File
@@ -7,22 +7,11 @@ set -e
# Get the project root directory
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
DOCS_DIR="$PROJECT_ROOT/hindsight-docs"
cd "$PROJECT_ROOT" || exit 1
echo "Starting documentation server..."
echo "Documentation directory: $DOCS_DIR"
# Check if node_modules exists
if [ ! -d "$DOCS_DIR/node_modules" ]; then
echo "Installing documentation dependencies..."
cd "$DOCS_DIR"
npm install
fi
# Start the Docusaurus dev server
cd "$DOCS_DIR"
echo ""
echo "Starting Docusaurus development server..."
echo "Documentation will be available at: http://localhost:3000"
echo ""
npm run start
npm run start -w hindsight-docs
+2 -2
View File
@@ -65,7 +65,7 @@ fi
print_info "Updating version in all components..."
# Update Python packages
PYTHON_PACKAGES=("hindsight-api" "hindsight-dev" "hindsight-dev/benchmarks" "hindsight")
PYTHON_PACKAGES=("hindsight-api" "hindsight-dev" "hindsight-dev/benchmarks" "hindsight" "hindsight-integrations/litellm")
for package in "${PYTHON_PACKAGES[@]}"; do
PYPROJECT_FILE="$package/pyproject.toml"
if [ -f "$PYPROJECT_FILE" ]; then
@@ -148,7 +148,7 @@ git add -A
git commit -m "Release v$VERSION
- Update version to $VERSION in all components
- Python packages: hindsight-api, hindsight-dev, hindsight-dev/benchmarks, hindsight-all
- Python packages: hindsight-api, hindsight-dev, hindsight-dev/benchmarks, hindsight-all, hindsight-litellm
- Python client: hindsight-clients/python
- TypeScript client: hindsight-clients/typescript
- Rust CLI: hindsight-cli
+798
View File
@@ -0,0 +1,798 @@
#!/usr/bin/env python3
"""
Documentation Example Tester
Tests code examples from documentation by running them directly.
Uses deterministic transformations (no LLM) for test generation.
LLM is only used to analyze failures and determine if they're real doc bugs.
Usage:
python scripts/test-doc-examples.py
Environment variables:
OPENAI_API_KEY: Required for failure analysis
HINDSIGHT_API_URL: URL of running Hindsight server (default: http://localhost:8888)
"""
import os
import re
import sys
import site
import json
import glob
import subprocess
import tempfile
import traceback
import uuid
from dataclasses import dataclass, field
from typing import Optional
from concurrent.futures import ThreadPoolExecutor, as_completed
import threading
from openai import OpenAI
# Thread-safe print
print_lock = threading.Lock()
def safe_print(*args, **kwargs):
with print_lock:
print(*args, **kwargs)
sys.stdout.flush()
@dataclass
class CodeExample:
file_path: str
language: str
code: str
context: str
line_number: int
@dataclass
class TestResult:
example: CodeExample
success: bool
output: str
error: Optional[str] = None
transformed_code: Optional[str] = None
skip_reason: Optional[str] = None
@dataclass
class TestReport:
total: int = 0
passed: int = 0
failed: int = 0
skipped: int = 0
results: list[TestResult] = field(default_factory=list)
def add_result(self, result: TestResult):
self.total += 1
self.results.append(result)
if result.skip_reason:
self.skipped += 1
elif result.success:
self.passed += 1
else:
self.failed += 1
# =============================================================================
# STEP 1: Extract code blocks from markdown
# =============================================================================
def find_markdown_files(repo_root: str) -> list[str]:
"""Find all markdown files, excluding auto-generated docs."""
skip_patterns = [
"node_modules", ".git", "venv", "__pycache__",
"hindsight_client_api/docs", "hindsight-clients/typescript/docs",
"target/", "dist/",
]
md_files = []
for pattern in ["*.md", "**/*.md"]:
for f in glob.glob(os.path.join(repo_root, pattern), recursive=True):
if os.path.islink(f):
continue
if any(skip in f for skip in skip_patterns):
continue
md_files.append(f)
return sorted(set(md_files))
def extract_code_blocks(file_path: str) -> list[CodeExample]:
"""Extract code blocks from a markdown file."""
with open(file_path, "r") as f:
content = f.read()
examples = []
pattern = r"```(\w+)\n(.*?)```"
for match in re.finditer(pattern, content, re.DOTALL):
language = match.group(1).lower()
code = match.group(2).strip()
line_number = content[:match.start()].count('\n') + 1
if language in ["python", "typescript", "javascript", "bash", "sh"]:
start = max(0, match.start() - 150)
end = min(len(content), match.end() + 150)
context = content[start:end]
examples.append(CodeExample(
file_path=file_path,
language=language,
code=code,
context=context,
line_number=line_number
))
return examples
# =============================================================================
# STEP 2: Determine if example should be skipped (no LLM needed)
# =============================================================================
def should_skip(code: str, language: str) -> Optional[str]:
"""Determine if example should be skipped. Returns reason or None."""
code_lower = code.lower().strip()
# Installation/setup commands
if language in ["bash", "sh"]:
if code_lower.startswith(("pip install", "npm install", "yarn add", "uv pip", "cargo install", "curl ", "wget ")):
return "Installation command"
if "docker" in code_lower or "docker-compose" in code_lower:
return "Docker command"
if code_lower.startswith("helm "):
return "Helm command"
if code_lower.startswith(("cargo build", "cargo test")):
return "Cargo command"
if "pytest" in code_lower:
return "Test suite command"
if code_lower.startswith("git clone"):
return "Git clone"
if "./scripts/" in code_lower:
return "Development script"
if any(x in code_lower for x in ["npm run dev", "npm run start", "npm run build", "npm run deploy"]):
return "NPM script"
if code_lower.startswith("cd ") and not code_lower.startswith("cd /tmp"):
return "Directory change"
if code_lower.startswith("export "):
return "Environment variable"
# Config files
if language in ["yaml", "toml", "json", "env"]:
return "Configuration file"
# Too short
if len(code.strip()) < 20:
return "Too short"
return None
# =============================================================================
# STEP 3: Transform code (LLM adds setup/cleanup around sacred doc code)
# =============================================================================
def transform_code(client: OpenAI, example: CodeExample, hindsight_url: str, cli_available: bool, model: str) -> tuple[str, Optional[str]]:
"""Use LLM to add setup/cleanup around doc code. The doc code itself is not modified."""
bank_id = f"doc-test-{uuid.uuid4()}"
# Skip CLI examples if CLI not available
if not cli_available and example.language in ["bash", "sh"] and "hindsight " in example.code.lower():
return "", "CLI not available"
if example.language == "python":
output_format = f"""Output a Python script (.py):
- The doc code goes inside a try block
- Add cleanup in finally: requests.delete("{hindsight_url}/v1/default/banks/{bank_id}")
- End with: print("TEST PASSED")
- Do NOT use async/await - the Hindsight client is synchronous"""
elif example.language in ["typescript", "javascript"]:
output_format = f"""Output a JavaScript ES module (.mjs):
- Remove TypeScript type annotations
- Wrap in async IIFE: (async () => {{ try {{ ... }} finally {{ ... }} }})();
- Add cleanup in finally: await fetch("{hindsight_url}/v1/default/banks/{bank_id}", {{ method: "DELETE" }})
- End with: console.log("TEST PASSED")"""
elif example.language in ["bash", "sh"]:
output_format = f"""Output a Bash script:
- Start with #!/bin/bash and set -e
- Use trap for cleanup: curl -s -X DELETE "{hindsight_url}/v1/default/banks/{bank_id}"
- End with: echo "TEST PASSED" """
else:
return "", f"Unsupported language: {example.language}"
prompt = f"""The documentation code below is the TEST CASE. Your job is to make it runnable.
DOCUMENTATION CODE ({example.language}):
```
{example.code}
```
RULES:
1. The doc code is SACRED - do not modify its logic, method calls, or parameters
2. You MAY add setup BEFORE it:
- Import statements the code assumes exist
- Object instantiation (e.g., if code uses 'client.foo()', create the client first)
- Variable definitions
3. You MAY add cleanup AFTER it
4. Replace placeholder values:
- URLs like localhost:8888 → {hindsight_url}
- Bank IDs like "my-bank", "demo", <bank_id> → "{bank_id}"
- Placeholder IDs like <entity_id>, <document_id> → "test-id"
{output_format}
Output ONLY the complete runnable code, no explanation."""
is_reasoning = model.startswith(("o1", "o3"))
kwargs = {
"model": model,
"messages": [{"role": "user", "content": prompt}],
}
if is_reasoning:
kwargs["max_completion_tokens"] = 4000
else:
kwargs["temperature"] = 0
kwargs["max_tokens"] = 4000
try:
response = client.chat.completions.create(**kwargs)
script = response.choices[0].message.content
# Clean up markdown code blocks if present
script = re.sub(r'^```\w*\n', '', script)
script = re.sub(r'\n```$', '', script)
script = script.strip()
return script, None
except Exception as e:
return "", f"Transform failed: {e}"
# =============================================================================
# STEP 4: Run tests
# =============================================================================
def get_python_path() -> str:
"""Get PYTHONPATH that includes all installed packages."""
paths = []
# Add virtual environment site-packages if in a venv
if hasattr(sys, 'real_prefix') or (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix):
# We're in a virtual environment
venv_site = os.path.join(sys.prefix, 'lib', f'python{sys.version_info.major}.{sys.version_info.minor}', 'site-packages')
if os.path.exists(venv_site):
paths.append(venv_site)
# Add system site-packages
paths.extend(site.getsitepackages())
# Add user site-packages
user_site = site.getusersitepackages()
if user_site and os.path.exists(user_site):
paths.append(user_site)
# Add existing PYTHONPATH
existing = os.environ.get("PYTHONPATH", "")
if existing:
paths.append(existing)
return ":".join(paths)
def run_python(script: str, timeout: int = 60) -> tuple[bool, str, Optional[str]]:
"""Run Python script."""
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
f.write(script)
f.flush()
try:
pythonpath = get_python_path()
result = subprocess.run(
[sys.executable, f.name],
capture_output=True, text=True, timeout=timeout,
env={**os.environ, "PYTHONPATH": pythonpath}
)
output = result.stdout + result.stderr
if "TEST PASSED" in output:
return True, output, None
return result.returncode == 0, output, result.stderr if result.returncode != 0 else None
except subprocess.TimeoutExpired:
return False, "", "Timeout"
except Exception as e:
return False, "", str(e)
finally:
os.unlink(f.name)
def run_javascript(script: str, timeout: int = 60) -> tuple[bool, str, Optional[str]]:
"""Run JavaScript script."""
with tempfile.NamedTemporaryFile(mode='w', suffix='.mjs', delete=False, dir='/tmp') as f:
f.write(script)
f.flush()
try:
env = {**os.environ}
env["NODE_PATH"] = f"/tmp/node_modules:{env.get('NODE_PATH', '')}"
result = subprocess.run(
["node", f.name],
capture_output=True, text=True, timeout=timeout,
env=env, cwd="/tmp"
)
output = result.stdout + result.stderr
if "TEST PASSED" in output:
return True, output, None
return result.returncode == 0, output, result.stderr if result.returncode != 0 else None
except subprocess.TimeoutExpired:
return False, "", "Timeout"
except Exception as e:
return False, "", str(e)
finally:
os.unlink(f.name)
def run_bash(script: str, timeout: int = 60) -> tuple[bool, str, Optional[str]]:
"""Run bash script."""
with tempfile.NamedTemporaryFile(mode='w', suffix='.sh', delete=False) as f:
f.write(script)
f.flush()
os.chmod(f.name, 0o755)
try:
result = subprocess.run(
["bash", f.name],
capture_output=True, text=True, timeout=timeout
)
output = result.stdout + result.stderr
if "TEST PASSED" in output:
return True, output, None
return result.returncode == 0, output, result.stderr if result.returncode != 0 else None
except subprocess.TimeoutExpired:
return False, "", "Timeout"
except Exception as e:
return False, "", str(e)
finally:
os.unlink(f.name)
# =============================================================================
# STEP 5: Analyze failures with LLM
# =============================================================================
def get_source_context(example: CodeExample, repo_root: str) -> str:
"""Get relevant source code for failure analysis."""
parts = []
code_lower = example.code.lower()
if example.language == "python":
if "recall" in code_lower or "weight" in code_lower:
try:
with open(os.path.join(repo_root, "hindsight-clients/python/hindsight_client_api/models/recall_result.py")) as f:
parts.append("=== RecallResult Model ===\n" + f.read()[:2000])
except: pass
if "reflect" in code_lower:
try:
with open(os.path.join(repo_root, "hindsight-clients/python/hindsight_client_api/models/reflect_response.py")) as f:
parts.append("=== ReflectResponse Model ===\n" + f.read()[:2000])
except: pass
try:
with open(os.path.join(repo_root, "hindsight-clients/python/hindsight_client/__init__.py")) as f:
parts.append("=== Hindsight Client ===\n" + f.read()[:3000])
except: pass
elif example.language in ["typescript", "javascript"]:
try:
with open(os.path.join(repo_root, "hindsight-clients/typescript/src/index.ts")) as f:
parts.append("=== TypeScript Client ===\n" + f.read()[:4000])
except: pass
elif example.language in ["bash", "sh"]:
try:
with open(os.path.join(repo_root, "hindsight-cli/src/main.rs")) as f:
lines = f.read().split('\n')[:350]
parts.append("=== CLI Commands ===\n" + '\n'.join(lines))
except: pass
return "\n\n".join(parts)
def get_doc_context(example: CodeExample) -> str:
"""Get the full documentation context around the failing code example."""
try:
with open(example.file_path, "r") as f:
content = f.read()
# Find the code block and get surrounding context (500 chars before/after)
# This gives us the explanatory text around the code
code_start = content.find(example.code[:50]) # Find by first 50 chars
if code_start == -1:
code_start = example.line_number * 50 # Rough estimate
start = max(0, code_start - 500)
end = min(len(content), code_start + len(example.code) + 500)
return content[start:end]
except:
return example.context # Fall back to the small context we already have
def analyze_failure(client: OpenAI, result: TestResult, repo_root: str, model: str) -> dict:
"""Use LLM to determine if failure is a real doc bug."""
source = get_source_context(result.example, repo_root)
doc_context = get_doc_context(result.example)
prompt = f"""Analyze this documentation test failure.
## Documentation File: {result.example.file_path}
### Documentation Context (text around the code example)
```markdown
{doc_context}
```
### The Code Example Being Tested (line {result.example.line_number})
```{result.example.language}
{result.example.code}
```
## Error When Running
{result.error[:800] if result.error else "Unknown"}
## Transformed Test Code (what we actually ran)
```
{result.transformed_code[:1500] if result.transformed_code else "N/A"}
```
## Actual Source Code (ground truth - what the API really looks like)
{source[:6000] if source else "Not available"}
## Your Task
Compare the DOCUMENTATION against the ACTUAL SOURCE CODE.
1. Does the documentation show something that doesn't exist in the source code?
- Wrong method names?
- Wrong attribute names (e.g., .weight when there's no weight field)?
- Wrong CLI commands?
- Wrong parameters?
2. Or is the documentation correct, but our test transformation/execution failed?
- Missing imports we didn't add?
- Environment issues?
- Timing/race conditions?
Respond JSON:
{{
"is_doc_bug": true/false,
"confidence": "high/medium/low",
"reason": "brief explanation of what's wrong",
"fix": "if doc bug, what should the doc say instead"
}}"""
is_reasoning = model.startswith(("o1", "o3"))
kwargs = {
"model": model,
"messages": [{"role": "user", "content": prompt}],
"response_format": {"type": "json_object"},
}
if is_reasoning:
kwargs["max_completion_tokens"] = 2000
else:
kwargs["temperature"] = 0
try:
response = client.chat.completions.create(**kwargs)
return json.loads(response.choices[0].message.content)
except Exception as e:
return {"is_doc_bug": True, "confidence": "low", "reason": str(e)}
# =============================================================================
# Main test runner
# =============================================================================
def test_example(example: CodeExample, openai_client: OpenAI, hindsight_url: str, cli_available: bool, model: str) -> TestResult:
"""Test a single code example."""
# Check if should skip
skip = should_skip(example.code, example.language)
if skip:
return TestResult(example=example, success=True, output="", skip_reason=skip)
# Transform using LLM
try:
transformed, skip = transform_code(openai_client, example, hindsight_url, cli_available, model)
if skip:
return TestResult(example=example, success=True, output="", skip_reason=skip)
if not transformed:
return TestResult(example=example, success=True, output="", skip_reason="Transform returned empty")
# Run based on language
if example.language == "python":
success, output, error = run_python(transformed)
elif example.language in ["typescript", "javascript"]:
success, output, error = run_javascript(transformed)
elif example.language in ["bash", "sh"]:
success, output, error = run_bash(transformed)
else:
return TestResult(example=example, success=True, output="", skip_reason=f"Unsupported: {example.language}")
return TestResult(
example=example,
success=success,
output=output,
error=error,
transformed_code=transformed
)
except Exception as e:
return TestResult(
example=example,
success=False,
output="",
error=f"Transform error: {e}\n{traceback.format_exc()}"
)
def check_cli_available() -> bool:
"""Check if hindsight CLI is available."""
try:
result = subprocess.run(["hindsight", "--version"], capture_output=True, timeout=5)
return result.returncode == 0
except:
return False
def check_dependencies() -> dict[str, bool]:
"""Check which dependencies are available for doc tests."""
deps = {}
# Check Python packages
python_packages = [
("hindsight_client", "Hindsight Python client"),
("hindsight_litellm", "Hindsight LiteLLM integration"),
("hindsight_openai", "Hindsight OpenAI integration"),
("anthropic", "Anthropic SDK"),
("openai", "OpenAI SDK"),
]
for module, name in python_packages:
try:
__import__(module)
deps[module] = True
except ImportError:
deps[module] = False
return deps
def print_dependency_status(deps: dict[str, bool]):
"""Print dependency availability status."""
print("\n=== Dependencies ===")
for name, available in deps.items():
status = "" if available else ""
print(f" {status} {name}")
# Print PYTHONPATH for debugging
pythonpath = get_python_path()
print(f"\nPYTHONPATH: {pythonpath[:100]}..." if len(pythonpath) > 100 else f"\nPYTHONPATH: {pythonpath}")
print(f"Python: {sys.executable}")
print(f"Prefix: {sys.prefix}")
print()
def main():
sys.stdout.reconfigure(line_buffering=True)
openai_key = os.environ.get("OPENAI_API_KEY")
if not openai_key:
print("ERROR: OPENAI_API_KEY required")
sys.exit(1)
hindsight_url = os.environ.get("HINDSIGHT_API_URL", "http://localhost:8888")
model = os.environ.get("DOC_TEST_MODEL", "gpt-4o")
# Find repo root - go up from script location
script_path = os.path.abspath(__file__)
repo_root = os.path.dirname(os.path.dirname(script_path))
# If running from a subdirectory (like hindsight-api), detect and fix
if not os.path.exists(os.path.join(repo_root, "hindsight-docs")):
# Try going up one more level
repo_root = os.path.dirname(repo_root)
if not os.path.exists(os.path.join(repo_root, "hindsight-docs")):
# Fall back to REPO_ROOT env var or cwd
repo_root = os.environ.get("REPO_ROOT", os.getcwd())
print(f"Repo: {repo_root}")
print(f"API: {hindsight_url}")
print(f"Model: {model}")
# Check CLI
cli_available = check_cli_available()
print(f"CLI: {'available' if cli_available else 'not available'}")
# Check and print dependencies
deps = check_dependencies()
print_dependency_status(deps)
# Warn if critical dependencies are missing
if not deps.get("hindsight_client"):
print("WARNING: hindsight_client not available - Python examples will fail")
print(" Install with: pip install hindsight-client or uv pip install <path-to-client>")
# Check API health
try:
import urllib.request
urllib.request.urlopen(f"{hindsight_url}/health", timeout=5)
print("API: healthy")
except Exception as e:
print(f"API: WARNING - {e}")
# Initialize OpenAI client early (needed for transforms and analysis)
client = OpenAI(api_key=openai_key)
# Find and extract examples
md_files = find_markdown_files(repo_root)
print(f"\nFound {len(md_files)} markdown files")
all_examples = []
for md_file in md_files:
examples = extract_code_blocks(md_file)
if examples:
all_examples.extend(examples)
print(f"Found {len(all_examples)} code examples")
# Run tests
report = TestReport()
max_workers = int(os.environ.get("MAX_WORKERS", "4")) # Lower default since LLM calls are slower
print(f"\nRunning tests with {max_workers} workers...")
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = {executor.submit(test_example, ex, client, hindsight_url, cli_available, model): ex for ex in all_examples}
for future in as_completed(futures):
result = future.result()
report.add_result(result)
status = "SKIP" if result.skip_reason else ("PASS" if result.success else "FAIL")
safe_print(f" [{status}] {result.example.file_path}:{result.example.line_number}")
# Print summary
print("\n" + "=" * 60)
print(f"Total: {report.total} | Pass: {report.passed} | Fail: {report.failed} | Skip: {report.skipped}")
print("=" * 60)
# Analyze failures with LLM
failures = [r for r in report.results if not r.success and not r.skip_reason]
if failures:
print(f"\n=== Analyzing {len(failures)} failures (parallel) ===")
doc_bugs = []
test_issues = []
results_lock = threading.Lock()
completed = [0] # Use list for mutable counter in closure
def analyze_one(result: TestResult) -> None:
analysis = analyze_failure(client, result, repo_root, model)
entry = {
"file": result.example.file_path,
"line": result.example.line_number,
"error": result.error[:200] if result.error else "",
"analysis": analysis
}
with results_lock:
completed[0] += 1
idx = completed[0]
if analysis.get("is_doc_bug", True):
doc_bugs.append(entry)
safe_print(f" [{idx}/{len(failures)}] {result.example.file_path}:{result.example.line_number}")
safe_print(f" → DOC BUG: {analysis.get('reason', '')[:50]}")
else:
test_issues.append(entry)
safe_print(f" [{idx}/{len(failures)}] {result.example.file_path}:{result.example.line_number}")
safe_print(f" → Test issue: {analysis.get('reason', '')[:50]}")
# Run analysis in parallel (limit concurrency to avoid rate limits)
with ThreadPoolExecutor(max_workers=10) as executor:
futures = [executor.submit(analyze_one, result) for result in failures]
for future in as_completed(futures):
try:
future.result()
except Exception as e:
safe_print(f" Analysis error: {e}")
# Write summary
print(f"\n=== RESULTS ===")
print(f"Documentation bugs: {len(doc_bugs)}")
print(f"Test/CI issues: {len(test_issues)}")
if doc_bugs:
print(f"\n--- Documentation Bugs ---")
for bug in doc_bugs:
print(f" {bug['file']}:{bug['line']}")
print(f" Reason: {bug['analysis'].get('reason', 'Unknown')}")
if bug['analysis'].get('fix'):
print(f" Fix: {bug['analysis']['fix']}")
if test_issues:
print(f"\n--- Test/CI Issues (not doc bugs) ---")
for issue in test_issues:
print(f" {issue['file']}:{issue['line']}")
print(f" Reason: {issue['analysis'].get('reason', 'Unknown')}")
# Write GitHub summary (include ALL failures for visibility)
write_summary(report, doc_bugs, test_issues)
# Exit code based on real doc bugs only
sys.exit(1 if doc_bugs else 0)
else:
print("\nAll tests passed!")
write_summary(report, [], [])
sys.exit(0)
def write_summary(report: TestReport, doc_bugs: list, test_issues: list):
"""Write GitHub Actions summary file."""
with open("/tmp/doc-test-summary.md", "w") as f:
# Header
status = "" if doc_bugs else ""
f.write(f"# {status} Documentation Test Results\n\n")
# Summary table
f.write(f"| Metric | Count |\n")
f.write(f"|--------|-------|\n")
f.write(f"| Total | {report.total} |\n")
f.write(f"| ✅ Passed | {report.passed} |\n")
f.write(f"| ❌ Failed | {report.failed} |\n")
f.write(f"| ⏭️ Skipped | {report.skipped} |\n\n")
if doc_bugs or test_issues:
f.write(f"| Category | Count |\n")
f.write(f"|----------|-------|\n")
f.write(f"| 🐛 Documentation Bugs | {len(doc_bugs)} |\n")
f.write(f"| ⚠️ Test/CI Issues | {len(test_issues)} |\n\n")
# Documentation bugs section
if doc_bugs:
f.write(f"## 🐛 Documentation Bugs ({len(doc_bugs)})\n\n")
f.write("These are real issues in the documentation that need to be fixed:\n\n")
for bug in doc_bugs:
file_short = bug['file'].split('/hindsight/')[-1] if '/hindsight/' in bug['file'] else bug['file']
f.write(f"### `{file_short}:{bug['line']}`\n")
f.write(f"- **Issue**: {bug['analysis'].get('reason', 'Unknown')}\n")
if bug['analysis'].get('fix'):
f.write(f"- **Suggested Fix**: {bug['analysis']['fix']}\n")
if bug.get('error'):
f.write(f"- **Error**: `{bug['error'][:150]}...`\n")
f.write("\n")
# Test/CI issues section
if test_issues:
f.write(f"## ⚠️ Test/CI Issues ({len(test_issues)})\n\n")
f.write("These failures are NOT documentation bugs - they're issues with the test setup or CI environment:\n\n")
for issue in test_issues:
file_short = issue['file'].split('/hindsight/')[-1] if '/hindsight/' in issue['file'] else issue['file']
f.write(f"### `{file_short}:{issue['line']}`\n")
f.write(f"- **Reason**: {issue['analysis'].get('reason', 'Unknown')}\n")
if issue.get('error'):
f.write(f"- **Error**: `{issue['error'][:150]}...`\n")
f.write("\n")
# No failures
if not doc_bugs and not test_issues:
if report.passed > 0:
f.write(f"All {report.passed} tests passed! ({report.skipped} skipped)\n")
else:
f.write(f"All {report.skipped} examples were skipped (install commands, docker, etc.)\n")
if __name__ == "__main__":
main()
Generated
+110 -118
View File
@@ -1141,7 +1141,7 @@ wheels = [
[[package]]
name = "hindsight-all"
version = "0.1.3"
version = "0.1.5"
source = { editable = "hindsight" }
dependencies = [
{ name = "hindsight-api" },
@@ -1165,7 +1165,7 @@ provides-extras = ["test"]
[[package]]
name = "hindsight-api"
version = "0.1.3"
version = "0.1.5"
source = { editable = "hindsight-api" }
dependencies = [
{ name = "alembic" },
@@ -1182,6 +1182,7 @@ dependencies = [
{ name = "opentelemetry-exporter-prometheus" },
{ name = "opentelemetry-instrumentation-fastapi" },
{ name = "opentelemetry-sdk" },
{ name = "pg0-embedded" },
{ name = "pgvector" },
{ name = "psycopg2-binary" },
{ name = "pydantic" },
@@ -1222,7 +1223,7 @@ requires-dist = [
{ name = "asyncpg", specifier = ">=0.29.0" },
{ name = "dateparser", specifier = ">=1.2.2" },
{ name = "fastapi", extras = ["standard"], specifier = ">=0.120.3" },
{ name = "fastmcp", specifier = ">=2.0.0" },
{ name = "fastmcp", specifier = ">=2.3.0" },
{ name = "filelock", marker = "extra == 'test'", specifier = ">=3.0.0" },
{ name = "google-genai", specifier = ">=1.0.0" },
{ name = "greenlet", specifier = ">=3.2.4" },
@@ -1233,6 +1234,7 @@ requires-dist = [
{ name = "opentelemetry-exporter-prometheus", specifier = ">=0.41b0" },
{ name = "opentelemetry-instrumentation-fastapi", specifier = ">=0.41b0" },
{ name = "opentelemetry-sdk", specifier = ">=1.20.0" },
{ name = "pg0-embedded", specifier = ">=0.1.0" },
{ name = "pgvector", specifier = ">=0.4.1" },
{ name = "psycopg2-binary", specifier = ">=2.9.11" },
{ name = "pydantic", specifier = ">=2.0.0" },
@@ -1243,11 +1245,11 @@ requires-dist = [
{ name = "python-dateutil", specifier = ">=2.8.0" },
{ name = "python-dotenv", specifier = ">=1.0.0" },
{ name = "rich", specifier = ">=13.0.0" },
{ name = "sentence-transformers", specifier = ">=3.0.0" },
{ name = "sentence-transformers", specifier = ">=3.0.0,<3.3.0" },
{ name = "sqlalchemy", specifier = ">=2.0.44" },
{ name = "tiktoken", specifier = ">=0.12.0" },
{ name = "torch", specifier = ">=2.0.0" },
{ name = "transformers", specifier = ">=4.30.0" },
{ name = "torch", specifier = ">=2.0.0,<2.6.0" },
{ name = "transformers", specifier = ">=4.30.0,<4.46.0" },
{ name = "uvicorn", specifier = ">=0.38.0" },
{ name = "wsproto", specifier = ">=1.0.0" },
]
@@ -1265,7 +1267,7 @@ dev = [
[[package]]
name = "hindsight-client"
version = "0.1.3"
version = "0.1.5"
source = { editable = "hindsight-clients/python" }
dependencies = [
{ name = "aiohttp" },
@@ -1297,7 +1299,7 @@ provides-extras = ["test"]
[[package]]
name = "hindsight-dev"
version = "0.1.3"
version = "0.1.5"
source = { editable = "hindsight-dev" }
dependencies = [
{ name = "hindsight-api" },
@@ -2095,77 +2097,69 @@ wheels = [
[[package]]
name = "nvidia-cublas-cu12"
version = "12.8.4.1"
version = "12.4.5.8"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/61/e24b560ab2e2eaeb3c839129175fb330dfcfc29e5203196e5541a4c44682/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:8ac4e771d5a348c551b2a426eda6193c19aa630236b418086020df5ba9667142", size = 594346921 },
{ url = "https://files.pythonhosted.org/packages/ae/71/1c91302526c45ab494c23f61c7a84aa568b8c1f9d196efa5993957faf906/nvidia_cublas_cu12-12.4.5.8-py3-none-manylinux2014_x86_64.whl", hash = "sha256:2fc8da60df463fdefa81e323eef2e36489e1c94335b5358bcb38360adf75ac9b", size = 363438805 },
]
[[package]]
name = "nvidia-cuda-cupti-cu12"
version = "12.8.90"
version = "12.4.127"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f8/02/2adcaa145158bf1a8295d83591d22e4103dbfd821bcaf6f3f53151ca4ffa/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea0cb07ebda26bb9b29ba82cda34849e73c166c18162d3913575b0c9db9a6182", size = 10248621 },
{ url = "https://files.pythonhosted.org/packages/67/42/f4f60238e8194a3106d06a058d494b18e006c10bb2b915655bd9f6ea4cb1/nvidia_cuda_cupti_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:9dec60f5ac126f7bb551c055072b69d85392b13311fcc1bcda2202d172df30fb", size = 13813957 },
]
[[package]]
name = "nvidia-cuda-nvrtc-cu12"
version = "12.8.93"
version = "12.4.127"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/05/6b/32f747947df2da6994e999492ab306a903659555dddc0fbdeb9d71f75e52/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:a7756528852ef889772a84c6cd89d41dfa74667e24cca16bb31f8f061e3e9994", size = 88040029 },
{ url = "https://files.pythonhosted.org/packages/2c/14/91ae57cd4db3f9ef7aa99f4019cfa8d54cb4caa7e00975df6467e9725a9f/nvidia_cuda_nvrtc_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a178759ebb095827bd30ef56598ec182b85547f1508941a3d560eb7ea1fbf338", size = 24640306 },
]
[[package]]
name = "nvidia-cuda-runtime-cu12"
version = "12.8.90"
version = "12.4.127"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0d/9b/a997b638fcd068ad6e4d53b8551a7d30fe8b404d6f1804abf1df69838932/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90", size = 954765 },
{ url = "https://files.pythonhosted.org/packages/ea/27/1795d86fe88ef397885f2e580ac37628ed058a92ed2c39dc8eac3adf0619/nvidia_cuda_runtime_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:64403288fa2136ee8e467cdc9c9427e0434110899d07c779f25b5c068934faa5", size = 883737 },
]
[[package]]
name = "nvidia-cudnn-cu12"
version = "9.10.2.21"
version = "9.1.0.70"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nvidia-cublas-cu12" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467 },
{ url = "https://files.pythonhosted.org/packages/9f/fd/713452cd72343f682b1c7b9321e23829f00b842ceaedcda96e742ea0b0b3/nvidia_cudnn_cu12-9.1.0.70-py3-none-manylinux2014_x86_64.whl", hash = "sha256:165764f44ef8c61fcdfdfdbe769d687e06374059fbb388b6c89ecb0e28793a6f", size = 664752741 },
]
[[package]]
name = "nvidia-cufft-cu12"
version = "11.3.3.83"
version = "11.2.1.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nvidia-nvjitlink-cu12" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695 },
]
[[package]]
name = "nvidia-cufile-cu12"
version = "1.13.1.3"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bb/fe/1bcba1dfbfb8d01be8d93f07bfc502c93fa23afa6fd5ab3fc7c1df71038a/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d069003be650e131b21c932ec3d8969c1715379251f8d23a1860554b1cb24fc", size = 1197834 },
{ url = "https://files.pythonhosted.org/packages/27/94/3266821f65b92b3138631e9c8e7fe1fb513804ac934485a8d05776e1dd43/nvidia_cufft_cu12-11.2.1.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f083fc24912aa410be21fa16d157fed2055dab1cc4b6934a0e03cba69eb242b9", size = 211459117 },
]
[[package]]
name = "nvidia-curand-cu12"
version = "10.3.9.90"
version = "10.3.5.147"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fb/aa/6584b56dc84ebe9cf93226a5cde4d99080c8e90ab40f0c27bda7a0f29aa1/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:b32331d4f4df5d6eefa0554c565b626c7216f87a06a4f56fab27c3b68a830ec9", size = 63619976 },
{ url = "https://files.pythonhosted.org/packages/8a/6d/44ad094874c6f1b9c654f8ed939590bdc408349f137f9b98a3a23ccec411/nvidia_curand_cu12-10.3.5.147-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a88f583d4e0bb643c49743469964103aa59f7f708d862c3ddb0fc07f851e3b8b", size = 56305206 },
]
[[package]]
name = "nvidia-cusolver-cu12"
version = "11.7.3.90"
version = "11.6.1.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nvidia-cublas-cu12" },
@@ -2173,58 +2167,42 @@ dependencies = [
{ name = "nvidia-nvjitlink-cu12" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905 },
{ url = "https://files.pythonhosted.org/packages/3a/e1/5b9089a4b2a4790dfdea8b3a006052cfecff58139d5a4e34cb1a51df8d6f/nvidia_cusolver_cu12-11.6.1.9-py3-none-manylinux2014_x86_64.whl", hash = "sha256:19e33fa442bcfd085b3086c4ebf7e8debc07cfe01e11513cc6d332fd918ac260", size = 127936057 },
]
[[package]]
name = "nvidia-cusparse-cu12"
version = "12.5.8.93"
version = "12.3.1.170"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nvidia-nvjitlink-cu12" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466 },
]
[[package]]
name = "nvidia-cusparselt-cu12"
version = "0.7.1"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/56/79/12978b96bd44274fe38b5dde5cfb660b1d114f70a65ef962bcbbed99b549/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623", size = 287193691 },
{ url = "https://files.pythonhosted.org/packages/db/f7/97a9ea26ed4bbbfc2d470994b8b4f338ef663be97b8f677519ac195e113d/nvidia_cusparse_cu12-12.3.1.170-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ea4f11a2904e2a8dc4b1833cc1b5181cde564edd0d5cd33e3c168eff2d1863f1", size = 207454763 },
]
[[package]]
name = "nvidia-nccl-cu12"
version = "2.27.5"
version = "2.21.5"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6e/89/f7a07dc961b60645dbbf42e80f2bc85ade7feb9a491b11a1e973aa00071f/nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ad730cf15cb5d25fe849c6e6ca9eb5b76db16a80f13f425ac68d8e2e55624457", size = 322348229 },
{ url = "https://files.pythonhosted.org/packages/df/99/12cd266d6233f47d00daf3a72739872bdc10267d0383508b0b9c84a18bb6/nvidia_nccl_cu12-2.21.5-py3-none-manylinux2014_x86_64.whl", hash = "sha256:8579076d30a8c24988834445f8d633c697d42397e92ffc3f63fa26766d25e0a0", size = 188654414 },
]
[[package]]
name = "nvidia-nvjitlink-cu12"
version = "12.8.93"
version = "12.4.127"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f6/74/86a07f1d0f42998ca31312f998bd3b9a7eff7f52378f4f270c8679c77fb9/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88", size = 39254836 },
]
[[package]]
name = "nvidia-nvshmem-cu12"
version = "3.3.20"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/6c/99acb2f9eb85c29fc6f3a7ac4dccfd992e22666dd08a642b303311326a97/nvidia_nvshmem_cu12-3.3.20-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d00f26d3f9b2e3c3065be895e3059d6479ea5c638a3f38c9fec49b1b9dd7c1e5", size = 124657145 },
{ url = "https://files.pythonhosted.org/packages/ff/ff/847841bacfbefc97a00036e0fce5a0f086b640756dc38caea5e1bb002655/nvidia_nvjitlink_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:06b3b9b25bf3f8af351d664978ca26a16d2c5127dbd53c0497e28d1fb9611d57", size = 21066810 },
]
[[package]]
name = "nvidia-nvtx-cu12"
version = "12.8.90"
version = "12.4.127"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a2/eb/86626c1bbc2edb86323022371c39aa48df6fd8b0a1647bc274577f72e90b/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f", size = 89954 },
{ url = "https://files.pythonhosted.org/packages/87/20/199b8713428322a2f22b722c62b8cc278cc53dffa9705d744484b5035ee9/nvidia_nvtx_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:781e950d9b9f60d8241ccea575b32f5105a5baf4c2351cab5256a24869f12a1a", size = 99144 },
]
[[package]]
@@ -2526,6 +2504,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9a/70/875f4a23bfc4731703a5835487d0d2fb999031bd415e7d17c0ae615c18b7/pathvalidate-3.3.1-py3-none-any.whl", hash = "sha256:5263baab691f8e1af96092fa5137ee17df5bdfbd6cff1fcac4d6ef4bc2e1735f", size = 24305 },
]
[[package]]
name = "pg0-embedded"
version = "0.10.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5d/2a/26aed143a5bc4396321c5016c3e7574d596b94122e4de555ec21ebd1f135/pg0_embedded-0.10.1.tar.gz", hash = "sha256:afbfa9e050bec48587d55410e2a93694390c8fb50e1bbab2ac22a36a7eec146d", size = 17619 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/08/61/03f089d9d812e782db200f330e7cf35903834d7f106a2d650bb92c73c8d7/pg0_embedded-0.10.1-py3-none-macosx_14_0_arm64.whl", hash = "sha256:913010ad1a2321367f47cdc907a1639537af36597cd9b61549a341d8da3f249b", size = 13073670 },
{ url = "https://files.pythonhosted.org/packages/72/03/d6e64310c59da880cda4931216f328f72a014c19722fa754b1a0d6422cbb/pg0_embedded-0.10.1-py3-none-manylinux_2_35_aarch64.whl", hash = "sha256:b6f2fc089e844a67dbc1b16899f582ca857744bdd7842b4166a6eecce807a5af", size = 14785516 },
{ url = "https://files.pythonhosted.org/packages/7b/25/a2f84a1c142b48c2a41f14765721650076e2be56762b5ad7cd72ae32e5e4/pg0_embedded-0.10.1-py3-none-manylinux_2_35_x86_64.whl", hash = "sha256:f2ae4ed1ce0aa42a310f20b1ea47dd6091f0f74106b7ded39e9c089e7f80ab25", size = 15224456 },
{ url = "https://files.pythonhosted.org/packages/d0/a8/64963aef0d6ae720b88068441ebdede95b727218c26927e0b8c17d91cf2f/pg0_embedded-0.10.1-py3-none-win_amd64.whl", hash = "sha256:39516c952edc050fbb9e24c35d28c9e013f108879b9c8e91091325231cbdb5a1", size = 54977766 },
]
[[package]]
name = "pgvector"
version = "0.4.1"
@@ -3830,7 +3820,7 @@ wheels = [
[[package]]
name = "sentence-transformers"
version = "5.1.2"
version = "3.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "huggingface-hub" },
@@ -3840,11 +3830,10 @@ dependencies = [
{ name = "torch" },
{ name = "tqdm" },
{ name = "transformers" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0f/96/f3f3409179d14dbfdbea8622e2e9eaa3c8836ddcaecd2cd5ff0a11731d20/sentence_transformers-5.1.2.tar.gz", hash = "sha256:0f6c8bd916a78dc65b366feb8d22fd885efdb37432e7630020d113233af2b856", size = 375185 }
sdist = { url = "https://files.pythonhosted.org/packages/de/61/708b20dedf26c460b416beb0acd5474c190dbca13e93b40858e99f17ac46/sentence_transformers-3.2.1.tar.gz", hash = "sha256:9fc38e620e5e1beba31d538a451778c9ccdbad77119d90f59f5bce49c4148e79", size = 202527 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bb/a6/a607a737dc1a00b7afe267b9bfde101b8cee2529e197e57471d23137d4e5/sentence_transformers-5.1.2-py3-none-any.whl", hash = "sha256:724ce0ea62200f413f1a5059712aff66495bc4e815a1493f7f9bca242414c333", size = 488009 },
{ url = "https://files.pythonhosted.org/packages/45/18/1ec591befcbdb2c97192a40fbe7c43a8b8a8b3c89b1fa101d3eeed4d79a4/sentence_transformers-3.2.1-py3-none-any.whl", hash = "sha256:c507e069eea33d15f1f2c72f74d7ea93abef298152cc235ab5af5e3a7584f738", size = 255758 },
]
[[package]]
@@ -4007,14 +3996,14 @@ wheels = [
[[package]]
name = "sympy"
version = "1.14.0"
version = "1.13.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mpmath" },
]
sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921 }
sdist = { url = "https://files.pythonhosted.org/packages/ca/99/5a5b6f19ff9f083671ddf7b9632028436167cd3d33e11015754e41b249a4/sympy-1.13.1.tar.gz", hash = "sha256:9cebf7e04ff162015ce31c9c6c9144daa34a93bd082f54fd8f12deca4f47515f", size = 7533040 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353 },
{ url = "https://files.pythonhosted.org/packages/b2/fe/81695a1aa331a842b582453b605175f419fe8540355886031328089d840a/sympy-1.13.1-py3-none-any.whl", hash = "sha256:db36cdc64bf61b9b24578b6f7bab1ecdd2452cf008f34faa33776680c26d66f8", size = 6189177 },
]
[[package]]
@@ -4091,27 +4080,49 @@ wheels = [
[[package]]
name = "tokenizers"
version = "0.22.1"
version = "0.20.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "huggingface-hub" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1c/46/fb6854cec3278fbfa4a75b50232c77622bc517ac886156e6afbfa4d8fc6e/tokenizers-0.22.1.tar.gz", hash = "sha256:61de6522785310a309b3407bac22d99c4db5dba349935e99e4d15ea2226af2d9", size = 363123 }
sdist = { url = "https://files.pythonhosted.org/packages/da/25/b1681c1c30ea3ea6e584ae3fffd552430b12faa599b558c4c4783f56d7ff/tokenizers-0.20.3.tar.gz", hash = "sha256:2278b34c5d0dd78e087e1ca7f9b1dcbf129d80211afa645f214bd6e051037539", size = 340513 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bf/33/f4b2d94ada7ab297328fc671fed209368ddb82f965ec2224eb1892674c3a/tokenizers-0.22.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:59fdb013df17455e5f950b4b834a7b3ee2e0271e6378ccb33aa74d178b513c73", size = 3069318 },
{ url = "https://files.pythonhosted.org/packages/1c/58/2aa8c874d02b974990e89ff95826a4852a8b2a273c7d1b4411cdd45a4565/tokenizers-0.22.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:8d4e484f7b0827021ac5f9f71d4794aaef62b979ab7608593da22b1d2e3c4edc", size = 2926478 },
{ url = "https://files.pythonhosted.org/packages/1e/3b/55e64befa1e7bfea963cf4b787b2cea1011362c4193f5477047532ce127e/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19d2962dd28bc67c1f205ab180578a78eef89ac60ca7ef7cbe9635a46a56422a", size = 3256994 },
{ url = "https://files.pythonhosted.org/packages/71/0b/fbfecf42f67d9b7b80fde4aabb2b3110a97fac6585c9470b5bff103a80cb/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:38201f15cdb1f8a6843e6563e6e79f4abd053394992b9bbdf5213ea3469b4ae7", size = 3153141 },
{ url = "https://files.pythonhosted.org/packages/17/a9/b38f4e74e0817af8f8ef925507c63c6ae8171e3c4cb2d5d4624bf58fca69/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1cbe5454c9a15df1b3443c726063d930c16f047a3cc724b9e6e1a91140e5a21", size = 3508049 },
{ url = "https://files.pythonhosted.org/packages/d2/48/dd2b3dac46bb9134a88e35d72e1aa4869579eacc1a27238f1577270773ff/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e7d094ae6312d69cc2a872b54b91b309f4f6fbce871ef28eb27b52a98e4d0214", size = 3710730 },
{ url = "https://files.pythonhosted.org/packages/93/0e/ccabc8d16ae4ba84a55d41345207c1e2ea88784651a5a487547d80851398/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afd7594a56656ace95cdd6df4cca2e4059d294c5cfb1679c57824b605556cb2f", size = 3412560 },
{ url = "https://files.pythonhosted.org/packages/d0/c6/dc3a0db5a6766416c32c034286d7c2d406da1f498e4de04ab1b8959edd00/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2ef6063d7a84994129732b47e7915e8710f27f99f3a3260b8a38fc7ccd083f4", size = 3250221 },
{ url = "https://files.pythonhosted.org/packages/d7/a6/2c8486eef79671601ff57b093889a345dd3d576713ef047776015dc66de7/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ba0a64f450b9ef412c98f6bcd2a50c6df6e2443b560024a09fa6a03189726879", size = 9345569 },
{ url = "https://files.pythonhosted.org/packages/6b/16/32ce667f14c35537f5f605fe9bea3e415ea1b0a646389d2295ec348d5657/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:331d6d149fa9c7d632cde4490fb8bbb12337fa3a0232e77892be656464f4b446", size = 9271599 },
{ url = "https://files.pythonhosted.org/packages/51/7c/a5f7898a3f6baa3fc2685c705e04c98c1094c523051c805cdd9306b8f87e/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:607989f2ea68a46cb1dfbaf3e3aabdf3f21d8748312dbeb6263d1b3b66c5010a", size = 9533862 },
{ url = "https://files.pythonhosted.org/packages/36/65/7e75caea90bc73c1dd8d40438adf1a7bc26af3b8d0a6705ea190462506e1/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a0f307d490295717726598ef6fa4f24af9d484809223bbc253b201c740a06390", size = 9681250 },
{ url = "https://files.pythonhosted.org/packages/30/2c/959dddef581b46e6209da82df3b78471e96260e2bc463f89d23b1bf0e52a/tokenizers-0.22.1-cp39-abi3-win32.whl", hash = "sha256:b5120eed1442765cd90b903bb6cfef781fd8fe64e34ccaecbae4c619b7b12a82", size = 2472003 },
{ url = "https://files.pythonhosted.org/packages/b3/46/e33a8c93907b631a99377ef4c5f817ab453d0b34f93529421f42ff559671/tokenizers-0.22.1-cp39-abi3-win_amd64.whl", hash = "sha256:65fd6e3fb11ca1e78a6a93602490f134d1fdeb13bcef99389d5102ea318ed138", size = 2674684 },
{ url = "https://files.pythonhosted.org/packages/c6/93/6742ef9206409d5ce1fdf44d5ca1687cdc3847ba0485424e2c731e6bcf67/tokenizers-0.20.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:585b51e06ca1f4839ce7759941e66766d7b060dccfdc57c4ca1e5b9a33013a90", size = 2674224 },
{ url = "https://files.pythonhosted.org/packages/aa/14/e75ece72e99f6ef9ae07777ca9fdd78608f69466a5cecf636e9bd2f25d5c/tokenizers-0.20.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:61cbf11954f3b481d08723ebd048ba4b11e582986f9be74d2c3bdd9293a4538d", size = 2558991 },
{ url = "https://files.pythonhosted.org/packages/46/54/033b5b2ba0c3ae01e026c6f7ced147d41a2fa1c573d00a66cb97f6d7f9b3/tokenizers-0.20.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef820880d5e4e8484e2fa54ff8d297bb32519eaa7815694dc835ace9130a3eea", size = 2892476 },
{ url = "https://files.pythonhosted.org/packages/e6/b0/cc369fb3297d61f3311cab523d16d48c869dc2f0ba32985dbf03ff811041/tokenizers-0.20.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:67ef4dcb8841a4988cd00dd288fb95dfc8e22ed021f01f37348fd51c2b055ba9", size = 2802775 },
{ url = "https://files.pythonhosted.org/packages/1a/74/62ad983e8ea6a63e04ed9c5be0b605056bf8aac2f0125f9b5e0b3e2b89fa/tokenizers-0.20.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff1ef8bd47a02b0dc191688ccb4da53600df5d4c9a05a4b68e1e3de4823e78eb", size = 3086138 },
{ url = "https://files.pythonhosted.org/packages/6b/ac/4637ba619db25094998523f9e6f5b456e1db1f8faa770a3d925d436db0c3/tokenizers-0.20.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:444d188186eab3148baf0615b522461b41b1f0cd58cd57b862ec94b6ac9780f1", size = 3098076 },
{ url = "https://files.pythonhosted.org/packages/58/ce/9793f2dc2ce529369807c9c74e42722b05034af411d60f5730b720388c7d/tokenizers-0.20.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37c04c032c1442740b2c2d925f1857885c07619224a533123ac7ea71ca5713da", size = 3379650 },
{ url = "https://files.pythonhosted.org/packages/50/f6/2841de926bc4118af996eaf0bdf0ea5b012245044766ffc0347e6c968e63/tokenizers-0.20.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:453c7769d22231960ee0e883d1005c93c68015025a5e4ae56275406d94a3c907", size = 2994005 },
{ url = "https://files.pythonhosted.org/packages/a3/b2/00915c4fed08e9505d37cf6eaab45b12b4bff8f6719d459abcb9ead86a4b/tokenizers-0.20.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4bb31f7b2847e439766aaa9cc7bccf7ac7088052deccdb2275c952d96f691c6a", size = 8977488 },
{ url = "https://files.pythonhosted.org/packages/e9/ac/1c069e7808181ff57bcf2d39e9b6fbee9133a55410e6ebdaa89f67c32e83/tokenizers-0.20.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:843729bf0f991b29655a069a2ff58a4c24375a553c70955e15e37a90dd4e045c", size = 9294935 },
{ url = "https://files.pythonhosted.org/packages/50/47/722feb70ee68d1c4412b12d0ea4acc2713179fd63f054913990f9e259492/tokenizers-0.20.3-cp311-none-win32.whl", hash = "sha256:efcce3a927b1e20ca694ba13f7a68c59b0bd859ef71e441db68ee42cf20c2442", size = 2197175 },
{ url = "https://files.pythonhosted.org/packages/75/68/1b4f928b15a36ed278332ac75d66d7eb65d865bf344d049c452c18447bf9/tokenizers-0.20.3-cp311-none-win_amd64.whl", hash = "sha256:88301aa0801f225725b6df5dea3d77c80365ff2362ca7e252583f2b4809c4cc0", size = 2381616 },
{ url = "https://files.pythonhosted.org/packages/07/00/92a08af2a6b0c88c50f1ab47d7189e695722ad9714b0ee78ea5e1e2e1def/tokenizers-0.20.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:49d12a32e190fad0e79e5bdb788d05da2f20d8e006b13a70859ac47fecf6ab2f", size = 2667951 },
{ url = "https://files.pythonhosted.org/packages/ec/9a/e17a352f0bffbf415cf7d73756f5c73a3219225fc5957bc2f39d52c61684/tokenizers-0.20.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:282848cacfb9c06d5e51489f38ec5aa0b3cd1e247a023061945f71f41d949d73", size = 2555167 },
{ url = "https://files.pythonhosted.org/packages/27/37/d108df55daf4f0fcf1f58554692ff71687c273d870a34693066f0847be96/tokenizers-0.20.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abe4e08c7d0cd6154c795deb5bf81d2122f36daf075e0c12a8b050d824ef0a64", size = 2898389 },
{ url = "https://files.pythonhosted.org/packages/b2/27/32f29da16d28f59472fa7fb38e7782069748c7e9ab9854522db20341624c/tokenizers-0.20.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ca94fc1b73b3883c98f0c88c77700b13d55b49f1071dfd57df2b06f3ff7afd64", size = 2795866 },
{ url = "https://files.pythonhosted.org/packages/29/4e/8a9a3c89e128c4a40f247b501c10279d2d7ade685953407c4d94c8c0f7a7/tokenizers-0.20.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef279c7e239f95c8bdd6ff319d9870f30f0d24915b04895f55b1adcf96d6c60d", size = 3085446 },
{ url = "https://files.pythonhosted.org/packages/b4/3b/a2a7962c496ebcd95860ca99e423254f760f382cd4bd376f8895783afaf5/tokenizers-0.20.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16384073973f6ccbde9852157a4fdfe632bb65208139c9d0c0bd0176a71fd67f", size = 3094378 },
{ url = "https://files.pythonhosted.org/packages/1f/f4/a8a33f0192a1629a3bd0afcad17d4d221bbf9276da4b95d226364208d5eb/tokenizers-0.20.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:312d522caeb8a1a42ebdec87118d99b22667782b67898a76c963c058a7e41d4f", size = 3385755 },
{ url = "https://files.pythonhosted.org/packages/9e/65/c83cb3545a65a9eaa2e13b22c93d5e00bd7624b354a44adbdc93d5d9bd91/tokenizers-0.20.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2b7cb962564785a83dafbba0144ecb7f579f1d57d8c406cdaa7f32fe32f18ad", size = 2997679 },
{ url = "https://files.pythonhosted.org/packages/55/e9/a80d4e592307688a67c7c59ab77e03687b6a8bd92eb5db763a2c80f93f57/tokenizers-0.20.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:124c5882ebb88dadae1fc788a582299fcd3a8bd84fc3e260b9918cf28b8751f5", size = 8989296 },
{ url = "https://files.pythonhosted.org/packages/90/af/60c957af8d2244321124e893828f1a4817cde1a2d08d09d423b73f19bd2f/tokenizers-0.20.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2b6e54e71f84c4202111a489879005cb14b92616a87417f6c102c833af961ea2", size = 9303621 },
{ url = "https://files.pythonhosted.org/packages/be/a9/96172310ee141009646d63a1ca267c099c462d747fe5ef7e33f74e27a683/tokenizers-0.20.3-cp312-none-win32.whl", hash = "sha256:83d9bfbe9af86f2d9df4833c22e94d94750f1d0cd9bfb22a7bb90a86f61cdb1c", size = 2188979 },
{ url = "https://files.pythonhosted.org/packages/bd/68/61d85ae7ae96dde7d0974ff3538db75d5cdc29be2e4329cd7fc51a283e22/tokenizers-0.20.3-cp312-none-win_amd64.whl", hash = "sha256:44def74cee574d609a36e17c8914311d1b5dbcfe37c55fd29369d42591b91cf2", size = 2380725 },
{ url = "https://files.pythonhosted.org/packages/07/19/36e9eaafb229616cb8502b42030fa7fe347550e76cb618de71b498fc3222/tokenizers-0.20.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e0b630e0b536ef0e3c8b42c685c1bc93bd19e98c0f1543db52911f8ede42cf84", size = 2666813 },
{ url = "https://files.pythonhosted.org/packages/b9/c7/e2ce1d4f756c8a62ef93fdb4df877c2185339b6d63667b015bf70ea9d34b/tokenizers-0.20.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a02d160d2b19bcbfdf28bd9a4bf11be4cb97d0499c000d95d4c4b1a4312740b6", size = 2555354 },
{ url = "https://files.pythonhosted.org/packages/7c/cf/5309c2d173a6a67f9ec8697d8e710ea32418de6fd8541778032c202a1c3e/tokenizers-0.20.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e3d80d89b068bc30034034b5319218c7c0a91b00af19679833f55f3becb6945", size = 2897745 },
{ url = "https://files.pythonhosted.org/packages/2c/e5/af3078e32f225e680e69d61f78855880edb8d53f5850a1834d519b2b103f/tokenizers-0.20.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:174a54910bed1b089226512b4458ea60d6d6fd93060254734d3bc3540953c51c", size = 2794385 },
{ url = "https://files.pythonhosted.org/packages/0b/a7/bc421fe46650cc4eb4a913a236b88c243204f32c7480684d2f138925899e/tokenizers-0.20.3-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:098b8a632b8656aa5802c46689462c5c48f02510f24029d71c208ec2c822e771", size = 3084580 },
{ url = "https://files.pythonhosted.org/packages/c6/22/97e1e95ee81f75922c9f569c23cb2b1fdc7f5a7a29c4c9fae17e63f751a6/tokenizers-0.20.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:78c8c143e3ae41e718588281eb3e212c2b31623c9d6d40410ec464d7d6221fb5", size = 3093581 },
{ url = "https://files.pythonhosted.org/packages/d5/14/f0df0ee3b9e516121e23c0099bccd7b9f086ba9150021a750e99b16ce56f/tokenizers-0.20.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b26b0aadb18cd8701077362ba359a06683662d5cafe3e8e8aba10eb05c037f1", size = 3385934 },
{ url = "https://files.pythonhosted.org/packages/66/52/7a171bd4929e3ffe61a29b4340fe5b73484709f92a8162a18946e124c34c/tokenizers-0.20.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07d7851a72717321022f3774e84aa9d595a041d643fafa2e87fbc9b18711dac0", size = 2997311 },
{ url = "https://files.pythonhosted.org/packages/7c/64/f1993bb8ebf775d56875ca0d50a50f2648bfbbb143da92fe2e6ceeb4abd5/tokenizers-0.20.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:bd44e48a430ada902c6266a8245f5036c4fe744fcb51f699999fbe82aa438797", size = 8988601 },
{ url = "https://files.pythonhosted.org/packages/d6/3f/49fa63422159bbc2f2a4ac5bfc597d04d4ec0ad3d2ef46649b5e9a340e37/tokenizers-0.20.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:a4c186bb006ccbe1f5cc4e0380d1ce7806f5955c244074fd96abc55e27b77f01", size = 9303950 },
{ url = "https://files.pythonhosted.org/packages/66/11/79d91aeb2817ad1993ef61c690afe73e6dbedbfb21918b302ef5a2ba9bfb/tokenizers-0.20.3-cp313-none-win32.whl", hash = "sha256:6e19e0f1d854d6ab7ea0c743d06e764d1d9a546932be0a67f33087645f00fe13", size = 2188941 },
{ url = "https://files.pythonhosted.org/packages/c2/ff/ac8410f868fb8b14b5e619efa304aa119cb8a40bd7df29fc81a898e64f99/tokenizers-0.20.3-cp313-none-win_amd64.whl", hash = "sha256:d50ede425c7e60966a9680d41b58b3a0950afa1bb570488e2972fa61662c4273", size = 2380269 },
]
[[package]]
@@ -4125,7 +4136,7 @@ wheels = [
[[package]]
name = "torch"
version = "2.9.0"
version = "2.5.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "filelock" },
@@ -4138,45 +4149,27 @@ dependencies = [
{ name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-cufile-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-cusparselt-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-nvshmem-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "setuptools", marker = "python_full_version >= '3.12'" },
{ name = "sympy" },
{ name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "triton", marker = "python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "typing-extensions" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/58/fe/334225e6330e672b36aef23d77451fa906ea12881570c08638a91331a212/torch-2.9.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:c596708b5105d0b199215acf0c9be7c1db5f1680d88eddadf4b75a299259a677", size = 104230578 },
{ url = "https://files.pythonhosted.org/packages/05/cc/49566caaa218872ec9a2912456f470ff92649894a4bc2e5274aa9ef87c4a/torch-2.9.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:51de31219c97c51cf4bf2be94d622e3deb5dcc526c6dc00e97c17eaec0fc1d67", size = 899815990 },
{ url = "https://files.pythonhosted.org/packages/74/25/e9ab21d5925b642d008f139d4a3c9664fc9ee1faafca22913c080cc4c0a5/torch-2.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:dd515c70059afd95f48b8192733764c08ca37a1d19803af6401b5ecad7c8676e", size = 109313698 },
{ url = "https://files.pythonhosted.org/packages/b3/b7/205ef3e94de636feffd64b28bb59a0dfac0771221201b9871acf9236f5ca/torch-2.9.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:614a185e4986326d526a91210c8fc1397e76e8cfafa78baf6296a790e53a9eec", size = 74463678 },
{ url = "https://files.pythonhosted.org/packages/d1/d3/3985739f3b8e88675127bf70f82b3a48ae083e39cda56305dbd90398fec0/torch-2.9.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:e5f7af1dc4c0a7c4a260c2534f41ddaf209714f7c89145e644c44712fbd6b642", size = 104107898 },
{ url = "https://files.pythonhosted.org/packages/a5/4b/f4bb2e6c25d0272f798cd6d7a04ed315da76cec68c602d87040c7847287f/torch-2.9.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:01cff95ecd9a212ea2f141db28acccdceb6a4c54f64e6c51091146f5e2a772c6", size = 899738273 },
{ url = "https://files.pythonhosted.org/packages/66/11/c1c5ba6691cda6279087c35bd626536e4fd29521fe740abf5008377a9a02/torch-2.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:4582b162f541651f0cb184d3e291c05c2f556c7117c64a9873e2ee158d40062b", size = 109280887 },
{ url = "https://files.pythonhosted.org/packages/dd/5f/b85bd8c05312d71de9402bf5868d217c38827cfd09d8f8514e5be128a52b/torch-2.9.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:33f58e9a102a91259af289d50525c30323b5c9ae1d31322b6447c0814da68695", size = 74478983 },
{ url = "https://files.pythonhosted.org/packages/c2/1c/90eb13833cdf4969ea9707586d7b57095c3b6e2b223a7256bf111689bcb8/torch-2.9.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:c30a17fc83eeab346913e237c64b15b5ba6407fff812f6c541e322e19bc9ea0e", size = 104111330 },
{ url = "https://files.pythonhosted.org/packages/0e/21/2254c54b8d523592c25ef4434769aa23e29b1e6bf5f4c0ad9e27bf442927/torch-2.9.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:8f25033b8667b57857dfd01458fbf2a9e6a6df1f8def23aef0dc46292f6aa642", size = 899750243 },
{ url = "https://files.pythonhosted.org/packages/b7/a5/5cb94fa4fd1e78223455c23c200f30f6dc10c6d4a2bcc8f6e7f2a2588370/torch-2.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:d037f1b4ffd25013be4a7bf3651a0a910c68554956c7b2c92ebe87c76475dece", size = 109284513 },
{ url = "https://files.pythonhosted.org/packages/66/e8/fc414d8656250ee46120b44836ffbb3266343db424b3e18ca79ebbf69d4f/torch-2.9.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e4e5b5cba837a2a8d1a497ba9a58dae46fa392593eaa13b871c42f71847503a5", size = 74830362 },
{ url = "https://files.pythonhosted.org/packages/ed/5f/9474c98fc5ae0cd04b9466035428cd360e6611a86b8352a0fc2fa504acdc/torch-2.9.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:64693568f5dc4dbd5f880a478b1cea0201cc6b510d91d1bc54fea86ac5d1a637", size = 104144940 },
{ url = "https://files.pythonhosted.org/packages/2d/5a/8e0c1cf57830172c109d4bd6be2708cabeaf550983eee7029291322447a0/torch-2.9.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:f8ed31ddd7d10bfb3fbe0b9fe01b1243577f13d75e6f4a0839a283915ce3791e", size = 899744054 },
{ url = "https://files.pythonhosted.org/packages/6d/28/82c28b30fcb4b7c9cdd995763d18bbb830d6521356712faebbad92ffa61d/torch-2.9.0-cp313-cp313t-win_amd64.whl", hash = "sha256:eff527d4e4846e6f70d2afd8058b73825761203d66576a7e04ea2ecfebcb4ab8", size = 109517546 },
{ url = "https://files.pythonhosted.org/packages/ff/c3/a91f96ec74347fa5fd24453fa514bc61c61ecc79196fa760b012a1873d96/torch-2.9.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:f8877779cf56d1ce431a7636703bdb13307f5960bb1af49716d8b179225e0e6a", size = 74480732 },
{ url = "https://files.pythonhosted.org/packages/5c/73/9f70af34b334a7e0ef496ceec96b7ec767bd778ea35385ce6f77557534d1/torch-2.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7e614fae699838038d888729f82b687c03413c5989ce2a9481f9a7e7a396e0bb", size = 74433037 },
{ url = "https://files.pythonhosted.org/packages/b7/84/37cf88625901934c97109e583ecc21777d21c6f54cda97a7e5bbad1ee2f2/torch-2.9.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:dfb5b8cd310ba3436c7e14e8b7833ef658cf3045e50d2bdaed23c8fc517065eb", size = 104116482 },
{ url = "https://files.pythonhosted.org/packages/56/8e/ca8b17866943a8d4f4664d402ea84210aa274588b4c5d89918f5caa24eec/torch-2.9.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:b3d29524993a478e46f5d598b249cd824b7ed98d7fba538bd9c4cde6c803948f", size = 899746916 },
{ url = "https://files.pythonhosted.org/packages/43/65/3b17c0fbbdab6501c5b320a52a648628d0d44e7379f64e27d9eef701b6bf/torch-2.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:71c7578984f5ec0eb645eb4816ac8435fcf3e3e2ae1901bcd2f519a9cafb5125", size = 109275151 },
{ url = "https://files.pythonhosted.org/packages/83/36/74f8c051f785500396e42f93542422422dfd874a174f21f8d955d36e5d64/torch-2.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:71d9309aee457bbe0b164bce2111cd911c4ed4e847e65d5077dbbcd3aba6befc", size = 74823353 },
{ url = "https://files.pythonhosted.org/packages/62/51/dc3b4e2f9ba98ae27238f0153ca098bf9340b2dafcc67fde645d496dfc2a/torch-2.9.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c08fb654d783899e204a32cca758a7ce8a45b2d78eeb89517cc937088316f78e", size = 104140340 },
{ url = "https://files.pythonhosted.org/packages/c0/8d/b00657f8141ac16af7bb6cda2e67de18499a3263b78d516b9a93fcbc98e3/torch-2.9.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ec8feb0099b2daa5728fbc7abb0b05730fd97e0f359ff8bda09865aaa7bd7d4b", size = 899731750 },
{ url = "https://files.pythonhosted.org/packages/fc/29/bd361e0cbb2c79ce6450f42643aaf6919956f89923a50571b0ebfe92d142/torch-2.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:695ba920f234ad4170c9c50e28d56c848432f8f530e6bc7f88fcb15ddf338e75", size = 109503850 },
{ url = "https://files.pythonhosted.org/packages/d1/35/e8b2daf02ce933e4518e6f5682c72fd0ed66c15910ea1fb4168f442b71c4/torch-2.5.1-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:de5b7d6740c4b636ef4db92be922f0edc425b65ed78c5076c43c42d362a45457", size = 906474467 },
{ url = "https://files.pythonhosted.org/packages/40/04/bd91593a4ca178ece93ca55f27e2783aa524aaccbfda66831d59a054c31e/torch-2.5.1-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:340ce0432cad0d37f5a31be666896e16788f1adf8ad7be481196b503dad675b9", size = 91919450 },
{ url = "https://files.pythonhosted.org/packages/0d/4a/e51420d46cfc90562e85af2fee912237c662ab31140ab179e49bd69401d6/torch-2.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:603c52d2fe06433c18b747d25f5c333f9c1d58615620578c326d66f258686f9a", size = 203098237 },
{ url = "https://files.pythonhosted.org/packages/d0/db/5d9cbfbc7968d79c5c09a0bc0bc3735da079f2fd07cc10498a62b320a480/torch-2.5.1-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:31f8c39660962f9ae4eeec995e3049b5492eb7360dd4f07377658ef4d728fa4c", size = 63884466 },
{ url = "https://files.pythonhosted.org/packages/8b/5c/36c114d120bfe10f9323ed35061bc5878cc74f3f594003854b0ea298942f/torch-2.5.1-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:ed231a4b3a5952177fafb661213d690a72caaad97d5824dd4fc17ab9e15cec03", size = 906389343 },
{ url = "https://files.pythonhosted.org/packages/6d/69/d8ada8b6e0a4257556d5b4ddeb4345ea8eeaaef3c98b60d1cca197c7ad8e/torch-2.5.1-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:3f4b7f10a247e0dcd7ea97dc2d3bfbfc90302ed36d7f3952b0008d0df264e697", size = 91811673 },
{ url = "https://files.pythonhosted.org/packages/5f/ba/607d013b55b9fd805db2a5c2662ec7551f1910b4eef39653eeaba182c5b2/torch-2.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:73e58e78f7d220917c5dbfad1a40e09df9929d3b95d25e57d9f8558f84c9a11c", size = 203046841 },
{ url = "https://files.pythonhosted.org/packages/57/6c/bf52ff061da33deb9f94f4121fde7ff3058812cb7d2036c97bc167793bd1/torch-2.5.1-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:8c712df61101964eb11910a846514011f0b6f5920c55dbf567bff8a34163d5b1", size = 63858109 },
{ url = "https://files.pythonhosted.org/packages/69/72/20cb30f3b39a9face296491a86adb6ff8f1a47a897e4d14667e6cf89d5c3/torch-2.5.1-cp313-cp313-manylinux1_x86_64.whl", hash = "sha256:9b61edf3b4f6e3b0e0adda8b3960266b9009d02b37555971f4d1c8f7a05afed7", size = 906393265 },
]
[[package]]
@@ -4212,7 +4205,7 @@ wheels = [
[[package]]
name = "transformers"
version = "4.57.1"
version = "4.45.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "filelock" },
@@ -4226,22 +4219,21 @@ dependencies = [
{ name = "tokenizers" },
{ name = "tqdm" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d6/68/a39307bcc4116a30b2106f2e689130a48de8bd8a1e635b5e1030e46fcd9e/transformers-4.57.1.tar.gz", hash = "sha256:f06c837959196c75039809636cd964b959f6604b75b8eeec6fdfc0440b89cc55", size = 10142511 }
sdist = { url = "https://files.pythonhosted.org/packages/4b/4c/3862b2dd6cdf83b187897bd351da0f7fb74d0df642b03c6f5d06353a3ca0/transformers-4.45.2.tar.gz", hash = "sha256:72bc390f6b203892561f05f86bbfaa0e234aab8e927a83e62b9d92ea7e3ae101", size = 8478357 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/71/d3/c16c3b3cf7655a67db1144da94b021c200ac1303f82428f2beef6c2e72bb/transformers-4.57.1-py3-none-any.whl", hash = "sha256:b10d05da8fa67dc41644dbbf9bc45a44cb86ae33da6f9295f5fbf5b7890bd267", size = 11990925 },
{ url = "https://files.pythonhosted.org/packages/f9/9d/030cc1b3e88172967e22ee1d012e0d5e0384eb70d2a098d1669d549aea29/transformers-4.45.2-py3-none-any.whl", hash = "sha256:c551b33660cfc815bae1f9f097ecfd1e65be623f13c6ee0dda372bd881460210", size = 9881312 },
]
[[package]]
name = "triton"
version = "3.5.0"
version = "3.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "filelock" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/3d/78/949a04391c21956c816523678f0e5fa308eb5b1e7622d88c4e4ef5fceca0/triton-3.5.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f34bfa21c5b3a203c0f0eab28dcc1e49bd1f67d22724e77fb6665a659200a4ec", size = 170433488 },
{ url = "https://files.pythonhosted.org/packages/f5/3a/e991574f3102147b642e49637e0281e9bb7c4ba254edb2bab78247c85e01/triton-3.5.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9e71db82261c4ffa3921cd050cd5faa18322d2d405c30eb56084afaff3b0833", size = 170476535 },
{ url = "https://files.pythonhosted.org/packages/6c/29/10728de8a6e932e517c10773486b8e99f85d1b1d9dd87d9a9616e1fef4a1/triton-3.5.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e6bb9aa5519c084a333acdba443789e50012a4b851cd486c54f0b8dc2a8d3a12", size = 170487289 },
{ url = "https://files.pythonhosted.org/packages/5c/38/db80e48b9220c9bce872b0f616ad0446cdf554a40b85c7865cbca99ab3c2/triton-3.5.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c83f2343e1a220a716c7b3ab9fccfcbe3ad4020d189549200e2d2e8d5868bed9", size = 170577179 },
{ url = "https://files.pythonhosted.org/packages/ff/60/1810655d1d856c9a4fcc90ee8966d85f552d98c53a6589f95ab2cbe27bb8/triton-3.5.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da0fa67ccd76c3dcfb0bffe1b1c57c685136a6bd33d141c24d9655d4185b1289", size = 170487949 },
{ url = "https://files.pythonhosted.org/packages/fb/b7/1dec8433ac604c061173d0589d99217fe7bf90a70bdc375e745d044b8aad/triton-3.5.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:317fe477ea8fd4524a6a8c499fb0a36984a56d0b75bf9c9cb6133a1c56d5a6e7", size = 170580176 },
{ url = "https://files.pythonhosted.org/packages/86/17/d9a5cf4fcf46291856d1e90762e36cbabd2a56c7265da0d1d9508c8e3943/triton-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f34f6e7885d1bf0eaaf7ba875a5f0ce6f3c13ba98f9503651c1e6dc6757ed5c", size = 209506424 },
{ url = "https://files.pythonhosted.org/packages/78/eb/65f5ba83c2a123f6498a3097746607e5b2f16add29e36765305e4ac7fdd8/triton-3.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8182f42fd8080a7d39d666814fa36c5e30cc00ea7eeeb1a2983dbb4c99a0fdc", size = 209551444 },
]
[[package]]