Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06d11cf867 | ||
|
|
7f5576cdee | ||
|
|
3572387051 | ||
|
|
3a91c0b87f | ||
|
|
706204bc4f | ||
|
|
147d46fc91 | ||
|
|
285bed65f9 | ||
|
|
c85a1ca58b | ||
|
|
583683b0a2 | ||
|
|
0d5503c892 | ||
|
|
af2756f2da | ||
|
|
b1e380bdae | ||
|
|
b8ec743962 | ||
|
|
d891124835 | ||
|
|
04ff24be8d | ||
|
|
076c33e854 | ||
|
|
a03c942296 | ||
|
|
b4e42bd0c6 | ||
|
|
95b2b7e78f | ||
|
|
2aa8700db8 | ||
|
|
c0093f1a97 | ||
|
|
460f045f16 | ||
|
|
0673d4813d |
@@ -0,0 +1,11 @@
|
||||
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
|
||||
@@ -20,15 +20,18 @@ 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: package-lock.json
|
||||
- run: npm ci --workspace=hindsight-docs
|
||||
- run: npm run build --workspace=hindsight-docs
|
||||
cache-dependency-path: hindsight-docs/package-lock.json
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: hindsight-docs/build
|
||||
|
||||
@@ -38,10 +38,6 @@ 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
|
||||
@@ -61,12 +57,6 @@ 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
|
||||
@@ -76,7 +66,6 @@ jobs:
|
||||
hindsight-clients/python/dist/*
|
||||
hindsight-api/dist/*
|
||||
hindsight/dist/*
|
||||
hindsight-integrations/litellm/dist/*
|
||||
retention-days: 1
|
||||
|
||||
release-typescript-client:
|
||||
@@ -91,14 +80,14 @@ jobs:
|
||||
with:
|
||||
node-version: '20'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --workspace=hindsight-clients/typescript
|
||||
working-directory: ./hindsight-clients/typescript
|
||||
run: npm ci
|
||||
|
||||
- name: Build
|
||||
run: npm run build --workspace=hindsight-clients/typescript
|
||||
working-directory: ./hindsight-clients/typescript
|
||||
run: npm run build
|
||||
|
||||
- name: Publish to npm
|
||||
working-directory: ./hindsight-clients/typescript
|
||||
@@ -317,7 +306,6 @@ 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
|
||||
@@ -328,11 +316,54 @@ 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/*
|
||||
generate_release_notes: true
|
||||
body_path: release-notes.md
|
||||
draft: false
|
||||
prerelease: false
|
||||
env:
|
||||
|
||||
+16
-245
@@ -9,54 +9,6 @@ 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
|
||||
|
||||
@@ -67,14 +19,14 @@ jobs:
|
||||
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-docs
|
||||
working-directory: ./hindsight-docs
|
||||
run: npm ci
|
||||
|
||||
- name: Build docs
|
||||
run: npm run build --workspace=hindsight-docs
|
||||
working-directory: ./hindsight-docs
|
||||
run: npm run build
|
||||
|
||||
build-rust-cli:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -154,9 +106,6 @@ 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)
|
||||
@@ -176,6 +125,9 @@ 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
|
||||
@@ -233,6 +185,9 @@ 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
|
||||
@@ -314,6 +269,9 @@ jobs:
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install pg0
|
||||
uses: ./.github/actions/setup-pg0
|
||||
|
||||
- name: Build API
|
||||
working-directory: ./hindsight-api
|
||||
run: uv build
|
||||
@@ -402,6 +360,9 @@ 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
|
||||
@@ -444,193 +405,3 @@ 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"
|
||||
@@ -9,9 +9,6 @@ wheels/
|
||||
# Virtual environments
|
||||
.venv
|
||||
|
||||
# Node
|
||||
node_modules/
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
|
||||
|
||||
+4
-14
@@ -5,23 +5,13 @@ Thanks for your interest in contributing to Hindsight!
|
||||
## Getting Started
|
||||
|
||||
1. Fork and clone the repository
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
git clone [email protected]:vectorize-io/hindsight.git
|
||||
cd hindsight
|
||||
cd hindsight-api && uv sync
|
||||
```
|
||||
2. Set up your environment:
|
||||
3. Set up your environment:
|
||||
```bash
|
||||
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
|
||||
export OPENAI_API_KEY=your-key
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
|
||||

|
||||

|
||||
|
||||
[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.
|
||||
|
||||
|
||||
@@ -54,15 +54,13 @@ 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
|
||||
WORKDIR /app/sdk
|
||||
|
||||
# Copy root package files for npm workspaces
|
||||
COPY package.json package-lock.json ./
|
||||
COPY hindsight-clients/typescript/ ./hindsight-clients/typescript/
|
||||
COPY hindsight-clients/typescript/package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
# Install and build SDK using workspace
|
||||
RUN npm ci -w @vectorize-io/hindsight-client
|
||||
RUN npm run build -w @vectorize-io/hindsight-client
|
||||
COPY hindsight-clients/typescript/ ./
|
||||
RUN npm run build
|
||||
|
||||
# =============================================================================
|
||||
# Stage: Control Plane Builder
|
||||
@@ -75,7 +73,7 @@ RUN if [ "$INCLUDE_CP" != "true" ]; then echo "Skipping CP build" && exit 0; fi
|
||||
WORKDIR /app
|
||||
|
||||
# Copy built SDK
|
||||
COPY --from=sdk-builder /app/hindsight-clients/typescript /app/sdk
|
||||
COPY --from=sdk-builder /app/sdk /app/sdk
|
||||
|
||||
# Install Control Plane dependencies
|
||||
# Only copy package.json (not package-lock.json) to ensure npm installs
|
||||
@@ -132,10 +130,38 @@ RUN mkdir -p /app/data && chown -R hindsight:hindsight /app
|
||||
USER hindsight
|
||||
|
||||
# Set PATH for hindsight user
|
||||
ENV PATH="/app/api/.venv/bin:${PATH}"
|
||||
ENV PATH="/home/hindsight/.hindsight/bin:/app/api/.venv/bin:${PATH}"
|
||||
|
||||
# Pre-cache PostgreSQL binaries by starting/stopping pg0-embedded
|
||||
# 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 && \
|
||||
ls -lh /home/hindsight/.hindsight/bin/pg0 && \
|
||||
file /home/hindsight/.hindsight/bin/pg0 && \
|
||||
ldd /home/hindsight/.hindsight/bin/pg0 2>&1 || true && \
|
||||
break || (echo "Retry $i failed, waiting..." && sleep 10); \
|
||||
done && \
|
||||
echo "Testing pg0 binary..." && \
|
||||
/home/hindsight/.hindsight/bin/pg0 --version || (echo "pg0 --version failed with exit code $?"; ldd /home/hindsight/.hindsight/bin/pg0; exit 1)
|
||||
|
||||
# Pre-download PostgreSQL binaries
|
||||
ENV PG0_HOME=/home/hindsight/.pg0-cache
|
||||
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
|
||||
|
||||
@@ -167,7 +193,7 @@ FROM node:20-alpine AS cp-only
|
||||
WORKDIR /app
|
||||
|
||||
# Copy built SDK
|
||||
COPY --from=sdk-builder /app/hindsight-clients/typescript /app/sdk
|
||||
COPY --from=sdk-builder /app/sdk /app/sdk
|
||||
|
||||
# Copy Control Plane standalone build
|
||||
WORKDIR /app/control-plane
|
||||
@@ -220,7 +246,7 @@ RUN useradd -m -s /bin/bash hindsight
|
||||
COPY --from=api-builder /app/api /app/api
|
||||
|
||||
# Copy built SDK
|
||||
COPY --from=sdk-builder /app/hindsight-clients/typescript /app/sdk
|
||||
COPY --from=sdk-builder /app/sdk /app/sdk
|
||||
|
||||
# Copy Control Plane standalone build
|
||||
WORKDIR /app/control-plane
|
||||
@@ -241,17 +267,38 @@ RUN mkdir -p /app/data && chown -R hindsight:hindsight /app
|
||||
USER hindsight
|
||||
|
||||
# Set PATH for hindsight user
|
||||
ENV PATH="/app/api/.venv/bin:${PATH}"
|
||||
ENV PATH="/home/hindsight/.hindsight/bin:/app/api/.venv/bin:${PATH}"
|
||||
|
||||
# Pre-cache PostgreSQL binaries by starting/stopping pg0-embedded
|
||||
# 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 && \
|
||||
ls -lh /home/hindsight/.hindsight/bin/pg0 && \
|
||||
file /home/hindsight/.hindsight/bin/pg0 && \
|
||||
ldd /home/hindsight/.hindsight/bin/pg0 2>&1 || true && \
|
||||
break || (echo "Retry $i failed, waiting..." && sleep 10); \
|
||||
done && \
|
||||
echo "Testing pg0 binary..." && \
|
||||
/home/hindsight/.hindsight/bin/pg0 --version || (echo "pg0 --version failed with exit code $?"; ldd /home/hindsight/.hindsight/bin/pg0; exit 1)
|
||||
|
||||
# Pre-download PostgreSQL binaries
|
||||
ENV PG0_HOME=/home/hindsight/.pg0-cache
|
||||
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"
|
||||
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
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ apiVersion: v2
|
||||
name: hindsight
|
||||
description: Hindsight helm chart
|
||||
type: application
|
||||
version: 0.1.5
|
||||
appVersion: "0.1.5"
|
||||
version: 0.1.4
|
||||
appVersion: "0.1.4"
|
||||
keywords:
|
||||
- ai
|
||||
- memory
|
||||
|
||||
@@ -121,7 +121,11 @@ class MCPMiddleware:
|
||||
self.app = app
|
||||
self.memory = memory
|
||||
self.mcp_server = create_mcp_server(memory)
|
||||
self.mcp_app = self.mcp_server.http_app()
|
||||
# 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()
|
||||
|
||||
async def __call__(self, scope, receive, send):
|
||||
if scope["type"] != "http":
|
||||
|
||||
@@ -29,7 +29,6 @@ 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"
|
||||
@@ -46,7 +45,6 @@ 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
|
||||
@@ -81,9 +79,6 @@ class HindsightConfig:
|
||||
log_level: str
|
||||
mcp_enabled: bool
|
||||
|
||||
# Recall
|
||||
graph_retriever: str
|
||||
|
||||
@classmethod
|
||||
def from_env(cls) -> "HindsightConfig":
|
||||
"""Create configuration from environment variables."""
|
||||
@@ -112,9 +107,6 @@ 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:
|
||||
@@ -155,7 +147,6 @@ 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:
|
||||
|
||||
@@ -175,13 +175,9 @@ 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_gpt4o_model and max_completion_tokens > 16384:
|
||||
max_completion_tokens = 16384
|
||||
elif is_gpt4_model and max_completion_tokens > 32000:
|
||||
if is_gpt4_model and max_completion_tokens > 32000:
|
||||
max_completion_tokens = 32000
|
||||
# For reasoning models, max_completion_tokens includes reasoning + output tokens
|
||||
# Enforce minimum of 16000 to ensure enough space for both
|
||||
@@ -272,9 +268,9 @@ class LLMProvider:
|
||||
raise
|
||||
|
||||
except APIStatusError as 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)}")
|
||||
# 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)}")
|
||||
raise
|
||||
|
||||
last_exception = e
|
||||
@@ -412,13 +408,13 @@ class LLMProvider:
|
||||
raise
|
||||
|
||||
except genai_errors.APIError as 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)}")
|
||||
# Fast fail on 4xx client errors (except 429 rate limit)
|
||||
if e.code and 400 <= e.code < 500 and e.code != 429:
|
||||
logger.error(f"Gemini client error (HTTP {e.code}), not retrying: {str(e)}")
|
||||
raise
|
||||
|
||||
# Retry on 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):
|
||||
# Retry on 429 and 5xx
|
||||
if e.code in (429, 500, 502, 503, 504):
|
||||
last_exception = e
|
||||
if attempt < max_retries:
|
||||
backoff = min(initial_backoff * (2 ** attempt), max_backoff)
|
||||
|
||||
@@ -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, retrieval_result in enumerate(all_retrievals):
|
||||
for idx, (ft_semantic, ft_bm25, ft_graph, ft_temporal, ft_timings, ft_temporal_constraint) 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(retrieval_result.semantic)}, bm25={len(retrieval_result.bm25)}, graph={len(retrieval_result.graph)}, temporal={len(retrieval_result.temporal) if retrieval_result.temporal else 0}")
|
||||
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}")
|
||||
|
||||
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)
|
||||
semantic_results.extend(ft_semantic)
|
||||
bm25_results.extend(ft_bm25)
|
||||
graph_results.extend(ft_graph)
|
||||
if ft_temporal:
|
||||
temporal_results.extend(ft_temporal)
|
||||
# Track max timing for each method (since they run in parallel across fact types)
|
||||
for method, duration in retrieval_result.timings.items():
|
||||
aggregated_timings[method] = max(aggregated_timings.get(method, 0.0), duration)
|
||||
for method, duration in ft_timings.items():
|
||||
aggregated_timings[method] = max(aggregated_timings[method], duration)
|
||||
# Capture temporal constraint (same across all fact types)
|
||||
if retrieval_result.temporal_constraint:
|
||||
detected_temporal_constraint = retrieval_result.temporal_constraint
|
||||
if ft_temporal_constraint:
|
||||
detected_temporal_constraint = ft_temporal_constraint
|
||||
|
||||
# If no temporal results from any fact type, set to None
|
||||
if not temporal_results:
|
||||
@@ -1203,57 +1203,49 @@ 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 - per fact type
|
||||
# Record retrieval results for tracer (convert typed results to old format)
|
||||
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 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 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 semantic retrieval results for this fact type
|
||||
# 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:
|
||||
tracer.add_retrieval_results(
|
||||
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
|
||||
method_name="temporal",
|
||||
results=to_tuple_format(temporal_results),
|
||||
duration_seconds=aggregated_timings["temporal"],
|
||||
score_field="temporal_score",
|
||||
metadata={"budget": thinking_budget}
|
||||
)
|
||||
|
||||
# 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)
|
||||
@@ -1295,24 +1287,31 @@ 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 using min-max normalization
|
||||
# Normalize RRF scores to [0, 1] range
|
||||
rrf_scores = [sr.candidate.rrf_score for sr in scored_results]
|
||||
max_rrf = max(rrf_scores) if rrf_scores else 0.0
|
||||
max_rrf = max(rrf_scores) if rrf_scores else 1.0
|
||||
min_rrf = min(rrf_scores) if rrf_scores else 0.0
|
||||
rrf_range = max_rrf - min_rrf # Don't force to 1.0, let fallback handle it
|
||||
rrf_range = max_rrf - min_rrf if max_rrf > min_rrf else 1.0
|
||||
|
||||
# Calculate recency based on occurred_start (more recent = higher score)
|
||||
now = utcnow()
|
||||
for sr in scored_results:
|
||||
# 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
|
||||
# Normalize RRF score
|
||||
sr.rrf_normalized = (sr.candidate.rrf_score - min_rrf) / rrf_range if rrf_range > 0 else 0.5
|
||||
|
||||
# Calculate recency (decay over 365 days, minimum 0.1)
|
||||
sr.recency = 0.5 # default for missing dates
|
||||
@@ -1344,17 +1343,6 @@ 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,27 +3,13 @@ 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,
|
||||
get_default_graph_retriever,
|
||||
set_default_graph_retriever,
|
||||
ParallelRetrievalResult,
|
||||
)
|
||||
from .graph_retrieval import GraphRetriever, BFSGraphRetriever
|
||||
from .mpfp_retrieval import MPFPGraphRetriever
|
||||
from .retrieval import retrieve_parallel
|
||||
from .reranking import CrossEncoderReranker
|
||||
|
||||
__all__ = [
|
||||
"retrieve_parallel",
|
||||
"get_default_graph_retriever",
|
||||
"set_default_graph_retriever",
|
||||
"ParallelRetrievalResult",
|
||||
"GraphRetriever",
|
||||
"BFSGraphRetriever",
|
||||
"MPFPGraphRetriever",
|
||||
"CrossEncoderReranker",
|
||||
]
|
||||
|
||||
@@ -1,235 +0,0 @@
|
||||
"""
|
||||
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
|
||||
@@ -1,454 +0,0 @@
|
||||
"""
|
||||
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,61 +4,15 @@ Retrieval module for 4-way parallel search.
|
||||
Implements:
|
||||
1. Semantic retrieval (vector similarity)
|
||||
2. BM25 retrieval (keyword/full-text search)
|
||||
3. Graph retrieval (via pluggable GraphRetriever interface)
|
||||
3. Graph retrieval (spreading activation)
|
||||
4. Temporal retrieval (time-aware search with spreading)
|
||||
"""
|
||||
|
||||
from typing import List, Dict, Optional
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Dict, Any, Tuple, Optional
|
||||
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(
|
||||
@@ -151,6 +105,121 @@ 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,
|
||||
@@ -350,9 +419,8 @@ async def retrieve_parallel(
|
||||
fact_type: str,
|
||||
thinking_budget: int,
|
||||
question_date: Optional[datetime] = None,
|
||||
query_analyzer: Optional["QueryAnalyzer"] = None,
|
||||
graph_retriever: Optional[GraphRetriever] = None,
|
||||
) -> ParallelRetrievalResult:
|
||||
query_analyzer: Optional["QueryAnalyzer"] = None
|
||||
) -> Tuple[List[RetrievalResult], List[RetrievalResult], List[RetrievalResult], Optional[List[RetrievalResult]], Dict[str, float], Optional[Tuple[datetime, datetime]]]:
|
||||
"""
|
||||
Run 3-way or 4-way parallel retrieval (adds temporal if detected).
|
||||
|
||||
@@ -360,318 +428,76 @@ async def retrieve_parallel(
|
||||
pool: Database connection pool
|
||||
query_text: Query text
|
||||
query_embedding_str: Query embedding as string
|
||||
bank_id: Bank ID
|
||||
agent_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:
|
||||
ParallelRetrievalResult with semantic, bm25, graph, temporal results and timings
|
||||
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
|
||||
"""
|
||||
# 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
|
||||
)
|
||||
|
||||
retriever = graph_retriever or get_default_graph_retriever()
|
||||
|
||||
if retriever.name == "mpfp":
|
||||
return await _retrieve_parallel_mpfp(
|
||||
pool, query_text, query_embedding_str, bank_id, fact_type,
|
||||
thinking_budget, temporal_constraint, retriever
|
||||
)
|
||||
else:
|
||||
return await _retrieve_parallel_bfs(
|
||||
pool, query_text, query_embedding_str, bank_id, fact_type,
|
||||
thinking_budget, temporal_constraint, retriever
|
||||
)
|
||||
|
||||
|
||||
@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)."""
|
||||
# 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
|
||||
|
||||
async def run_semantic():
|
||||
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
|
||||
return await retrieve_semantic(conn, query_embedding_str, bank_id, fact_type, limit=thinking_budget)
|
||||
|
||||
# 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 def run_bm25():
|
||||
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)
|
||||
return await retrieve_bm25(conn, query_text, bank_id, fact_type, limit=thinking_budget)
|
||||
|
||||
async def run_temporal(tc_start, tc_end) -> _TimedResult:
|
||||
"""Temporal retrieval (uses its own entry point finding)."""
|
||||
start = time.time()
|
||||
async def run_graph():
|
||||
async with acquire_with_retry(pool) as conn:
|
||||
results = await retrieve_temporal(
|
||||
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,
|
||||
tc_start, tc_end, budget=thinking_budget, semantic_threshold=0.1
|
||||
start_date, end_date, budget=thinking_budget, semantic_threshold=0.1
|
||||
)
|
||||
return _TimedResult(results, time.time() - start)
|
||||
|
||||
# Run parallel task chains
|
||||
# Run retrievals in parallel with timing
|
||||
timings = {}
|
||||
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,
|
||||
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))
|
||||
)
|
||||
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:
|
||||
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,
|
||||
results = await asyncio.gather(
|
||||
timed_retrieval("semantic", run_semantic()),
|
||||
timed_retrieval("bm25", run_bm25()),
|
||||
timed_retrieval("graph", run_graph())
|
||||
)
|
||||
semantic_results, _, timings["semantic"] = results[0]
|
||||
bm25_results, _, timings["bm25"] = results[1]
|
||||
graph_results, _, timings["graph"] = results[2]
|
||||
temporal_results = 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,
|
||||
)
|
||||
return semantic_results, bm25_results, graph_results, temporal_results, timings, temporal_constraint
|
||||
|
||||
@@ -108,7 +108,6 @@ 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,8 +289,7 @@ 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,
|
||||
fact_type: Optional[str] = None
|
||||
metadata: Optional[Dict[str, Any]] = None
|
||||
):
|
||||
"""
|
||||
Record results from a single retrieval method.
|
||||
@@ -301,7 +300,6 @@ 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):
|
||||
@@ -315,7 +313,7 @@ class SearchTracer:
|
||||
text=data.get("text", ""),
|
||||
context=data.get("context", ""),
|
||||
event_date=data.get("event_date"),
|
||||
fact_type=data.get("fact_type") or fact_type,
|
||||
fact_type=data.get("fact_type"),
|
||||
score=score,
|
||||
score_name=score_field,
|
||||
)
|
||||
@@ -324,7 +322,6 @@ 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 {},
|
||||
@@ -370,10 +367,8 @@ 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 ["cross_encoder_score", "cross_encoder_score_normalized", "rrf_score", "rrf_normalized", "temporal", "recency", "combined_score"]:
|
||||
for key in ["semantic_similarity", "bm25_score", "rrf_score", "recency_normalized", "frequency_normalized", "cross_encoder_score", "cross_encoder_score_normalized"]:
|
||||
if key in result and result[key] is not None:
|
||||
score_components[key] = result[key]
|
||||
|
||||
|
||||
@@ -31,9 +31,8 @@ class RetrievalResult:
|
||||
embedding: Optional[List[float]] = None
|
||||
|
||||
# Retrieval-specific scores (only one will be set depending on retrieval method)
|
||||
similarity: Optional[float] = None # Semantic retrieval
|
||||
similarity: Optional[float] = None # Semantic/graph 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
|
||||
|
||||
@@ -55,7 +54,6 @@ 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"),
|
||||
)
|
||||
@@ -154,7 +152,6 @@ 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
|
||||
|
||||
@@ -1,116 +1,373 @@
|
||||
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
|
||||
|
||||
from pg0 import Pg0
|
||||
import httpx
|
||||
|
||||
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-embedded."""
|
||||
"""
|
||||
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()
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
# Will be set when binary is found/installed
|
||||
self._binary_path: Optional[Path] = _find_pg0_binary()
|
||||
|
||||
async def start(self, max_retries: int = 3, retry_delay: float = 2.0) -> str:
|
||||
"""Start the PostgreSQL server with retry logic."""
|
||||
logger.info(f"Starting embedded PostgreSQL (name: {self.name}, port: {self.port})...")
|
||||
@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
|
||||
|
||||
pg0 = self._get_pg0()
|
||||
last_error = None
|
||||
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
|
||||
|
||||
for attempt in range(1, max_retries + 1):
|
||||
try:
|
||||
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 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}")
|
||||
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"
|
||||
|
||||
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})...")
|
||||
|
||||
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()
|
||||
logger.info(f"PostgreSQL started on port {self.port}")
|
||||
return uri
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
# 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}"
|
||||
f"Last error: {last_error.strip() if last_error else 'unknown'}"
|
||||
)
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""Stop the PostgreSQL server."""
|
||||
pg0 = self._get_pg0()
|
||||
if not self.is_installed():
|
||||
return
|
||||
|
||||
logger.info(f"Stopping embedded PostgreSQL (name: {self.name})...")
|
||||
|
||||
try:
|
||||
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():
|
||||
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: {e}")
|
||||
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}")
|
||||
|
||||
async def get_uri(self) -> str:
|
||||
"""Get the connection URI for the PostgreSQL server."""
|
||||
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}"
|
||||
info = await self._get_info()
|
||||
uri = info.get("uri")
|
||||
if not uri:
|
||||
raise RuntimeError("PostgreSQL server is not running or URI not available")
|
||||
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:
|
||||
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:
|
||||
info = await self._get_info()
|
||||
return info.get("running", False)
|
||||
except RuntimeError:
|
||||
return False
|
||||
|
||||
async def ensure_running(self) -> str:
|
||||
"""Ensure the PostgreSQL server is running, starting it if needed."""
|
||||
"""
|
||||
Ensure the PostgreSQL server is running.
|
||||
|
||||
Installs if needed, starts if not running.
|
||||
|
||||
Returns:
|
||||
The connection URI.
|
||||
"""
|
||||
await self.ensure_installed()
|
||||
|
||||
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
|
||||
|
||||
@@ -118,18 +375,33 @@ _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."""
|
||||
return await get_embedded_postgres().ensure_running()
|
||||
"""
|
||||
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()
|
||||
|
||||
|
||||
async def stop_embedded_postgres() -> None:
|
||||
"""Stop the default embedded PostgreSQL instance."""
|
||||
global _default_instance
|
||||
|
||||
if _default_instance:
|
||||
await _default_instance.stop()
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "hindsight-api"
|
||||
version = "0.1.5"
|
||||
version = "0.1.4"
|
||||
description = "Temporal + Semantic + Entity Memory System for AI agents using PostgreSQL"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
@@ -28,8 +28,7 @@ dependencies = [
|
||||
"torch>=2.0.0,<2.6.0",
|
||||
"tiktoken>=0.12.0",
|
||||
"httpx>=0.27.0",
|
||||
"fastmcp>=2.3.0",
|
||||
"pg0-embedded>=0.1.0",
|
||||
"fastmcp>=2.0.0",
|
||||
"python-dateutil>=2.8.0",
|
||||
"opentelemetry-api>=1.20.0",
|
||||
"opentelemetry-sdk>=1.20.0",
|
||||
|
||||
@@ -1,323 +0,0 @@
|
||||
"""
|
||||
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)
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "hindsight-cli"
|
||||
version = "0.1.5"
|
||||
version = "0.1.4"
|
||||
edition = "2021"
|
||||
authors = ["Hindsight Team"]
|
||||
description = "A beautiful CLI for Hindsight - semantic memory system"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "hindsight-client"
|
||||
version = "0.1.5"
|
||||
version = "0.1.4"
|
||||
description = "Python client for Hindsight - Semantic memory system with personality-driven thinking"
|
||||
authors = [
|
||||
{name = "Hindsight Team"}
|
||||
|
||||
+4409
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vectorize-io/hindsight-client",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.4",
|
||||
"description": "TypeScript client for Hindsight - Semantic memory system with personality-driven thinking",
|
||||
"main": "./dist/src/index.js",
|
||||
"types": "./dist/src/index.d.ts",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"lib": ["ES2020", "DOM"],
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./",
|
||||
|
||||
+9845
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hindsight-control-plane",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.4",
|
||||
"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.10",
|
||||
"next": "^16.0.7",
|
||||
"postcss": "^8.5.6",
|
||||
"react": "^19.2.0",
|
||||
"react-chrono": "^2.9.1",
|
||||
|
||||
@@ -180,13 +180,4 @@ 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);
|
||||
}
|
||||
@@ -18,7 +18,7 @@ export default function RootLayout({
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className="bg-background text-foreground">
|
||||
<body>
|
||||
<ThemeProvider>
|
||||
<BankProvider>
|
||||
{children}
|
||||
|
||||
@@ -239,7 +239,7 @@ export function BankProfileView() {
|
||||
<div className="flex gap-2">
|
||||
{editMode ? (
|
||||
<>
|
||||
<Button onClick={handleCancel} variant="secondary" disabled={saving}>
|
||||
<Button onClick={handleCancel} variant="outline" disabled={saving}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={saving}>
|
||||
@@ -258,7 +258,7 @@ export function BankProfileView() {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button onClick={loadData} variant="secondary" size="sm">
|
||||
<Button onClick={loadData} variant="outline" size="sm">
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
Refresh
|
||||
</Button>
|
||||
|
||||
@@ -266,7 +266,7 @@ function BankSelectorInner() {
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="secondary"
|
||||
variant="outline"
|
||||
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 text-foreground">Content *</label>
|
||||
<label className="font-bold block mb-1 text-sm">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 text-foreground">Context</label>
|
||||
<label className="font-bold block mb-1 text-sm">Context</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={docContext}
|
||||
@@ -317,17 +317,16 @@ function BankSelectorInner() {
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="font-bold block mb-1 text-sm text-foreground">Event Date</label>
|
||||
<label className="font-bold block mb-1 text-sm">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 text-foreground">Document ID</label>
|
||||
<label className="font-bold block mb-1 text-sm">Document ID</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={docDocumentId}
|
||||
@@ -343,7 +342,7 @@ function BankSelectorInner() {
|
||||
checked={docAsync}
|
||||
onCheckedChange={(checked) => setDocAsync(checked as boolean)}
|
||||
/>
|
||||
<label htmlFor="async-doc" className="text-sm cursor-pointer text-foreground">
|
||||
<label htmlFor="async-doc" className="text-sm cursor-pointer">
|
||||
Process in background (async)
|
||||
</label>
|
||||
</div>
|
||||
@@ -354,7 +353,7 @@ function BankSelectorInner() {
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="secondary"
|
||||
variant="outline"
|
||||
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 text-foreground">{row.text}</div>
|
||||
<div className="line-clamp-2 text-sm leading-snug">{row.text}</div>
|
||||
{row.context && (
|
||||
<div className="text-xs text-muted-foreground mt-0.5 truncate">{row.context}</div>
|
||||
)}
|
||||
@@ -506,10 +506,10 @@ export function DataView({ factType }: DataViewProps) {
|
||||
<span className="text-xs text-muted-foreground">-</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs py-2 text-foreground">
|
||||
<TableCell className="text-xs py-2">
|
||||
{occurredDisplay || <span className="text-muted-foreground">-</span>}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs py-2 text-foreground">
|
||||
<TableCell className="text-xs py-2">
|
||||
{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="secondary"
|
||||
variant="ghost"
|
||||
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="secondary"
|
||||
variant="ghost"
|
||||
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 text-foreground">
|
||||
<span className="text-[10px] px-2 min-w-[50px] text-center border-x border-border">
|
||||
{granularityLabels[granularity]}
|
||||
</span>
|
||||
<Button
|
||||
variant="secondary"
|
||||
variant="ghost"
|
||||
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="secondary"
|
||||
variant="ghost"
|
||||
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="secondary"
|
||||
variant="ghost"
|
||||
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 text-foreground">
|
||||
<span className="text-[10px] px-2 min-w-[60px] text-center border-x border-border">
|
||||
{currentIndex + 1} / {timelineGroups.length}
|
||||
</span>
|
||||
<Button
|
||||
variant="secondary"
|
||||
variant="ghost"
|
||||
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="secondary"
|
||||
variant="ghost"
|
||||
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 text-foreground">{data.id}</div>
|
||||
<div className="text-sm font-mono break-all">{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 text-foreground">
|
||||
<div className="text-sm">
|
||||
{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 text-foreground">{data.memory_unit_count}</div>
|
||||
<div className="text-sm">{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 text-foreground">
|
||||
<div className="text-sm">
|
||||
{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 text-foreground">
|
||||
<pre className="text-sm whitespace-pre-wrap font-mono">
|
||||
{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 text-foreground">
|
||||
<div className="text-sm font-mono break-all">
|
||||
{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 text-foreground">
|
||||
<div className="text-sm font-mono break-all">
|
||||
{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 text-foreground">{data.chunk_index}</div>
|
||||
<div className="text-sm">{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 text-foreground">
|
||||
<div className="text-sm">
|
||||
{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 text-foreground">
|
||||
<div className="text-sm">
|
||||
{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 text-foreground">
|
||||
<pre className="text-sm whitespace-pre-wrap font-mono">
|
||||
{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} className="text-card-foreground">
|
||||
<TableCell title={doc.id}>
|
||||
{doc.id.length > 30 ? doc.id.substring(0, 30) + '...' : doc.id}
|
||||
</TableCell>
|
||||
<TableCell className="text-card-foreground">
|
||||
<TableCell>
|
||||
{doc.created_at ? new Date(doc.created_at).toLocaleString() : 'N/A'}
|
||||
</TableCell>
|
||||
<TableCell className="text-card-foreground">
|
||||
<TableCell>
|
||||
{doc.retain_params?.context || '-'}
|
||||
</TableCell>
|
||||
<TableCell className="text-card-foreground">{doc.text_length?.toLocaleString()} chars</TableCell>
|
||||
<TableCell className="text-card-foreground">{doc.memory_unit_count}</TableCell>
|
||||
<TableCell>{doc.text_length?.toLocaleString()} chars</TableCell>
|
||||
<TableCell>{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' : 'secondary'}
|
||||
variant={selectedDocument?.id === doc.id ? 'default' : 'outline'}
|
||||
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="secondary"
|
||||
variant="outline"
|
||||
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 text-card-foreground">{selectedDocument.id}</div>
|
||||
<div className="text-sm font-mono break-all">{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 text-card-foreground">{new Date(selectedDocument.created_at).toLocaleString()}</div>
|
||||
<div className="text-sm font-medium">{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 text-card-foreground">{selectedDocument.memory_unit_count}</div>
|
||||
<div className="text-sm font-medium">{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 text-card-foreground">{selectedDocument.original_text.length.toLocaleString()} characters</div>
|
||||
<div className="text-sm font-medium">{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 text-card-foreground">
|
||||
<div className="text-sm space-y-2">
|
||||
{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 text-card-foreground">{JSON.stringify(selectedDocument.retain_params.metadata, null, 2)}</pre>
|
||||
<pre className="mt-1 text-xs bg-background p-2 rounded">{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 text-card-foreground">{selectedDocument.original_text}</pre>
|
||||
<pre className="text-sm whitespace-pre-wrap font-mono leading-relaxed">{selectedDocument.original_text}</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -92,9 +92,9 @@ export function EntitiesView() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex gap-4">
|
||||
{/* Entity List */}
|
||||
<div>
|
||||
<div className="flex-1">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<div className="text-center">
|
||||
@@ -111,6 +111,7 @@ export function EntitiesView() {
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>ID</TableHead>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Mentions</TableHead>
|
||||
<TableHead>First Seen</TableHead>
|
||||
@@ -122,14 +123,15 @@ export function EntitiesView() {
|
||||
<TableRow
|
||||
key={entity.id}
|
||||
onClick={() => loadEntityDetail(entity.id)}
|
||||
className={`cursor-pointer hover:bg-muted/50 ${
|
||||
selectedEntity?.id === entity.id ? 'bg-primary/10' : ''
|
||||
className={`cursor-pointer ${
|
||||
selectedEntity?.id === entity.id ? 'bg-accent' : ''
|
||||
}`}
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -147,81 +149,60 @@ export function EntitiesView() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Entity Detail Panel - Fixed overlay */}
|
||||
{/* Entity Detail Panel */}
|
||||
{selectedEntity && (
|
||||
<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>
|
||||
<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>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={regenerateObservations}
|
||||
disabled={regenerating}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => setSelectedEntity(null)}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<span className="text-lg">×</span>
|
||||
{regenerating ? 'Regenerating...' : 'Regenerate'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
{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 "Regenerate" to generate observations from facts.
|
||||
</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 "Regenerate" to generate observations from facts.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -49,9 +49,6 @@ 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';
|
||||
|
||||
@@ -67,7 +64,7 @@ export function MemoryDetailPanel({
|
||||
<p className="text-sm text-muted-foreground mt-1">Full memory content and metadata</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onClose}
|
||||
className="h-8 w-8 p-0"
|
||||
@@ -80,14 +77,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 text-foreground">{memory.text}</div>
|
||||
<div className="text-sm whitespace-pre-wrap leading-relaxed">{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 text-foreground">{memory.context}</div>
|
||||
<div className="text-sm">{memory.context}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -95,7 +92,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 text-foreground">
|
||||
<div className="text-sm font-medium">
|
||||
{memory.occurred_start
|
||||
? new Date(memory.occurred_start).toLocaleString()
|
||||
: 'N/A'}
|
||||
@@ -103,7 +100,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 text-foreground">
|
||||
<div className="text-sm font-medium">
|
||||
{memory.mentioned_at
|
||||
? new Date(memory.mentioned_at).toLocaleString()
|
||||
: 'N/A'}
|
||||
@@ -132,26 +129,24 @@ export function MemoryDetailPanel({
|
||||
)}
|
||||
|
||||
{/* ID */}
|
||||
{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 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>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Document/Chunk buttons */}
|
||||
{(memory.document_id || memory.chunk_id) && (
|
||||
@@ -159,7 +154,7 @@ export function MemoryDetailPanel({
|
||||
{memory.document_id && (
|
||||
<Button
|
||||
onClick={() => openDocumentModal(memory.document_id)}
|
||||
variant="secondary"
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
>
|
||||
View Document
|
||||
@@ -168,7 +163,7 @@ export function MemoryDetailPanel({
|
||||
{memory.chunk_id && (
|
||||
<Button
|
||||
onClick={() => openChunkModal(memory.chunk_id)}
|
||||
variant="secondary"
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
>
|
||||
View Chunk
|
||||
@@ -272,26 +267,24 @@ export function MemoryDetailPanel({
|
||||
)}
|
||||
|
||||
{/* ID */}
|
||||
{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 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>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Document/Chunk buttons */}
|
||||
{(memory.document_id || memory.chunk_id) && (
|
||||
@@ -300,7 +293,7 @@ export function MemoryDetailPanel({
|
||||
<Button
|
||||
onClick={() => openDocumentModal(memory.document_id)}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
variant="outline"
|
||||
className={`flex-1 ${compact ? 'h-7 text-xs' : ''}`}
|
||||
>
|
||||
View Document
|
||||
@@ -310,7 +303,7 @@ export function MemoryDetailPanel({
|
||||
<Button
|
||||
onClick={() => openChunkModal(memory.chunk_id)}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
variant="outline"
|
||||
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, ChevronDown, Database, FileText, Users, ArrowDown } from 'lucide-react';
|
||||
import { Search, Clock, Zap, ChevronRight, Database, FileText, Users } from 'lucide-react';
|
||||
import JsonView from 'react18-json-view';
|
||||
import 'react18-json-view/src/style.css';
|
||||
import { MemoryDetailPanel } from './memory-detail-panel';
|
||||
@@ -38,34 +38,6 @@ 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) {
|
||||
@@ -344,431 +316,55 @@ export function SearchDebugView() {
|
||||
|
||||
{/* Trace View */}
|
||||
{viewMode === 'trace' && trace && (
|
||||
<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 (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Recall Trace</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{/* Retrieval Methods */}
|
||||
{trace.retrieval_results && (
|
||||
<div>
|
||||
<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" />
|
||||
<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>
|
||||
<div className="text-2xl font-bold">{method.results?.length || 0}</div>
|
||||
<div className="text-xs text-muted-foreground">results</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
)}
|
||||
|
||||
{/* Step 2: RRF Merge */}
|
||||
{trace.rrf_merged && (() => {
|
||||
const stepKey = 'rrf-merge';
|
||||
const isExpanded = expandedSteps.has(stepKey);
|
||||
|
||||
return (
|
||||
{/* RRF Merge */}
|
||||
{trace.rrf_merged && (
|
||||
<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="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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 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;
|
||||
|
||||
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" />
|
||||
<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>
|
||||
);
|
||||
})()}
|
||||
)}
|
||||
|
||||
{/* Step 3: Combined Scoring */}
|
||||
{trace.reranked && (() => {
|
||||
const stepKey = 'reranking';
|
||||
const isExpanded = expandedSteps.has(stepKey);
|
||||
|
||||
return (
|
||||
{/* Reranking */}
|
||||
{trace.reranked && (
|
||||
<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" />
|
||||
<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>
|
||||
</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>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* JSON View */}
|
||||
|
||||
@@ -44,7 +44,7 @@ const DialogContent = React.forwardRef<
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<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">
|
||||
<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">
|
||||
<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-foreground",
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1,359 +0,0 @@
|
||||
#!/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()
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "hindsight-dev"
|
||||
version = "0.1.5"
|
||||
version = "0.1.4"
|
||||
description = "Development utilities for Hindsight"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
@@ -24,4 +24,3 @@ hindsight-api = { workspace = true }
|
||||
|
||||
[project.scripts]
|
||||
generate-openapi = "hindsight_dev.generate_openapi:generate_openapi_spec"
|
||||
generate-changelog = "hindsight_dev.generate_changelog:main"
|
||||
|
||||
@@ -4,35 +4,4 @@ sidebar_position: 1
|
||||
|
||||
# Changelog
|
||||
|
||||
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))
|
||||
Coming soon.
|
||||
|
||||
@@ -6,70 +6,14 @@ 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.
|
||||
@@ -78,13 +22,14 @@ Converts text into dense vector representations for semantic similarity search.
|
||||
|
||||
**Alternatives:**
|
||||
|
||||
| Model | Use Case |
|
||||
|-------|----------|
|
||||
| `BAAI/bge-small-en-v1.5` | Default, fast, good quality |
|
||||
| `sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2` | Multilingual (50+ languages) |
|
||||
| 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) |
|
||||
|
||||
:::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:**
|
||||
@@ -126,3 +71,44 @@ 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.
|
||||
|
||||
@@ -1,345 +0,0 @@
|
||||
---
|
||||
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
|
||||
+233
-9279
File diff suppressed because it is too large
Load Diff
@@ -147,18 +147,6 @@ const sidebars: SidebarsConfig = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Integrations',
|
||||
collapsible: false,
|
||||
items: [
|
||||
{
|
||||
type: 'doc',
|
||||
id: 'sdks/integrations/litellm',
|
||||
label: 'LiteLLM',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
cookbookSidebar: [
|
||||
{
|
||||
|
||||
@@ -514,27 +514,6 @@ 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_"] {
|
||||
|
||||
@@ -33,14 +33,9 @@ print_warning() {
|
||||
|
||||
print_banner() {
|
||||
echo ""
|
||||
# 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 -e "${BLUE}╔══════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${BLUE}║ HINDSIGHT CLI INSTALLER ║${NC}"
|
||||
echo -e "${BLUE}╚══════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
@@ -77,44 +72,16 @@ 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 version=$2
|
||||
local download_url="${REPO_URL}/releases/download/${version}/hindsight-${platform}"
|
||||
local download_url="${REPO_URL}/releases/latest/download/hindsight-${platform}"
|
||||
local tmp_file="/tmp/hindsight-$$"
|
||||
|
||||
print_info "Downloading Hindsight CLI ${version} for $platform..." >&2
|
||||
print_info "Downloading Hindsight CLI for $platform..." >&2
|
||||
|
||||
if command -v curl > /dev/null 2>&1; then
|
||||
# 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
|
||||
curl -fsSL "$download_url" -o "$tmp_file"
|
||||
elif command -v wget > /dev/null 2>&1; then
|
||||
wget -q "$download_url" -O "$tmp_file"
|
||||
else
|
||||
@@ -169,12 +136,8 @@ 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" "$version")
|
||||
tmp_file=$(download_binary "$platform")
|
||||
|
||||
# Install binary
|
||||
install_binary "$tmp_file"
|
||||
|
||||
@@ -1,799 +0,0 @@
|
||||
<?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>
|
||||
|
Before Width: | Height: | Size: 64 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
+371
-481
File diff suppressed because it is too large
Load Diff
@@ -1,437 +0,0 @@
|
||||
# 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
|
||||
@@ -1,817 +0,0 @@
|
||||
"""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",
|
||||
]
|
||||
@@ -1,640 +0,0 @@
|
||||
"""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
|
||||
@@ -1,232 +0,0 @@
|
||||
"""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
@@ -1,59 +0,0 @@
|
||||
[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"]
|
||||
@@ -1 +0,0 @@
|
||||
# Tests for hindsight-litellm
|
||||
@@ -1,471 +0,0 @@
|
||||
"""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
|
||||
Generated
-2188
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "hindsight-all"
|
||||
version = "0.1.5"
|
||||
version = "0.1.4"
|
||||
description = "All-in-one package for Hindsight - Semantic memory system with personality-driven thinking"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"name": "hindsight",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"hindsight-clients/typescript",
|
||||
"hindsight-control-plane",
|
||||
"hindsight-docs"
|
||||
]
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
#!/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 "$@"
|
||||
@@ -13,10 +13,13 @@ if [ ! -f "$ROOT_DIR/.env" ]; then
|
||||
fi
|
||||
|
||||
echo "🔨 Building TypeScript SDK first to ensure it's up to date..."
|
||||
npm run build -w @vectorize-io/hindsight-client
|
||||
cd "$ROOT_DIR/hindsight-clients/typescript" || exit 1
|
||||
npm run build
|
||||
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"
|
||||
@@ -30,4 +33,4 @@ fi
|
||||
export HOSTNAME="${HINDSIGHT_CP_HOSTNAME:-0.0.0.0}"
|
||||
|
||||
# Run dev server
|
||||
npm run dev -w hindsight-control-plane
|
||||
npm run dev
|
||||
@@ -7,11 +7,22 @@ set -e
|
||||
|
||||
# Get the project root directory
|
||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
cd "$PROJECT_ROOT" || exit 1
|
||||
DOCS_DIR="$PROJECT_ROOT/hindsight-docs"
|
||||
|
||||
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 -w hindsight-docs
|
||||
npm run start
|
||||
|
||||
+2
-2
@@ -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" "hindsight-integrations/litellm")
|
||||
PYTHON_PACKAGES=("hindsight-api" "hindsight-dev" "hindsight-dev/benchmarks" "hindsight")
|
||||
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, hindsight-litellm
|
||||
- Python packages: hindsight-api, hindsight-dev, hindsight-dev/benchmarks, hindsight-all
|
||||
- Python client: hindsight-clients/python
|
||||
- TypeScript client: hindsight-clients/typescript
|
||||
- Rust CLI: hindsight-cli
|
||||
|
||||
@@ -1,798 +0,0 @@
|
||||
#!/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()
|
||||
@@ -1141,7 +1141,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "hindsight-all"
|
||||
version = "0.1.5"
|
||||
version = "0.1.3"
|
||||
source = { editable = "hindsight" }
|
||||
dependencies = [
|
||||
{ name = "hindsight-api" },
|
||||
@@ -1165,7 +1165,7 @@ provides-extras = ["test"]
|
||||
|
||||
[[package]]
|
||||
name = "hindsight-api"
|
||||
version = "0.1.5"
|
||||
version = "0.1.3"
|
||||
source = { editable = "hindsight-api" }
|
||||
dependencies = [
|
||||
{ name = "alembic" },
|
||||
@@ -1182,7 +1182,6 @@ dependencies = [
|
||||
{ name = "opentelemetry-exporter-prometheus" },
|
||||
{ name = "opentelemetry-instrumentation-fastapi" },
|
||||
{ name = "opentelemetry-sdk" },
|
||||
{ name = "pg0-embedded" },
|
||||
{ name = "pgvector" },
|
||||
{ name = "psycopg2-binary" },
|
||||
{ name = "pydantic" },
|
||||
@@ -1223,7 +1222,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.3.0" },
|
||||
{ name = "fastmcp", specifier = ">=2.0.0" },
|
||||
{ name = "filelock", marker = "extra == 'test'", specifier = ">=3.0.0" },
|
||||
{ name = "google-genai", specifier = ">=1.0.0" },
|
||||
{ name = "greenlet", specifier = ">=3.2.4" },
|
||||
@@ -1234,7 +1233,6 @@ 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" },
|
||||
@@ -1245,11 +1243,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,<3.3.0" },
|
||||
{ name = "sentence-transformers", specifier = ">=3.0.0" },
|
||||
{ name = "sqlalchemy", specifier = ">=2.0.44" },
|
||||
{ name = "tiktoken", specifier = ">=0.12.0" },
|
||||
{ name = "torch", specifier = ">=2.0.0,<2.6.0" },
|
||||
{ name = "transformers", specifier = ">=4.30.0,<4.46.0" },
|
||||
{ name = "torch", specifier = ">=2.0.0" },
|
||||
{ name = "transformers", specifier = ">=4.30.0" },
|
||||
{ name = "uvicorn", specifier = ">=0.38.0" },
|
||||
{ name = "wsproto", specifier = ">=1.0.0" },
|
||||
]
|
||||
@@ -1267,7 +1265,7 @@ dev = [
|
||||
|
||||
[[package]]
|
||||
name = "hindsight-client"
|
||||
version = "0.1.5"
|
||||
version = "0.1.3"
|
||||
source = { editable = "hindsight-clients/python" }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
@@ -1299,7 +1297,7 @@ provides-extras = ["test"]
|
||||
|
||||
[[package]]
|
||||
name = "hindsight-dev"
|
||||
version = "0.1.5"
|
||||
version = "0.1.3"
|
||||
source = { editable = "hindsight-dev" }
|
||||
dependencies = [
|
||||
{ name = "hindsight-api" },
|
||||
@@ -2097,69 +2095,77 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "nvidia-cublas-cu12"
|
||||
version = "12.4.5.8"
|
||||
version = "12.8.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ 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 },
|
||||
{ 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 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvidia-cuda-cupti-cu12"
|
||||
version = "12.4.127"
|
||||
version = "12.8.90"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ 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 },
|
||||
{ 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 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvidia-cuda-nvrtc-cu12"
|
||||
version = "12.4.127"
|
||||
version = "12.8.93"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ 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 },
|
||||
{ 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 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvidia-cuda-runtime-cu12"
|
||||
version = "12.4.127"
|
||||
version = "12.8.90"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ 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 },
|
||||
{ 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 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvidia-cudnn-cu12"
|
||||
version = "9.1.0.70"
|
||||
version = "9.10.2.21"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "nvidia-cublas-cu12" },
|
||||
]
|
||||
wheels = [
|
||||
{ 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 },
|
||||
{ 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 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvidia-cufft-cu12"
|
||||
version = "11.2.1.3"
|
||||
version = "11.3.3.83"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "nvidia-nvjitlink-cu12" },
|
||||
]
|
||||
wheels = [
|
||||
{ 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 },
|
||||
{ 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 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvidia-curand-cu12"
|
||||
version = "10.3.5.147"
|
||||
version = "10.3.9.90"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ 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 },
|
||||
{ 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 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvidia-cusolver-cu12"
|
||||
version = "11.6.1.9"
|
||||
version = "11.7.3.90"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "nvidia-cublas-cu12" },
|
||||
@@ -2167,42 +2173,58 @@ dependencies = [
|
||||
{ name = "nvidia-nvjitlink-cu12" },
|
||||
]
|
||||
wheels = [
|
||||
{ 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 },
|
||||
{ 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 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvidia-cusparse-cu12"
|
||||
version = "12.3.1.170"
|
||||
version = "12.5.8.93"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "nvidia-nvjitlink-cu12" },
|
||||
]
|
||||
wheels = [
|
||||
{ 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 },
|
||||
{ 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 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvidia-nccl-cu12"
|
||||
version = "2.21.5"
|
||||
version = "2.27.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ 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 },
|
||||
{ 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 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvidia-nvjitlink-cu12"
|
||||
version = "12.4.127"
|
||||
version = "12.8.93"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ 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 },
|
||||
{ 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 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvidia-nvtx-cu12"
|
||||
version = "12.4.127"
|
||||
version = "12.8.90"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ 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 },
|
||||
{ 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 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2504,18 +2526,6 @@ 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"
|
||||
@@ -3820,7 +3830,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "sentence-transformers"
|
||||
version = "3.2.1"
|
||||
version = "5.1.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "huggingface-hub" },
|
||||
@@ -3830,10 +3840,11 @@ dependencies = [
|
||||
{ name = "torch" },
|
||||
{ name = "tqdm" },
|
||||
{ name = "transformers" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/de/61/708b20dedf26c460b416beb0acd5474c190dbca13e93b40858e99f17ac46/sentence_transformers-3.2.1.tar.gz", hash = "sha256:9fc38e620e5e1beba31d538a451778c9ccdbad77119d90f59f5bce49c4148e79", size = 202527 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0f/96/f3f3409179d14dbfdbea8622e2e9eaa3c8836ddcaecd2cd5ff0a11731d20/sentence_transformers-5.1.2.tar.gz", hash = "sha256:0f6c8bd916a78dc65b366feb8d22fd885efdb37432e7630020d113233af2b856", size = 375185 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/45/18/1ec591befcbdb2c97192a40fbe7c43a8b8a8b3c89b1fa101d3eeed4d79a4/sentence_transformers-3.2.1-py3-none-any.whl", hash = "sha256:c507e069eea33d15f1f2c72f74d7ea93abef298152cc235ab5af5e3a7584f738", size = 255758 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/a6/a607a737dc1a00b7afe267b9bfde101b8cee2529e197e57471d23137d4e5/sentence_transformers-5.1.2-py3-none-any.whl", hash = "sha256:724ce0ea62200f413f1a5059712aff66495bc4e815a1493f7f9bca242414c333", size = 488009 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3996,14 +4007,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "sympy"
|
||||
version = "1.13.1"
|
||||
version = "1.14.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mpmath" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ca/99/5a5b6f19ff9f083671ddf7b9632028436167cd3d33e11015754e41b249a4/sympy-1.13.1.tar.gz", hash = "sha256:9cebf7e04ff162015ce31c9c6c9144daa34a93bd082f54fd8f12deca4f47515f", size = 7533040 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/fe/81695a1aa331a842b582453b605175f419fe8540355886031328089d840a/sympy-1.13.1-py3-none-any.whl", hash = "sha256:db36cdc64bf61b9b24578b6f7bab1ecdd2452cf008f34faa33776680c26d66f8", size = 6189177 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4080,49 +4091,27 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "tokenizers"
|
||||
version = "0.20.3"
|
||||
version = "0.22.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "huggingface-hub" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/da/25/b1681c1c30ea3ea6e584ae3fffd552430b12faa599b558c4c4783f56d7ff/tokenizers-0.20.3.tar.gz", hash = "sha256:2278b34c5d0dd78e087e1ca7f9b1dcbf129d80211afa645f214bd6e051037539", size = 340513 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1c/46/fb6854cec3278fbfa4a75b50232c77622bc517ac886156e6afbfa4d8fc6e/tokenizers-0.22.1.tar.gz", hash = "sha256:61de6522785310a309b3407bac22d99c4db5dba349935e99e4d15ea2226af2d9", size = 363123 }
|
||||
wheels = [
|
||||
{ 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 },
|
||||
{ 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 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4136,7 +4125,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "torch"
|
||||
version = "2.5.1"
|
||||
version = "2.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "filelock" },
|
||||
@@ -4149,27 +4138,45 @@ 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 = "python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
wheels = [
|
||||
{ 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 },
|
||||
{ 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 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4205,7 +4212,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "transformers"
|
||||
version = "4.45.2"
|
||||
version = "4.57.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "filelock" },
|
||||
@@ -4219,21 +4226,22 @@ dependencies = [
|
||||
{ name = "tokenizers" },
|
||||
{ name = "tqdm" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4b/4c/3862b2dd6cdf83b187897bd351da0f7fb74d0df642b03c6f5d06353a3ca0/transformers-4.45.2.tar.gz", hash = "sha256:72bc390f6b203892561f05f86bbfaa0e234aab8e927a83e62b9d92ea7e3ae101", size = 8478357 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d6/68/a39307bcc4116a30b2106f2e689130a48de8bd8a1e635b5e1030e46fcd9e/transformers-4.57.1.tar.gz", hash = "sha256:f06c837959196c75039809636cd964b959f6604b75b8eeec6fdfc0440b89cc55", size = 10142511 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/9d/030cc1b3e88172967e22ee1d012e0d5e0384eb70d2a098d1669d549aea29/transformers-4.45.2-py3-none-any.whl", hash = "sha256:c551b33660cfc815bae1f9f097ecfd1e65be623f13c6ee0dda372bd881460210", size = 9881312 },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/d3/c16c3b3cf7655a67db1144da94b021c200ac1303f82428f2beef6c2e72bb/transformers-4.57.1-py3-none-any.whl", hash = "sha256:b10d05da8fa67dc41644dbbf9bc45a44cb86ae33da6f9295f5fbf5b7890bd267", size = 11990925 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "triton"
|
||||
version = "3.1.0"
|
||||
version = "3.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "filelock" },
|
||||
]
|
||||
wheels = [
|
||||
{ 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 },
|
||||
{ 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 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user