Compare commits

..
2 Commits
Author SHA1 Message Date
Nicolò Boschi 943c3663ba feat: add optional graph retriever MPFP 2025-12-12 16:34:34 +01:00
Nicolò Boschi 1d3a5b202f feat: add optional graph retriever MPFP 2025-12-12 16:34:30 +01:00
57 changed files with 15508 additions and 18565 deletions
+11
View File
@@ -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
+6 -3
View File
@@ -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
+48 -17
View File
@@ -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
View File
@@ -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"
-3
View File
@@ -9,9 +9,6 @@ wheels/
# Virtual environments
.venv
# Node
node_modules/
# Environment variables
.env
+4 -14
View File
@@ -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
+2 -2
View File
@@ -1,6 +1,6 @@
<div align="center">
![Hindsight Banner](./hindsight-docs/static/img/banner.svg)
![Hindsight Banner](./hindsight-docs/static/img/banner.webp)
[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.
+68 -21
View File
@@ -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 -2
View File
@@ -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
+5 -1
View File
@@ -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":
@@ -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)
+330 -58
View File
@@ -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()
+2 -3
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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"}
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@vectorize-io/hindsight-client",
"version": "0.1.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",
+1 -1
View File
@@ -2,7 +2,7 @@
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"lib": ["ES2020", "DOM"],
"declaration": true,
"outDir": "./dist",
"rootDir": "./",
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "hindsight-control-plane",
"version": "0.1.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);
}
@@ -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>
<code className="text-sm font-mono break-all text-foreground">{selectedDocument.id}</code>
</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 text-foreground">{new Date(selectedDocument.created_at).toLocaleString()}</div>
</div>
<div className="p-4 bg-muted/50 rounded-lg">
<div className="text-xs font-bold text-muted-foreground uppercase mb-2">Memory Units</div>
<div className="text-sm font-medium text-card-foreground">{selectedDocument.memory_unit_count}</div>
<div className="text-sm font-medium text-foreground">{selectedDocument.memory_unit_count}</div>
</div>
</div>
)}
@@ -214,7 +214,7 @@ export function DocumentsView() {
{selectedDocument.original_text && (
<div className="p-4 bg-muted/50 rounded-lg">
<div className="text-xs font-bold text-muted-foreground uppercase mb-2">Text Length</div>
<div className="text-sm font-medium text-card-foreground">{selectedDocument.original_text.length.toLocaleString()} characters</div>
<div className="text-sm font-medium text-foreground">{selectedDocument.original_text.length.toLocaleString()} characters</div>
</div>
)}
@@ -222,7 +222,7 @@ export function DocumentsView() {
{selectedDocument.retain_params && (
<div className="p-4 bg-muted/50 rounded-lg">
<div className="text-xs font-bold text-muted-foreground uppercase mb-2">Retain Parameters</div>
<div className="text-sm space-y-2 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 text-foreground">{selectedDocument.original_text}</pre>
</div>
</div>
)}
@@ -126,10 +126,10 @@ export function EntitiesView() {
selectedEntity?.id === entity.id ? 'bg-primary/10' : ''
}`}
>
<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="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>
@@ -154,7 +154,7 @@ export function EntitiesView() {
{/* 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>
<h3 className="text-xl font-bold text-foreground">{selectedEntity.canonical_name}</h3>
<p className="text-sm text-muted-foreground mt-1">Entity details</p>
</div>
<Button
@@ -172,11 +172,11 @@ export function EntitiesView() {
<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 className="text-lg font-semibold text-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 className="text-sm font-medium text-foreground">{formatDate(selectedEntity.first_seen)}</div>
</div>
</div>
@@ -206,7 +206,7 @@ export function EntitiesView() {
<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>
<div className="text-sm text-foreground">{obs.text}</div>
{obs.mentioned_at && (
<div className="text-xs text-muted-foreground mt-2">
{formatDate(obs.mentioned_at)}
@@ -67,7 +67,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 +80,14 @@ export function MemoryDetailPanel({
{/* Full Text */}
<div>
<div className="text-xs font-bold text-muted-foreground uppercase mb-2">Full Text</div>
<div className="text-sm whitespace-pre-wrap leading-relaxed 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 +95,7 @@ export function MemoryDetailPanel({
<div className="grid grid-cols-2 gap-4">
<div className="p-4 bg-muted/50 rounded-lg">
<div className="text-xs font-bold text-muted-foreground uppercase mb-2">Occurred</div>
<div className="text-sm font-medium text-foreground">
<div className="text-sm font-medium">
{memory.occurred_start
? new Date(memory.occurred_start).toLocaleString()
: 'N/A'}
@@ -103,7 +103,7 @@ export function MemoryDetailPanel({
</div>
<div className="p-4 bg-muted/50 rounded-lg">
<div className="text-xs font-bold text-muted-foreground uppercase mb-2">Mentioned</div>
<div className="text-sm font-medium text-foreground">
<div className="text-sm font-medium">
{memory.mentioned_at
? new Date(memory.mentioned_at).toLocaleString()
: 'N/A'}
@@ -159,7 +159,7 @@ export function MemoryDetailPanel({
{memory.document_id && (
<Button
onClick={() => openDocumentModal(memory.document_id)}
variant="secondary"
variant="outline"
className="flex-1"
>
View Document
@@ -168,7 +168,7 @@ export function MemoryDetailPanel({
{memory.chunk_id && (
<Button
onClick={() => openChunkModal(memory.chunk_id)}
variant="secondary"
variant="outline"
className="flex-1"
>
View Chunk
@@ -300,7 +300,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 +310,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
@@ -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()
+1 -2
View File
@@ -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"
+1 -32
View File
@@ -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.
+48 -62
View File
@@ -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
File diff suppressed because it is too large Load Diff
-12
View File
@@ -147,18 +147,6 @@ const sidebars: SidebarsConfig = {
},
],
},
{
type: 'category',
label: 'Integrations',
collapsible: false,
items: [
{
type: 'doc',
id: 'sdks/integrations/litellm',
label: 'LiteLLM',
},
],
},
],
cookbookSidebar: [
{
-21
View File
@@ -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_"] {
-799
View 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

File diff suppressed because it is too large Load Diff
-437
View File
@@ -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
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "hindsight-all"
version = "0.1.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"
-9
View File
@@ -1,9 +0,0 @@
{
"name": "hindsight",
"private": true,
"workspaces": [
"hindsight-clients/typescript",
"hindsight-control-plane",
"hindsight-docs"
]
}
-32
View File
@@ -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 "$@"
+5 -2
View File
@@ -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
+13 -2
View File
@@ -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
View File
@@ -65,7 +65,7 @@ fi
print_info "Updating version in all components..."
# Update Python packages
PYTHON_PACKAGES=("hindsight-api" "hindsight-dev" "hindsight-dev/benchmarks" "hindsight" "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
-798
View File
@@ -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()
Generated
+5 -19
View File
@@ -1141,7 +1141,7 @@ wheels = [
[[package]]
name = "hindsight-all"
version = "0.1.5"
version = "0.1.4"
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.4"
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" },
@@ -1267,7 +1265,7 @@ dev = [
[[package]]
name = "hindsight-client"
version = "0.1.5"
version = "0.1.4"
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.4"
source = { editable = "hindsight-dev" }
dependencies = [
{ name = "hindsight-api" },
@@ -2504,18 +2502,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"