Compare commits

..
Author SHA1 Message Date
Nicolò Boschi 5369c2b364 feat: delete document from ui 2025-12-23 13:45:11 +01:00
Nicolò Boschi 2ca97ab4a6 feat: delete document from ui 2025-12-23 13:36:42 +01:00
150 changed files with 12524 additions and 6283 deletions
-12
View File
@@ -2,23 +2,11 @@
# Copy this file to .env and fill in your values
# LLM Configuration (Required)
# Supported providers: openai, groq, ollama, gemini, anthropic, lmstudio
HINDSIGHT_API_LLM_PROVIDER=openai
HINDSIGHT_API_LLM_API_KEY=your-api-key-here
HINDSIGHT_API_LLM_MODEL=o3-mini
HINDSIGHT_API_LLM_BASE_URL=https://api.openai.com/v1
# Example: Anthropic Claude configuration
# HINDSIGHT_API_LLM_PROVIDER=anthropic
# HINDSIGHT_API_LLM_API_KEY=your-anthropic-api-key
# HINDSIGHT_API_LLM_MODEL=claude-sonnet-4-20250514
# Example: LM Studio local configuration (Qwen 2.5 32B recommended)
# HINDSIGHT_API_LLM_PROVIDER=lmstudio
# HINDSIGHT_API_LLM_API_KEY=lmstudio
# HINDSIGHT_API_LLM_BASE_URL=http://localhost:1234/v1
# HINDSIGHT_API_LLM_MODEL=qwen2.5-32b-instruct
# API Configuration (Optional)
HINDSIGHT_API_HOST=0.0.0.0
HINDSIGHT_API_PORT=8888
-71
View File
@@ -1,71 +0,0 @@
name: Bug Report
description: Report a bug or unexpected behavior
labels: ["bug", "triage"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to report a bug! Please fill out the sections below.
- type: textarea
id: description
attributes:
label: Bug Description
description: A clear and concise description of the bug
placeholder: What happened?
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Steps to Reproduce
description: Steps to reproduce the behavior
placeholder: |
1. Configure '...'
2. Call '...'
3. See error
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected Behavior
description: What did you expect to happen?
validations:
required: true
- type: textarea
id: actual
attributes:
label: Actual Behavior
description: What actually happened?
validations:
required: true
- type: input
id: version
attributes:
label: Version
description: What version are you using?
placeholder: e.g., 0.1.0 or commit hash
validations:
required: false
- type: dropdown
id: llm-provider
attributes:
label: LLM Provider
description: Which LLM provider are you using?
options:
- OpenAI
- Anthropic
- Gemini
- Groq
- Ollama
- LM Studio
- Other
validations:
required: false
-8
View File
@@ -1,8 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: Questions & Help
url: https://github.com/vectorize-io/hindsight/discussions/categories/q-a
about: Please ask questions and get help in Discussions instead of opening an issue.
- name: Ideas & Feedback
url: https://github.com/vectorize-io/hindsight/discussions/categories/ideas
about: Share ideas or give feedback in Discussions.
@@ -1,82 +0,0 @@
name: Feature Request
description: Suggest a new feature or enhancement
labels: ["enhancement", "triage"]
body:
- type: markdown
attributes:
value: |
Thanks for suggesting a feature! Please describe what you'd like to see added.
- type: textarea
id: use-case
attributes:
label: Use Case
description: Describe your specific use case. What are you building? What's your goal?
placeholder: |
I'm building an AI agent that needs to...
My application handles...
validations:
required: true
- type: textarea
id: problem
attributes:
label: Problem Statement
description: What problem are you facing? What's missing or difficult today?
placeholder: Currently I have to... which causes...
validations:
required: true
- type: textarea
id: benefit
attributes:
label: How This Feature Would Help
description: Explain how this feature would improve your workflow or solve your problem
placeholder: With this feature, I would be able to...
validations:
required: true
- type: textarea
id: solution
attributes:
label: Proposed Solution
description: Describe your ideal solution (optional - we may have ideas too!)
placeholder: It would be great if Hindsight could...
validations:
required: false
- type: textarea
id: alternatives
attributes:
label: Alternatives Considered
description: Have you considered any alternative solutions or workarounds?
validations:
required: false
- type: dropdown
id: priority
attributes:
label: Priority
description: How important is this feature to you?
options:
- Nice to have
- Important - affects my workflow
- Critical - blocking my use case
validations:
required: true
- type: textarea
id: additional
attributes:
label: Additional Context
description: Any other context, mockups, or examples?
validations:
required: false
- type: checkboxes
id: checklist
attributes:
label: Checklist
options:
- label: I would be willing to contribute this feature
required: false
+1 -165
View File
@@ -325,7 +325,6 @@ jobs:
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
HINDSIGHT_API_EMBEDDINGS_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)
@@ -614,97 +613,6 @@ jobs:
echo "=== API Server Logs ==="
cat /tmp/api-server.log || echo "No API server log found"
test-integration:
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
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
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: Build API
working-directory: ./hindsight-api
run: uv build
- name: Install API dependencies
working-directory: ./hindsight-api
run: uv sync --no-install-project --index-strategy unsafe-best-match
- name: Install integration test dependencies
working-directory: ./hindsight-integration-tests
run: uv sync
- name: Cache HuggingFace models
uses: actions/cache@v4
with:
path: ~/.cache/huggingface
key: ${{ runner.os }}-huggingface-${{ hashFiles('hindsight-api/pyproject.toml') }}
restore-keys: |
${{ runner.os }}-huggingface-
- name: Pre-download models
working-directory: ./hindsight-api
run: |
uv run python -c "
from sentence_transformers import SentenceTransformer, CrossEncoder
print('Downloading embedding model...')
SentenceTransformer('BAAI/bge-small-en-v1.5')
print('Downloading cross-encoder model...')
CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')
print('Models downloaded successfully')
"
- name: 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: Run integration tests
working-directory: ./hindsight-integration-tests
run: uv run pytest tests/ -v
- name: Show API server logs
if: always()
run: |
echo "=== API Server Logs ==="
cat /tmp/api-server.log || echo "No API server log found"
test-litellm-integration:
runs-on: ubuntu-latest
@@ -884,76 +792,4 @@ jobs:
if: always()
run: |
echo "=== API Server Logs ==="
cat /tmp/api-server.log || echo "No API server log found"
verify-generated-files:
runs-on: ubuntu-latest
env:
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
- 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: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
key: ${{ runner.os }}-cargo-gen-${{ hashFiles('**/Cargo.lock') }}
- name: Install Node dependencies
run: npm ci
- name: Install Python dependencies
run: |
cd hindsight-dev && uv sync --index-strategy unsafe-best-match
cd ../hindsight-api && uv sync --index-strategy unsafe-best-match
cd ../hindsight-embed && uv sync --index-strategy unsafe-best-match
- name: Run generate-openapi
run: ./scripts/generate-openapi.sh
- name: Run generate-clients
run: ./scripts/generate-clients.sh
- name: Run lint
run: ./scripts/hooks/lint.sh
- name: Verify no uncommitted changes
run: |
if [ -n "$(git status --porcelain)" ]; then
echo "❌ Error: Generated files are out of sync with committed files."
echo ""
echo "The following files have changed after running generation scripts:"
git status --porcelain
echo ""
echo "Please run the following commands locally and commit the changes:"
echo " ./scripts/generate-openapi.sh"
echo " ./scripts/generate-clients.sh"
echo " ./scripts/hooks/lint.sh"
echo ""
git diff --stat
exit 1
fi
echo "✓ All generated files are up to date"
cat /tmp/api-server.log || echo "No API server log found"
+1 -3
View File
@@ -12,10 +12,8 @@ wheels/
# Node
node_modules/
# Environment variables and local config
# Environment variables
.env
docker-compose.yml
docker-compose.override.yml
# IDE
.idea/
-146
View File
@@ -1,146 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Hindsight is an agent memory system that provides long-term memory for AI agents using biomimetic data structures. It stores memories as World facts, Experiences, Opinions, and Observations across memory banks.
## Development Commands
### API Server (Python/FastAPI)
```bash
# Start API server (loads .env automatically)
./scripts/dev/start-api.sh
# Run tests
cd hindsight-api && uv run pytest tests/
# Run specific test file
cd hindsight-api && uv run pytest tests/test_http_api_integration.py -v
# Lint
cd hindsight-api && uv run ruff check .
```
### Control Plane (Next.js)
```bash
./scripts/dev/start-control-plane.sh
# Or manually:
cd hindsight-control-plane && npm run dev
```
### Documentation Site (Docusaurus)
```bash
./scripts/dev/start-docs.sh
```
### Generating Clients/OpenAPI
```bash
# Regenerate OpenAPI spec after API changes
./scripts/generate-openapi.sh
# Regenerate all client SDKs (Python, TypeScript, Rust)
./scripts/generate-clients.sh
```
### Benchmarks
```bash
./scripts/benchmarks/run-longmemeval.sh
./scripts/benchmarks/run-locomo.sh
./scripts/benchmarks/start-visualizer.sh # View results at localhost:8001
```
## Architecture
### Monorepo Structure
- **hindsight-api/**: Core FastAPI server with memory engine (Python, uv)
- **hindsight/**: Embedded Python bundle (hindsight-all package)
- **hindsight-control-plane/**: Admin UI (Next.js, npm)
- **hindsight-cli/**: CLI tool (Rust, cargo)
- **hindsight-clients/**: Generated SDK clients (Python, TypeScript, Rust)
- **hindsight-docs/**: Docusaurus documentation site
- **hindsight-integrations/**: Framework integrations (LiteLLM, OpenAI)
- **hindsight-dev/**: Development tools and benchmarks
### Core Engine (hindsight-api/hindsight_api/engine/)
- `memory_engine.py`: Main orchestrator for retain/recall/reflect operations
- `llm_wrapper.py`: LLM abstraction supporting OpenAI, Anthropic, Gemini, Groq, Ollama, LM Studio
- `embeddings.py`: Embedding generation (local or TEI)
- `cross_encoder.py`: Reranking (local or TEI)
- `entity_resolver.py`: Entity extraction and normalization
- `query_analyzer.py`: Query intent analysis
- `retain/`: Memory ingestion pipeline
- `search/`: Multi-strategy retrieval (semantic, BM25, graph, temporal)
### API Layer (hindsight-api/hindsight_api/api/)
FastAPI routers for all endpoints. Main operations:
- **Retain**: Store memories, extracts facts/entities/relationships
- **Recall**: Retrieve memories via parallel search strategies + reranking
- **Reflect**: Deep analysis forming new opinions/observations
### Database
PostgreSQL with pgvector. Schema managed via Alembic migrations in `hindsight-api/hindsight_api/alembic/`. Migrations run automatically on API startup.
Key tables: `banks`, `memory_units`, `documents`, `entities`, `entity_links`
### Database Backups (IMPORTANT)
**Before any operation that may affect the database, run a backup:**
```bash
docker exec hindsight /backups/backup.sh
```
Operations requiring backup:
- Running database migrations
- Modifying Alembic migration files
- Rebuilding Docker images
- Resetting or recreating containers
- Any schema changes
- Bulk data operations
Backups are stored in `~/hindsight-backups/` on the host.
To restore:
```bash
docker exec -it hindsight /backups/restore.sh <backup-file.sql.gz>
```
## Key Conventions
### Memory Banks
- Each bank is isolated (no cross-bank data access)
- Banks have dispositions (skepticism, literalism, empathy traits 1-5) affecting reflect
- Banks can have background context
### API Design
- All endpoints operate on a single bank per request
- Multi-bank queries are client responsibility
- Disposition traits only affect reflect, not recall
### Python Style
- Python 3.11+, type hints required
- Async throughout (asyncpg, async FastAPI)
- Pydantic models for request/response
- Ruff for linting (line-length 120)
### TypeScript Style
- Next.js App Router for control plane
- Tailwind CSS with shadcn/ui components
## Environment Setup
```bash
cp .env.example .env
# Edit .env with LLM API key
# Python deps
uv sync --directory hindsight-api/
# Node deps (workspace)
npm install
```
Required env vars:
- `HINDSIGHT_API_LLM_PROVIDER`: openai, anthropic, gemini, groq, ollama, lmstudio
- `HINDSIGHT_API_LLM_API_KEY`: Your API key
- `HINDSIGHT_API_LLM_MODEL`: Model name (e.g., o3-mini, claude-sonnet-4-20250514)
+1 -30
View File
@@ -51,36 +51,7 @@ cd hindsight-api
uv run pytest tests/
```
### Code Style
We use [Ruff](https://docs.astral.sh/ruff/) for Python linting and formatting, and ESLint/Prettier for TypeScript.
#### Setting up git hooks (recommended)
Set up git hooks to automatically lint and format code before each commit:
```bash
./scripts/setup-hooks.sh
```
This configures git to use the hooks in `.githooks/`, which run all scripts in `scripts/hooks/` on commit. The lint hook runs in parallel:
- **Python**: `ruff check --fix`, `ruff format`, `ty check`
- **TypeScript**: `eslint --fix`, `prettier`
#### Manual linting and formatting
```bash
# Run all lints (same as pre-commit)
./scripts/hooks/lint.sh
# Or run individually for Python:
cd hindsight-api
uv run ruff check --fix . # Lint and auto-fix
uv run ruff format . # Format code
uv run ty check hindsight_api # Type check
```
#### Style guidelines
### Code style
- Use Python type hints
- Follow existing code patterns
-2
View File
@@ -81,8 +81,6 @@ docker run --rm -it --pull always -p 8888:8888 -p 9999:9999 \
ghcr.io/vectorize-io/hindsight:latest
```
You can modify the LLM provider by setting `HINDSIGHT_API_LLM_PROVIDER`. Valid options are `openai`, `anthropic`, `gemini`, `groq`, `ollama`, and `lmstudio`. The documentation provides more details on [supported models](https://hindsight.vectorize.io/developer/models).
API: http://localhost:8888
UI: http://localhost:9999
+29 -3
View File
@@ -125,6 +125,7 @@ FROM python:3.11-slim AS api-only
WORKDIR /app
# Install pg0 dependencies (procps provides 'kill' command needed by pg0)
# Note: libicu version varies by Debian version - try common versions in order
RUN apt-get update && apt-get install -y \
curl \
@@ -137,6 +138,7 @@ RUN apt-get update && apt-get install -y \
&& rm -rf /var/lib/apt/lists/* \
&& pip install --no-cache-dir uv
# Create non-root user (PostgreSQL cannot run as root)
RUN useradd -m -s /bin/bash hindsight
# Copy API with virtual environment from builder
@@ -146,12 +148,20 @@ COPY --from=api-builder /app/api /app/api
COPY docker/standalone/start-all.sh /app/start-all.sh
RUN chmod +x /app/start-all.sh
RUN chown -R hindsight:hindsight /app
# Create data directory for pg0 and set ownership
RUN mkdir -p /app/data && chown -R hindsight:hindsight /app
# Switch to non-root user
USER hindsight
# Set PATH for hindsight user
ENV PATH="/app/api/.venv/bin:${PATH}"
# Pre-cache PostgreSQL binaries by starting/stopping pg0-embedded
ENV PG0_HOME=/home/hindsight/.pg0-cache
ENV PG0_HOME=/home/hindsight/.pg0
# Pre-download ML models to avoid runtime download (conditional)
ARG PRELOAD_ML_MODELS
RUN if [ "$PRELOAD_ML_MODELS" = "true" ]; then \
@@ -216,7 +226,7 @@ FROM python:3.11-slim AS standalone
WORKDIR /app
# Install Node.js, curl, uv, and system dependencies
# Install Node.js, curl, uv, and pg0 dependencies (procps provides 'kill' command needed by pg0)
# Note: libicu version varies by Debian version - try common versions in order
RUN apt-get update && apt-get install -y \
curl \
@@ -231,6 +241,7 @@ RUN apt-get update && apt-get install -y \
&& rm -rf /var/lib/apt/lists/* \
&& pip install --no-cache-dir uv
# Create non-root user (PostgreSQL cannot run as root)
RUN useradd -m -s /bin/bash hindsight
# Copy API with virtual environment from builder
@@ -251,12 +262,27 @@ WORKDIR /app
COPY docker/standalone/start-all.sh /app/start-all.sh
RUN chmod +x /app/start-all.sh
RUN chown -R hindsight:hindsight /app
# Create data directory for pg0 and set ownership
RUN mkdir -p /app/data && chown -R hindsight:hindsight /app
# Switch to non-root user
USER hindsight
# Set PATH for hindsight user
ENV PATH="/app/api/.venv/bin:${PATH}"
# Pre-cache PostgreSQL binaries by starting/stopping pg0-embedded
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"
ENV PG0_HOME=/home/hindsight/.pg0
# Pre-download ML models to avoid runtime download (conditional)
ARG PRELOAD_ML_MODELS
RUN if [ "$PRELOAD_ML_MODELS" = "true" ]; then \
+9 -63
View File
@@ -5,70 +5,16 @@ set -e
ENABLE_API="${HINDSIGHT_ENABLE_API:-true}"
ENABLE_CP="${HINDSIGHT_ENABLE_CP:-true}"
# =============================================================================
# Dependency waiting (opt-in via HINDSIGHT_WAIT_FOR_DEPS=true)
#
# Problem: When running with LM Studio, the LLM may take time to load models.
# If Hindsight starts before LM Studio is ready, it fails on LLM verification.
# This wait loop ensures dependencies are ready before starting.
# =============================================================================
if [ "${HINDSIGHT_WAIT_FOR_DEPS:-false}" = "true" ]; then
LLM_BASE_URL="${HINDSIGHT_API_LLM_BASE_URL:-http://host.docker.internal:1234/v1}"
MAX_RETRIES="${HINDSIGHT_RETRY_MAX:-0}" # 0 = infinite
RETRY_INTERVAL="${HINDSIGHT_RETRY_INTERVAL:-10}"
# Check if external database is configured (skip check for embedded pg0)
SKIP_DB_CHECK=false
if [ -z "${HINDSIGHT_API_DATABASE_URL}" ]; then
SKIP_DB_CHECK=true
else
DB_CHECK_HOST=$(echo "$HINDSIGHT_API_DATABASE_URL" | sed -E 's|.*@([^:/]+):([0-9]+)/.*|\1 \2|')
# Copy pre-cached PostgreSQL data if runtime directory is empty (first run with volume)
if [ "$ENABLE_API" = "true" ]; then
PG0_CACHE="/home/hindsight/.pg0-cache"
PG0_HOME="/home/hindsight/.pg0"
if [ -d "$PG0_CACHE" ] && [ "$(ls -A $PG0_CACHE 2>/dev/null)" ]; then
if [ ! "$(ls -A $PG0_HOME 2>/dev/null)" ]; then
echo "📦 Copying pre-cached PostgreSQL data..."
cp -r "$PG0_CACHE"/* "$PG0_HOME"/ 2>/dev/null || true
fi
fi
check_db() {
if $SKIP_DB_CHECK; then
return 0
fi
if command -v pg_isready &> /dev/null; then
pg_isready -h $(echo $DB_CHECK_HOST | cut -d' ' -f1) -p $(echo $DB_CHECK_HOST | cut -d' ' -f2) &>/dev/null
else
python3 -c "import socket; s=socket.socket(); s.settimeout(5); exit(0 if s.connect_ex(('$(echo $DB_CHECK_HOST | cut -d' ' -f1)', $(echo $DB_CHECK_HOST | cut -d' ' -f2))) == 0 else 1)" 2>/dev/null
fi
}
check_llm() {
curl -sf "${LLM_BASE_URL}/models" --connect-timeout 5 &>/dev/null
}
echo "⏳ Waiting for dependencies to be ready..."
attempt=1
while true; do
db_ok=false
llm_ok=false
if check_db; then
db_ok=true
fi
if check_llm; then
llm_ok=true
fi
if $db_ok && $llm_ok; then
echo "✅ Dependencies ready!"
break
fi
if [ "$MAX_RETRIES" -ne 0 ] && [ "$attempt" -ge "$MAX_RETRIES" ]; then
echo "❌ Max retries ($MAX_RETRIES) reached. Dependencies not available."
exit 1
fi
echo " Attempt $attempt: DB=$( $db_ok && echo 'ok' || echo 'waiting' ), LLM=$( $llm_ok && echo 'ok' || echo 'waiting' )"
sleep "$RETRY_INTERVAL"
((attempt++))
done
fi
# Track PIDs for wait
+2 -2
View File
@@ -2,8 +2,8 @@ apiVersion: v2
name: hindsight
description: Hindsight helm chart
type: application
version: 0.2.1
appVersion: "0.2.1"
version: 0.1.14
appVersion: "0.1.14"
keywords:
- ai
- memory
+1 -1
View File
@@ -80,7 +80,7 @@ Configure via environment variables:
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_DATABASE_URL` | PostgreSQL connection string | `pg0` (embedded) |
| `HINDSIGHT_API_LLM_PROVIDER` | `openai`, `anthropic`, `gemini`, `groq`, `ollama`, `lmstudio` | `openai` |
| `HINDSIGHT_API_LLM_PROVIDER` | `openai`, `groq`, `gemini`, `ollama` | `openai` |
| `HINDSIGHT_API_LLM_API_KEY` | API key for LLM provider | - |
| `HINDSIGHT_API_LLM_MODEL` | Model name | `gpt-4o-mini` |
| `HINDSIGHT_API_HOST` | Server bind address | `0.0.0.0` |
+13 -37
View File
@@ -5,7 +5,6 @@ Provides both HTTP REST API and MCP (Model Context Protocol) server.
"""
import logging
from contextlib import asynccontextmanager
from typing import Optional
from fastapi import FastAPI
@@ -46,18 +45,6 @@ def create_app(
# Both HTTP and MCP
app = create_app(memory, mcp_api_enabled=True)
"""
mcp_app = None
# Create MCP app first if enabled (we need its lifespan for chaining)
if mcp_api_enabled:
try:
from .mcp import create_mcp_app
mcp_app = create_mcp_app(memory=memory)
except ImportError as e:
logger.error(f"MCP server requested but dependencies not available: {e}")
logger.error("Install with: pip install hindsight-api[mcp]")
raise
# Import and create HTTP API if enabled
if http_api_enabled:
@@ -70,31 +57,20 @@ def create_app(
app = FastAPI(title="Hindsight API", version="0.0.7")
logger.info("HTTP REST API disabled")
# Mount MCP server and chain its lifespan if enabled
if mcp_app is not None:
# Get the MCP app's underlying Starlette app for lifespan access
mcp_starlette_app = mcp_app.mcp_app
# Mount MCP server if enabled
if mcp_api_enabled:
try:
from .mcp import create_mcp_app
# Store the original lifespan
original_lifespan = app.router.lifespan_context
@asynccontextmanager
async def chained_lifespan(app_instance: FastAPI):
"""Chain the MCP lifespan with the main app lifespan."""
# Start MCP lifespan first
async with mcp_starlette_app.router.lifespan_context(mcp_starlette_app):
logger.info("MCP lifespan started")
# Then start the original app lifespan
async with original_lifespan(app_instance):
yield
logger.info("MCP lifespan stopped")
# Replace the app's lifespan with the chained version
app.router.lifespan_context = chained_lifespan
# Mount the MCP middleware
app.mount(mcp_mount_path, mcp_app)
logger.info(f"MCP server enabled at {mcp_mount_path}/")
# Create MCP app with dynamic bank_id support
# Supports: /mcp/{bank_id}/sse (bank-specific SSE endpoint)
mcp_app = create_mcp_app(memory=memory)
app.mount(mcp_mount_path, mcp_app)
logger.info(f"MCP server enabled at {mcp_mount_path}/{{bank_id}}/sse")
except ImportError as e:
logger.error(f"MCP server requested but dependencies not available: {e}")
logger.error("Install with: pip install hindsight-api[mcp]")
raise
return app
+9 -100
View File
@@ -14,8 +14,6 @@ from typing import Any
from fastapi import Depends, FastAPI, Header, HTTPException, Query
from hindsight_api.extensions import AuthenticationError
def _parse_metadata(metadata: Any) -> dict[str, Any]:
"""Parse metadata that may be a dict, JSON string, or None."""
@@ -37,7 +35,7 @@ from hindsight_api import MemoryEngine
from hindsight_api.engine.db_utils import acquire_with_retry
from hindsight_api.engine.memory_engine import Budget, fq_table
from hindsight_api.engine.response_models import VALID_RECALL_FACT_TYPES
from hindsight_api.extensions import HttpExtension, OperationValidationError, load_extension
from hindsight_api.extensions import HttpExtension, load_extension
from hindsight_api.metrics import create_metrics_collector, get_metrics_collector, initialize_metrics
from hindsight_api.models import RequestContext
@@ -281,13 +279,6 @@ class RecallResponse(BaseModel):
chunks: dict[str, ChunkData] | None = Field(default=None, description="Chunks for facts, keyed by chunk_id")
class EntityInput(BaseModel):
"""Entity to associate with retained content."""
text: str = Field(description="The entity name/text")
type: str | None = Field(default=None, description="Optional entity type (e.g., 'PERSON', 'ORG', 'CONCEPT')")
class MemoryItem(BaseModel):
"""Single memory item for retain."""
@@ -299,7 +290,6 @@ class MemoryItem(BaseModel):
"context": "team meeting",
"metadata": {"source": "slack", "channel": "engineering"},
"document_id": "meeting_notes_2024_01_15",
"entities": [{"text": "Alice"}, {"text": "ML model", "type": "CONCEPT"}],
}
},
)
@@ -309,10 +299,6 @@ class MemoryItem(BaseModel):
context: str | None = None
metadata: dict[str, str] | None = None
document_id: str | None = Field(default=None, description="Optional document ID for this memory item.")
entities: list[EntityInput] | None = Field(
default=None,
description="Optional entities to combine with auto-extracted entities.",
)
@field_validator("timestamp", mode="before")
@classmethod
@@ -399,16 +385,7 @@ class ReflectRequest(BaseModel):
"query": "What do you think about artificial intelligence?",
"budget": "low",
"context": "This is for a research paper on AI ethics",
"max_tokens": 4096,
"include": {"facts": {}},
"response_schema": {
"type": "object",
"properties": {
"summary": {"type": "string"},
"key_points": {"type": "array", "items": {"type": "string"}},
},
"required": ["summary", "key_points"],
},
}
}
)
@@ -416,14 +393,9 @@ class ReflectRequest(BaseModel):
query: str
budget: Budget = Budget.LOW
context: str | None = None
max_tokens: int = Field(default=4096, description="Maximum tokens for the response")
include: ReflectIncludeOptions = Field(
default_factory=ReflectIncludeOptions, description="Options for including additional data (disabled by default)"
)
response_schema: dict | None = Field(
default=None,
description="Optional JSON Schema for structured output. When provided, the response will include a 'structured_output' field with the LLM response parsed according to this schema.",
)
class OpinionItem(BaseModel):
@@ -468,20 +440,12 @@ class ReflectResponse(BaseModel):
{"id": "123", "text": "AI is used in healthcare", "type": "world"},
{"id": "456", "text": "I discussed AI applications last week", "type": "experience"},
],
"structured_output": {
"summary": "AI is transformative",
"key_points": ["Used in healthcare", "Discussed recently"],
},
}
}
)
text: str
based_on: list[ReflectFact] = [] # Facts used to generate the response
structured_output: dict | None = Field(
default=None,
description="Structured output parsed according to the request's response_schema. Only present when response_schema was provided in the request.",
)
class BanksResponse(BaseModel):
@@ -1003,16 +967,6 @@ def _register_routes(app: FastAPI):
api_key = authorization.strip()
return RequestContext(api_key=api_key)
# Global exception handler for authentication errors
@app.exception_handler(AuthenticationError)
async def authentication_error_handler(request, exc: AuthenticationError):
from fastapi.responses import JSONResponse
return JSONResponse(
status_code=401,
content={"detail": str(exc)},
)
@app.get(
"/health",
summary="Health check endpoint",
@@ -1060,8 +1014,6 @@ def _register_routes(app: FastAPI):
try:
data = await app.state.memory.get_graph_data(bank_id, type, request_context=request_context)
return data
except (AuthenticationError, HTTPException):
raise
except Exception as e:
import traceback
@@ -1108,8 +1060,6 @@ def _register_routes(app: FastAPI):
request_context=request_context,
)
return data
except (AuthenticationError, HTTPException):
raise
except Exception as e:
import traceback
@@ -1226,10 +1176,6 @@ def _register_routes(app: FastAPI):
)
except HTTPException:
raise
except OperationValidationError as e:
raise HTTPException(status_code=e.status_code, detail=e.reason)
except (AuthenticationError, HTTPException):
raise
except Exception as e:
import traceback
@@ -1265,8 +1211,6 @@ def _register_routes(app: FastAPI):
query=request.query,
budget=request.budget,
context=request.context,
max_tokens=request.max_tokens,
response_schema=request.response_schema,
request_context=request_context,
)
@@ -1289,13 +1233,8 @@ def _register_routes(app: FastAPI):
return ReflectResponse(
text=core_result.text,
based_on=based_on_facts,
structured_output=core_result.structured_output,
)
except OperationValidationError as e:
raise HTTPException(status_code=e.status_code, detail=e.reason)
except (AuthenticationError, HTTPException):
raise
except Exception as e:
import traceback
@@ -1316,8 +1255,6 @@ def _register_routes(app: FastAPI):
try:
banks = await app.state.memory.list_banks(request_context=request_context)
return BankListResponse(banks=banks)
except (AuthenticationError, HTTPException):
raise
except Exception as e:
import traceback
@@ -1441,8 +1378,6 @@ def _register_routes(app: FastAPI):
failed_operations=failed_operations,
)
except (AuthenticationError, HTTPException):
raise
except Exception as e:
import traceback
@@ -1467,8 +1402,6 @@ def _register_routes(app: FastAPI):
try:
entities = await app.state.memory.list_entities(bank_id, limit=limit, request_context=request_context)
return EntityListResponse(items=[EntityListItem(**e) for e in entities])
except (AuthenticationError, HTTPException):
raise
except Exception as e:
import traceback
@@ -1506,7 +1439,7 @@ def _register_routes(app: FastAPI):
for obs in entity["observations"]
],
)
except (AuthenticationError, HTTPException):
except HTTPException:
raise
except Exception as e:
import traceback
@@ -1559,7 +1492,7 @@ def _register_routes(app: FastAPI):
for obs in entity["observations"]
],
)
except (AuthenticationError, HTTPException):
except HTTPException:
raise
except Exception as e:
import traceback
@@ -1597,8 +1530,6 @@ def _register_routes(app: FastAPI):
bank_id=bank_id, search_query=q, limit=limit, offset=offset, request_context=request_context
)
return data
except (AuthenticationError, HTTPException):
raise
except Exception as e:
import traceback
@@ -1607,7 +1538,7 @@ def _register_routes(app: FastAPI):
raise HTTPException(status_code=500, detail=str(e))
@app.get(
"/v1/default/banks/{bank_id}/documents/{document_id:path}",
"/v1/default/banks/{bank_id}/documents/{document_id}",
response_model=DocumentResponse,
summary="Get document details",
description="Get a specific document including its original text",
@@ -1629,7 +1560,7 @@ def _register_routes(app: FastAPI):
if not document:
raise HTTPException(status_code=404, detail="Document not found")
return document
except (AuthenticationError, HTTPException):
except HTTPException:
raise
except Exception as e:
import traceback
@@ -1639,7 +1570,7 @@ def _register_routes(app: FastAPI):
raise HTTPException(status_code=500, detail=str(e))
@app.get(
"/v1/default/chunks/{chunk_id:path}",
"/v1/default/chunks/{chunk_id}",
response_model=ChunkResponse,
summary="Get chunk details",
description="Get a specific chunk by its ID",
@@ -1658,7 +1589,7 @@ def _register_routes(app: FastAPI):
if not chunk:
raise HTTPException(status_code=404, detail="Chunk not found")
return chunk
except (AuthenticationError, HTTPException):
except HTTPException:
raise
except Exception as e:
import traceback
@@ -1668,7 +1599,7 @@ def _register_routes(app: FastAPI):
raise HTTPException(status_code=500, detail=str(e))
@app.delete(
"/v1/default/banks/{bank_id}/documents/{document_id:path}",
"/v1/default/banks/{bank_id}/documents/{document_id}",
response_model=DeleteDocumentResponse,
summary="Delete a document",
description="Delete a document and all its associated memory units and links.\n\n"
@@ -1702,7 +1633,7 @@ def _register_routes(app: FastAPI):
document_id=document_id,
memory_units_deleted=result["memory_units_deleted"],
)
except (AuthenticationError, HTTPException):
except HTTPException:
raise
except Exception as e:
import traceback
@@ -1727,8 +1658,6 @@ def _register_routes(app: FastAPI):
bank_id=bank_id,
operations=[OperationResponse(**op) for op in operations],
)
except (AuthenticationError, HTTPException):
raise
except Exception as e:
import traceback
@@ -1759,8 +1688,6 @@ def _register_routes(app: FastAPI):
return CancelOperationResponse(**result)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
except (AuthenticationError, HTTPException):
raise
except Exception as e:
import traceback
@@ -1792,8 +1719,6 @@ def _register_routes(app: FastAPI):
disposition=DispositionTraits(**disposition_dict),
background=profile["background"],
)
except (AuthenticationError, HTTPException):
raise
except Exception as e:
import traceback
@@ -1832,8 +1757,6 @@ def _register_routes(app: FastAPI):
disposition=DispositionTraits(**disposition_dict),
background=profile["background"],
)
except (AuthenticationError, HTTPException):
raise
except Exception as e:
import traceback
@@ -1863,8 +1786,6 @@ def _register_routes(app: FastAPI):
response.disposition = DispositionTraits(**result["disposition"])
return response
except (AuthenticationError, HTTPException):
raise
except Exception as e:
import traceback
@@ -1916,8 +1837,6 @@ def _register_routes(app: FastAPI):
disposition=DispositionTraits(**disposition_dict),
background=final_profile["background"],
)
except (AuthenticationError, HTTPException):
raise
except Exception as e:
import traceback
@@ -1945,8 +1864,6 @@ def _register_routes(app: FastAPI):
+ result.get("entities_deleted", 0)
+ result.get("documents_deleted", 0),
)
except (AuthenticationError, HTTPException):
raise
except Exception as e:
import traceback
@@ -1998,8 +1915,6 @@ def _register_routes(app: FastAPI):
content_dict["metadata"] = item.metadata
if item.document_id:
content_dict["document_id"] = item.document_id
if item.entities:
content_dict["entities"] = [{"text": e.text, "type": e.type or "CONCEPT"} for e in item.entities]
contents.append(content_dict)
if request.async_:
@@ -2023,10 +1938,6 @@ def _register_routes(app: FastAPI):
return RetainResponse.model_validate(
{"success": True, "bank_id": bank_id, "items_count": len(contents), "async": False}
)
except OperationValidationError as e:
raise HTTPException(status_code=e.status_code, detail=e.reason)
except (AuthenticationError, HTTPException):
raise
except Exception as e:
import traceback
@@ -2065,8 +1976,6 @@ def _register_routes(app: FastAPI):
await app.state.memory.delete_bank(bank_id, fact_type=type, request_context=request_context)
return DeleteResponse(success=True)
except (AuthenticationError, HTTPException):
raise
except Exception as e:
import traceback
+50 -201
View File
@@ -8,7 +8,6 @@ from contextvars import ContextVar
from fastmcp import FastMCP
from hindsight_api import MemoryEngine
from hindsight_api.api.http import BankListItem, BankListResponse, BankProfileResponse, DispositionTraits
from hindsight_api.engine.response_models import VALID_RECALL_FACT_TYPES
from hindsight_api.models import RequestContext
@@ -28,15 +27,12 @@ logging.basicConfig(
)
logger = logging.getLogger(__name__)
# Default bank_id from environment variable
DEFAULT_BANK_ID = os.environ.get("HINDSIGHT_MCP_BANK_ID", "default")
# Context variable to hold the current bank_id
# Context variable to hold the current bank_id from the URL path
_current_bank_id: ContextVar[str | None] = ContextVar("current_bank_id", default=None)
def get_current_bank_id() -> str | None:
"""Get the current bank_id from context."""
"""Get the current bank_id from context (set from URL path)."""
return _current_bank_id.get()
@@ -48,13 +44,12 @@ def create_mcp_server(memory: MemoryEngine) -> FastMCP:
memory: MemoryEngine instance (required)
Returns:
Configured FastMCP server instance with stateless_http enabled
Configured FastMCP server instance
"""
# Use stateless_http=True for Claude Code compatibility
mcp = FastMCP("hindsight-mcp-server", stateless_http=True)
mcp = FastMCP("hindsight-mcp-server")
@mcp.tool()
async def retain(content: str, context: str = "general", bank_id: str | None = None) -> str:
async def retain(content: str, context: str = "general") -> str:
"""
Store important information to long-term memory.
@@ -70,24 +65,21 @@ def create_mcp_server(memory: MemoryEngine) -> FastMCP:
Args:
content: The fact/memory to store (be specific and include relevant details)
context: Category for the memory (e.g., 'preferences', 'work', 'hobbies', 'family'). Default: 'general'
bank_id: Optional bank to store in (defaults to session bank). Use for cross-bank operations.
"""
try:
target_bank = bank_id or get_current_bank_id()
if target_bank is None:
bank_id = get_current_bank_id()
if bank_id is None:
return "Error: No bank_id configured"
await memory.retain_batch_async(
bank_id=target_bank,
contents=[{"content": content, "context": context}],
request_context=RequestContext(),
bank_id=bank_id, contents=[{"content": content, "context": context}], request_context=RequestContext()
)
return f"Memory stored successfully in bank '{target_bank}'"
return "Memory stored successfully"
except Exception as e:
logger.error(f"Error storing memory: {e}", exc_info=True)
return f"Error: {str(e)}"
@mcp.tool()
async def recall(query: str, max_tokens: int = 4096, bank_id: str | None = None) -> str:
async def recall(query: str, max_results: int = 10) -> str:
"""
Search memories to provide personalized, context-aware responses.
@@ -99,184 +91,49 @@ def create_mcp_server(memory: MemoryEngine) -> FastMCP:
Args:
query: Natural language search query (e.g., "user's food preferences", "what projects is user working on")
max_tokens: Maximum tokens in the response (default: 4096)
bank_id: Optional bank to search in (defaults to session bank). Use for cross-bank operations.
max_results: Maximum number of results to return (default: 10)
"""
try:
target_bank = bank_id or get_current_bank_id()
if target_bank is None:
bank_id = get_current_bank_id()
if bank_id is None:
return "Error: No bank_id configured"
from hindsight_api.engine.memory_engine import Budget
recall_result = await memory.recall_async(
bank_id=target_bank,
search_result = await memory.recall_async(
bank_id=bank_id,
query=query,
fact_type=list(VALID_RECALL_FACT_TYPES),
budget=Budget.HIGH,
max_tokens=max_tokens,
budget=Budget.LOW,
request_context=RequestContext(),
)
# Use model's JSON serialization
return recall_result.model_dump_json(indent=2)
results = [
{
"id": fact.id,
"text": fact.text,
"type": fact.fact_type,
"context": fact.context,
"occurred_start": fact.occurred_start,
}
for fact in search_result.results[:max_results]
]
return json.dumps({"results": results}, indent=2)
except Exception as e:
logger.error(f"Error searching: {e}", exc_info=True)
return f'{{"error": "{e}", "results": []}}'
@mcp.tool()
async def reflect(query: str, context: str | None = None, budget: str = "low", bank_id: str | None = None) -> str:
"""
Generate thoughtful analysis by synthesizing stored memories with the bank's personality.
WHEN TO USE THIS TOOL:
Use reflect when you need reasoned analysis, not just fact retrieval. This tool
thinks through the question using everything the bank knows and its personality traits.
EXAMPLES OF GOOD QUERIES:
- "What patterns have emerged in how I approach debugging?"
- "Based on my past decisions, what architectural style do I prefer?"
- "What might be the best approach for this problem given what you know about me?"
- "How should I prioritize these tasks based on my goals?"
HOW IT DIFFERS FROM RECALL:
- recall: Returns raw facts matching your search (fast lookup)
- reflect: Reasons across memories to form a synthesized answer (deeper analysis)
Use recall for "what did I say about X?" and reflect for "what should I do about X?"
Args:
query: The question or topic to reflect on
context: Optional context about why this reflection is needed
budget: Search budget - 'low', 'mid', or 'high' (default: 'low')
bank_id: Optional bank to reflect in (defaults to session bank). Use for cross-bank operations.
"""
try:
target_bank = bank_id or get_current_bank_id()
if target_bank is None:
return "Error: No bank_id configured"
from hindsight_api.engine.memory_engine import Budget
# Map string budget to enum
budget_map = {"low": Budget.LOW, "mid": Budget.MID, "high": Budget.HIGH}
budget_enum = budget_map.get(budget.lower(), Budget.LOW)
reflect_result = await memory.reflect_async(
bank_id=target_bank,
query=query,
budget=budget_enum,
context=context,
request_context=RequestContext(),
)
return reflect_result.model_dump_json(indent=2)
except Exception as e:
logger.error(f"Error reflecting: {e}", exc_info=True)
return f'{{"error": "{e}", "text": ""}}'
@mcp.tool()
async def list_banks() -> str:
"""
List all available memory banks.
Use this to discover banks for orchestration or to find
the correct bank_id for cross-bank operations.
Returns:
JSON object with banks array containing bank_id, name, disposition, background, and timestamps
"""
try:
banks = await memory.list_banks(request_context=RequestContext())
bank_items = [
BankListItem(
bank_id=b.get("bank_id") or b.get("id"),
name=b.get("name"),
disposition=DispositionTraits(
**b.get("disposition", {"skepticism": 3, "literalism": 3, "empathy": 3})
),
background=b.get("background"),
created_at=str(b.get("created_at")) if b.get("created_at") else None,
updated_at=str(b.get("updated_at")) if b.get("updated_at") else None,
)
for b in banks
]
return BankListResponse(banks=bank_items).model_dump_json(indent=2)
except Exception as e:
logger.error(f"Error listing banks: {e}", exc_info=True)
return f'{{"error": "{e}", "banks": []}}'
@mcp.tool()
async def create_bank(bank_id: str, name: str | None = None, background: str | None = None) -> str:
"""
Create or update a memory bank.
Use this to create new banks for different agents, sessions, or purposes.
Banks are isolated memory stores - each bank has its own memories and personality.
Args:
bank_id: Unique identifier for the bank (e.g., 'orchestrator-memory', 'agent-1')
name: Human-readable name for the bank
background: Context about what this bank stores or its purpose
"""
try:
# Get or create the bank profile (auto-creates with defaults)
await memory.get_bank_profile(bank_id, request_context=RequestContext())
# Update name and/or background if provided
if name is not None or background is not None:
await memory.update_bank(bank_id, name=name, background=background, request_context=RequestContext())
# Get final profile and return using BankProfileResponse model
profile = await memory.get_bank_profile(bank_id, request_context=RequestContext())
disposition = profile.get("disposition")
if hasattr(disposition, "model_dump"):
disposition_traits = DispositionTraits(**disposition.model_dump())
else:
disposition_traits = DispositionTraits(
**dict(disposition or {"skepticism": 3, "literalism": 3, "empathy": 3})
)
response = BankProfileResponse(
bank_id=bank_id,
name=profile.get("name") or "",
disposition=disposition_traits,
background=profile.get("background") or "",
)
return response.model_dump_json(indent=2)
except Exception as e:
logger.error(f"Error creating bank: {e}", exc_info=True)
return json.dumps({"error": str(e)})
return json.dumps({"error": str(e), "results": []})
return mcp
class MCPMiddleware:
"""ASGI middleware that extracts bank_id from header or path and sets context.
Bank ID can be provided via:
1. X-Bank-Id header (recommended for Claude Code)
2. URL path: /mcp/{bank_id}/
3. Environment variable HINDSIGHT_MCP_BANK_ID (fallback default)
For Claude Code, configure with:
claude mcp add --transport http hindsight http://localhost:8888/mcp \\
--header "X-Bank-Id: my-bank"
"""
"""ASGI middleware that extracts bank_id from path and sets context."""
def __init__(self, app, memory: MemoryEngine):
self.app = app
self.memory = memory
self.mcp_server = create_mcp_server(memory)
self.mcp_app = self.mcp_server.http_app(path="/")
# Expose the lifespan for the parent app to chain
self.lifespan = self.mcp_app.lifespan_handler if hasattr(self.mcp_app, "lifespan_handler") else None
def _get_header(self, scope: dict, name: str) -> str | None:
"""Extract a header value from ASGI scope."""
name_lower = name.lower().encode()
for header_name, header_value in scope.get("headers", []):
if header_name.lower() == name_lower:
return header_value.decode()
return None
self.mcp_app = self.mcp_server.http_app()
async def __call__(self, scope, receive, send):
if scope["type"] != "http":
@@ -293,39 +150,32 @@ class MCPMiddleware:
# Also handle case where mount path wasn't stripped (e.g., /mcp/...)
if path.startswith("/mcp/"):
path = path[4:] # Remove /mcp prefix
elif path == "/mcp":
path = "/"
# Try to get bank_id from header first (for Claude Code compatibility)
bank_id = self._get_header(scope, "X-Bank-Id")
# Extract bank_id from path: /{bank_id}/ or /{bank_id}
# http_app expects requests at /
if not path.startswith("/") or len(path) <= 1:
# No bank_id in path - return error
await self._send_error(send, 400, "bank_id required in path: /mcp/{bank_id}/")
return
# MCP endpoint paths that should not be treated as bank_ids
MCP_ENDPOINTS = {"sse", "messages"}
# Extract bank_id from first path segment
parts = path[1:].split("/", 1)
if not parts[0]:
await self._send_error(send, 400, "bank_id required in path: /mcp/{bank_id}/")
return
# If no header, try to extract from path: /{bank_id}/...
new_path = path
if not bank_id and path.startswith("/") and len(path) > 1:
parts = path[1:].split("/", 1)
# Don't treat MCP endpoints as bank_ids
if parts[0] and parts[0] not in MCP_ENDPOINTS:
# First segment looks like a bank_id
bank_id = parts[0]
new_path = "/" + parts[1] if len(parts) > 1 else "/"
# Fall back to default bank_id
if not bank_id:
bank_id = DEFAULT_BANK_ID
logger.debug(f"Using default bank_id: {bank_id}")
bank_id = parts[0]
new_path = "/" + parts[1] if len(parts) > 1 else "/"
# Set bank_id context
token = _current_bank_id.set(bank_id)
try:
new_scope = scope.copy()
new_scope["path"] = new_path
# Clear root_path since we're passing directly to the app
new_scope["root_path"] = ""
# Wrap send to rewrite the SSE endpoint URL to include bank_id if using path-based routing
# Wrap send to rewrite the SSE endpoint URL to include bank_id
# The SSE app sends "event: endpoint\ndata: /messages\n" but we need
# the client to POST to /{bank_id}/messages instead
async def send_wrapper(message):
if message["type"] == "http.response.body":
body = message.get("body", b"")
@@ -361,10 +211,9 @@ def create_mcp_app(memory: MemoryEngine):
"""
Create an ASGI app that handles MCP requests.
Bank ID can be provided via:
1. X-Bank-Id header: claude mcp add --transport http hindsight http://localhost:8888/mcp --header "X-Bank-Id: my-bank"
2. URL path: /mcp/{bank_id}/
3. Environment variable HINDSIGHT_MCP_BANK_ID (fallback, default: "default")
URL pattern: /mcp/{bank_id}/
The bank_id is extracted from the URL path and made available to tools.
Args:
memory: MemoryEngine instance
+2 -34
View File
@@ -16,15 +16,10 @@ ENV_LLM_PROVIDER = "HINDSIGHT_API_LLM_PROVIDER"
ENV_LLM_API_KEY = "HINDSIGHT_API_LLM_API_KEY"
ENV_LLM_MODEL = "HINDSIGHT_API_LLM_MODEL"
ENV_LLM_BASE_URL = "HINDSIGHT_API_LLM_BASE_URL"
ENV_LLM_MAX_CONCURRENT = "HINDSIGHT_API_LLM_MAX_CONCURRENT"
ENV_LLM_TIMEOUT = "HINDSIGHT_API_LLM_TIMEOUT"
ENV_LLM_GROQ_SERVICE_TIER = "HINDSIGHT_API_LLM_GROQ_SERVICE_TIER"
ENV_EMBEDDINGS_PROVIDER = "HINDSIGHT_API_EMBEDDINGS_PROVIDER"
ENV_EMBEDDINGS_LOCAL_MODEL = "HINDSIGHT_API_EMBEDDINGS_LOCAL_MODEL"
ENV_EMBEDDINGS_TEI_URL = "HINDSIGHT_API_EMBEDDINGS_TEI_URL"
ENV_EMBEDDINGS_OPENAI_API_KEY = "HINDSIGHT_API_EMBEDDINGS_OPENAI_API_KEY"
ENV_EMBEDDINGS_OPENAI_MODEL = "HINDSIGHT_API_EMBEDDINGS_OPENAI_MODEL"
ENV_RERANKER_PROVIDER = "HINDSIGHT_API_RERANKER_PROVIDER"
ENV_RERANKER_LOCAL_MODEL = "HINDSIGHT_API_RERANKER_LOCAL_MODEL"
@@ -38,10 +33,6 @@ ENV_GRAPH_RETRIEVER = "HINDSIGHT_API_GRAPH_RETRIEVER"
ENV_MCP_LOCAL_BANK_ID = "HINDSIGHT_API_MCP_LOCAL_BANK_ID"
ENV_MCP_INSTRUCTIONS = "HINDSIGHT_API_MCP_INSTRUCTIONS"
# Observation thresholds
ENV_OBSERVATION_MIN_FACTS = "HINDSIGHT_API_OBSERVATION_MIN_FACTS"
ENV_OBSERVATION_TOP_ENTITIES = "HINDSIGHT_API_OBSERVATION_TOP_ENTITIES"
# Optimization flags
ENV_SKIP_LLM_VERIFICATION = "HINDSIGHT_API_SKIP_LLM_VERIFICATION"
ENV_LAZY_RERANKER = "HINDSIGHT_API_LAZY_RERANKER"
@@ -50,13 +41,9 @@ ENV_LAZY_RERANKER = "HINDSIGHT_API_LAZY_RERANKER"
DEFAULT_DATABASE_URL = "pg0"
DEFAULT_LLM_PROVIDER = "openai"
DEFAULT_LLM_MODEL = "gpt-5-mini"
DEFAULT_LLM_MAX_CONCURRENT = 32
DEFAULT_LLM_TIMEOUT = 120.0 # seconds
DEFAULT_EMBEDDINGS_PROVIDER = "local"
DEFAULT_EMBEDDINGS_LOCAL_MODEL = "BAAI/bge-small-en-v1.5"
DEFAULT_EMBEDDINGS_OPENAI_MODEL = "text-embedding-3-small"
DEFAULT_EMBEDDING_DIMENSION = 384
DEFAULT_RERANKER_PROVIDER = "local"
DEFAULT_RERANKER_LOCAL_MODEL = "cross-encoder/ms-marco-MiniLM-L-6-v2"
@@ -68,10 +55,6 @@ DEFAULT_MCP_ENABLED = True
DEFAULT_GRAPH_RETRIEVER = "bfs" # Options: "bfs", "mpfp"
DEFAULT_MCP_LOCAL_BANK_ID = "mcp"
# Observation thresholds
DEFAULT_OBSERVATION_MIN_FACTS = 5 # Min facts required to generate entity observations
DEFAULT_OBSERVATION_TOP_ENTITIES = 5 # Max entities to process per retain batch
# Default MCP tool descriptions (can be customized via env vars)
DEFAULT_MCP_RETAIN_DESCRIPTION = """Store important information to long-term memory.
@@ -92,8 +75,8 @@ Use this tool PROACTIVELY to:
- Remember user's goals and context
- Personalize responses based on past interactions"""
# Default embedding dimension (used by initial migration, adjusted at runtime)
EMBEDDING_DIMENSION = DEFAULT_EMBEDDING_DIMENSION
# Required embedding dimension for database schema
EMBEDDING_DIMENSION = 384
@dataclass
@@ -108,8 +91,6 @@ class HindsightConfig:
llm_api_key: str | None
llm_model: str
llm_base_url: str | None
llm_max_concurrent: int
llm_timeout: float
# Embeddings
embeddings_provider: str
@@ -130,10 +111,6 @@ class HindsightConfig:
# Recall
graph_retriever: str
# Observation thresholds
observation_min_facts: int
observation_top_entities: int
# Optimization flags
skip_llm_verification: bool
lazy_reranker: bool
@@ -149,8 +126,6 @@ class HindsightConfig:
llm_api_key=os.getenv(ENV_LLM_API_KEY),
llm_model=os.getenv(ENV_LLM_MODEL, DEFAULT_LLM_MODEL),
llm_base_url=os.getenv(ENV_LLM_BASE_URL) or None,
llm_max_concurrent=int(os.getenv(ENV_LLM_MAX_CONCURRENT, str(DEFAULT_LLM_MAX_CONCURRENT))),
llm_timeout=float(os.getenv(ENV_LLM_TIMEOUT, str(DEFAULT_LLM_TIMEOUT))),
# Embeddings
embeddings_provider=os.getenv(ENV_EMBEDDINGS_PROVIDER, DEFAULT_EMBEDDINGS_PROVIDER),
embeddings_local_model=os.getenv(ENV_EMBEDDINGS_LOCAL_MODEL, DEFAULT_EMBEDDINGS_LOCAL_MODEL),
@@ -169,11 +144,6 @@ class HindsightConfig:
# Optimization flags
skip_llm_verification=os.getenv(ENV_SKIP_LLM_VERIFICATION, "false").lower() == "true",
lazy_reranker=os.getenv(ENV_LAZY_RERANKER, "false").lower() == "true",
# Observation thresholds
observation_min_facts=int(os.getenv(ENV_OBSERVATION_MIN_FACTS, str(DEFAULT_OBSERVATION_MIN_FACTS))),
observation_top_entities=int(
os.getenv(ENV_OBSERVATION_TOP_ENTITIES, str(DEFAULT_OBSERVATION_TOP_ENTITIES))
),
)
def get_llm_base_url(self) -> str:
@@ -186,8 +156,6 @@ class HindsightConfig:
return "https://api.groq.com/openai/v1"
elif provider == "ollama":
return "http://localhost:11434/v1"
elif provider == "lmstudio":
return "http://localhost:1234/v1"
else:
return ""
+26 -176
View File
@@ -3,8 +3,8 @@ Embeddings abstraction for the memory system.
Provides an interface for generating embeddings with different backends.
The embedding dimension is auto-detected from the model at initialization.
The database schema is automatically adjusted to match the model's dimension.
IMPORTANT: All embeddings must produce 384-dimensional vectors to match
the database schema (pgvector column defined as vector(384)).
Configuration via environment variables - see hindsight_api.config for all env var names.
"""
@@ -17,14 +17,11 @@ import httpx
from ..config import (
DEFAULT_EMBEDDINGS_LOCAL_MODEL,
DEFAULT_EMBEDDINGS_OPENAI_MODEL,
DEFAULT_EMBEDDINGS_PROVIDER,
EMBEDDING_DIMENSION,
ENV_EMBEDDINGS_LOCAL_MODEL,
ENV_EMBEDDINGS_OPENAI_API_KEY,
ENV_EMBEDDINGS_OPENAI_MODEL,
ENV_EMBEDDINGS_PROVIDER,
ENV_EMBEDDINGS_TEI_URL,
ENV_LLM_API_KEY,
)
logger = logging.getLogger(__name__)
@@ -34,8 +31,8 @@ class Embeddings(ABC):
"""
Abstract base class for embedding generation.
The embedding dimension is determined by the model and detected at initialization.
The database schema is automatically adjusted to match the model's dimension.
All implementations MUST generate 384-dimensional embeddings to match
the database schema.
"""
@property
@@ -44,12 +41,6 @@ class Embeddings(ABC):
"""Return a human-readable name for this provider (e.g., 'local', 'tei')."""
pass
@property
@abstractmethod
def dimension(self) -> int:
"""Return the embedding dimension produced by this model."""
pass
@abstractmethod
async def initialize(self) -> None:
"""
@@ -63,13 +54,13 @@ class Embeddings(ABC):
@abstractmethod
def encode(self, texts: list[str]) -> list[list[float]]:
"""
Generate embeddings for a list of texts.
Generate 384-dimensional embeddings for a list of texts.
Args:
texts: List of text strings to encode
Returns:
List of embedding vectors (each is a list of floats)
List of 384-dimensional embedding vectors (each is a list of floats)
"""
pass
@@ -79,7 +70,9 @@ class LocalSTEmbeddings(Embeddings):
Local embeddings implementation using SentenceTransformers.
Call initialize() during startup to load the model and avoid cold starts.
The embedding dimension is auto-detected from the model.
Default model is BAAI/bge-small-en-v1.5 which produces 384-dimensional
embeddings matching the database schema.
"""
def __init__(self, model_name: str | None = None):
@@ -88,22 +81,16 @@ class LocalSTEmbeddings(Embeddings):
Args:
model_name: Name of the SentenceTransformer model to use.
Must produce 384-dimensional embeddings.
Default: BAAI/bge-small-en-v1.5
"""
self.model_name = model_name or DEFAULT_EMBEDDINGS_LOCAL_MODEL
self._model = None
self._dimension: int | None = None
@property
def provider_name(self) -> str:
return "local"
@property
def dimension(self) -> int:
if self._dimension is None:
raise RuntimeError("Embeddings not initialized. Call initialize() first.")
return self._dimension
async def initialize(self) -> None:
"""Load the embedding model."""
if self._model is not None:
@@ -125,18 +112,26 @@ class LocalSTEmbeddings(Embeddings):
model_kwargs={"low_cpu_mem_usage": False, "device_map": None},
)
self._dimension = self._model.get_sentence_embedding_dimension()
logger.info(f"Embeddings: local provider initialized (dim: {self._dimension})")
# Validate dimension matches database schema
model_dim = self._model.get_sentence_embedding_dimension()
if model_dim != EMBEDDING_DIMENSION:
raise ValueError(
f"Model {self.model_name} produces {model_dim}-dimensional embeddings, "
f"but database schema requires {EMBEDDING_DIMENSION} dimensions. "
f"Use a model that produces {EMBEDDING_DIMENSION}-dimensional embeddings."
)
logger.info(f"Embeddings: local provider initialized (dim: {model_dim})")
def encode(self, texts: list[str]) -> list[list[float]]:
"""
Generate embeddings for a list of texts.
Generate 384-dimensional embeddings for a list of texts.
Args:
texts: List of text strings to encode
Returns:
List of embedding vectors
List of 384-dimensional embedding vectors
"""
if self._model is None:
raise RuntimeError("Embeddings not initialized. Call initialize() first.")
@@ -151,7 +146,7 @@ class RemoteTEIEmbeddings(Embeddings):
TEI provides a high-performance inference server for embedding models.
See: https://github.com/huggingface/text-embeddings-inference
The embedding dimension is auto-detected from the server at initialization.
The server should be running a model that produces 384-dimensional embeddings.
"""
def __init__(
@@ -179,18 +174,11 @@ class RemoteTEIEmbeddings(Embeddings):
self.retry_delay = retry_delay
self._client: httpx.Client | None = None
self._model_id: str | None = None
self._dimension: int | None = None
@property
def provider_name(self) -> str:
return "tei"
@property
def dimension(self) -> int:
if self._dimension is None:
raise RuntimeError("Embeddings not initialized. Call initialize() first.")
return self._dimension
def _request_with_retry(self, method: str, url: str, **kwargs) -> httpx.Response:
"""Make an HTTP request with automatic retries on transient errors."""
import time
@@ -241,24 +229,7 @@ class RemoteTEIEmbeddings(Embeddings):
response = self._request_with_retry("GET", f"{self.base_url}/info")
info = response.json()
self._model_id = info.get("model_id", "unknown")
# Get dimension from server info or by doing a test embedding
if "max_input_length" in info and "model_dtype" in info:
# Try to get dimension from info endpoint (some TEI versions expose it)
# If not available, do a test embedding
pass
# Do a test embedding to detect dimension
test_response = self._request_with_retry(
"POST",
f"{self.base_url}/embed",
json={"inputs": ["test"]},
)
test_embeddings = test_response.json()
if test_embeddings and len(test_embeddings) > 0:
self._dimension = len(test_embeddings[0])
logger.info(f"Embeddings: TEI provider initialized (model: {self._model_id}, dim: {self._dimension})")
logger.info(f"Embeddings: TEI provider initialized (model: {self._model_id})")
except httpx.HTTPError as e:
raise RuntimeError(f"Failed to connect to TEI server at {self.base_url}: {e}")
@@ -298,117 +269,6 @@ class RemoteTEIEmbeddings(Embeddings):
return all_embeddings
class OpenAIEmbeddings(Embeddings):
"""
OpenAI embeddings implementation using the OpenAI API.
Supports text-embedding-3-small (1536 dims), text-embedding-3-large (3072 dims),
and text-embedding-ada-002 (1536 dims, legacy).
The embedding dimension is auto-detected from the model at initialization.
"""
# Known dimensions for OpenAI embedding models
MODEL_DIMENSIONS = {
"text-embedding-3-small": 1536,
"text-embedding-3-large": 3072,
"text-embedding-ada-002": 1536,
}
def __init__(
self,
api_key: str,
model: str = DEFAULT_EMBEDDINGS_OPENAI_MODEL,
batch_size: int = 100,
max_retries: int = 3,
):
"""
Initialize OpenAI embeddings client.
Args:
api_key: OpenAI API key
model: OpenAI embedding model name (default: text-embedding-3-small)
batch_size: Maximum batch size for embedding requests (default: 100)
max_retries: Maximum number of retries for failed requests (default: 3)
"""
self.api_key = api_key
self.model = model
self.batch_size = batch_size
self.max_retries = max_retries
self._client = None
self._dimension: int | None = None
@property
def provider_name(self) -> str:
return "openai"
@property
def dimension(self) -> int:
if self._dimension is None:
raise RuntimeError("Embeddings not initialized. Call initialize() first.")
return self._dimension
async def initialize(self) -> None:
"""Initialize the OpenAI client and detect dimension."""
if self._client is not None:
return
try:
from openai import OpenAI
except ImportError:
raise ImportError("openai is required for OpenAIEmbeddings. Install it with: pip install openai")
logger.info(f"Embeddings: initializing OpenAI provider with model {self.model}")
self._client = OpenAI(api_key=self.api_key, max_retries=self.max_retries)
# Try to get dimension from known models, otherwise do a test embedding
if self.model in self.MODEL_DIMENSIONS:
self._dimension = self.MODEL_DIMENSIONS[self.model]
else:
# Do a test embedding to detect dimension
response = self._client.embeddings.create(
model=self.model,
input=["test"],
)
if response.data:
self._dimension = len(response.data[0].embedding)
logger.info(f"Embeddings: OpenAI provider initialized (model: {self.model}, dim: {self._dimension})")
def encode(self, texts: list[str]) -> list[list[float]]:
"""
Generate embeddings using the OpenAI API.
Args:
texts: List of text strings to encode
Returns:
List of embedding vectors
"""
if self._client is None:
raise RuntimeError("Embeddings not initialized. Call initialize() first.")
if not texts:
return []
all_embeddings = []
# Process in batches
for i in range(0, len(texts), self.batch_size):
batch = texts[i : i + self.batch_size]
response = self._client.embeddings.create(
model=self.model,
input=batch,
)
# Sort by index to ensure correct order
batch_embeddings = sorted(response.data, key=lambda x: x.index)
all_embeddings.extend([e.embedding for e in batch_embeddings])
return all_embeddings
def create_embeddings_from_env() -> Embeddings:
"""
Create an Embeddings instance based on environment variables.
@@ -429,15 +289,5 @@ def create_embeddings_from_env() -> Embeddings:
model = os.environ.get(ENV_EMBEDDINGS_LOCAL_MODEL)
model_name = model or DEFAULT_EMBEDDINGS_LOCAL_MODEL
return LocalSTEmbeddings(model_name=model_name)
elif provider == "openai":
# Use dedicated embeddings API key, or fall back to LLM API key
api_key = os.environ.get(ENV_EMBEDDINGS_OPENAI_API_KEY) or os.environ.get(ENV_LLM_API_KEY)
if not api_key:
raise ValueError(
f"{ENV_EMBEDDINGS_OPENAI_API_KEY} or {ENV_LLM_API_KEY} is required "
f"when {ENV_EMBEDDINGS_PROVIDER} is 'openai'"
)
model = os.environ.get(ENV_EMBEDDINGS_OPENAI_MODEL, DEFAULT_EMBEDDINGS_OPENAI_MODEL)
return OpenAIEmbeddings(api_key=api_key, model=model)
else:
raise ValueError(f"Unknown embeddings provider: {provider}. Supported: 'local', 'tei', 'openai'")
raise ValueError(f"Unknown embeddings provider: {provider}. Supported: 'local', 'tei'")
@@ -110,8 +110,6 @@ class MemoryEngineInterface(ABC):
*,
budget: "Budget | None" = None,
context: str | None = None,
max_tokens: int = 4096,
response_schema: dict | None = None,
request_context: "RequestContext",
) -> "ReflectResult":
"""
@@ -122,8 +120,6 @@ class MemoryEngineInterface(ABC):
query: The question to reflect on.
budget: Search budget for retrieving context.
context: Additional context for the reflection.
max_tokens: Maximum tokens for the response.
response_schema: Optional JSON Schema for structured output.
request_context: Request context for authentication.
Returns:
+47 -286
View File
@@ -6,7 +6,6 @@ import asyncio
import json
import logging
import os
import re
import time
from typing import Any
@@ -16,14 +15,6 @@ from google.genai import errors as genai_errors
from google.genai import types as genai_types
from openai import APIConnectionError, APIStatusError, AsyncOpenAI, LengthFinishReasonError
from ..config import (
DEFAULT_LLM_MAX_CONCURRENT,
DEFAULT_LLM_TIMEOUT,
ENV_LLM_GROQ_SERVICE_TIER,
ENV_LLM_MAX_CONCURRENT,
ENV_LLM_TIMEOUT,
)
# Seed applied to every Groq request for deterministic behavior.
DEFAULT_LLM_SEED = 4242
@@ -33,9 +24,7 @@ logger = logging.getLogger(__name__)
logging.getLogger("httpx").setLevel(logging.WARNING)
# Global semaphore to limit concurrent LLM requests across all instances
# Set HINDSIGHT_API_LLM_MAX_CONCURRENT=1 for local LLMs (LM Studio, Ollama)
_llm_max_concurrent = int(os.getenv(ENV_LLM_MAX_CONCURRENT, str(DEFAULT_LLM_MAX_CONCURRENT)))
_global_llm_semaphore = asyncio.Semaphore(_llm_max_concurrent)
_global_llm_semaphore = asyncio.Semaphore(32)
class OutputTooLongError(Exception):
@@ -64,29 +53,25 @@ class LLMProvider:
base_url: str,
model: str,
reasoning_effort: str = "low",
groq_service_tier: str | None = None,
):
"""
Initialize LLM provider.
Args:
provider: Provider name ("openai", "groq", "ollama", "gemini", "anthropic", "lmstudio").
provider: Provider name ("openai", "groq", "ollama", "gemini").
api_key: API key.
base_url: Base URL for the API.
model: Model name.
reasoning_effort: Reasoning effort level for supported providers.
groq_service_tier: Groq service tier ("on_demand", "flex", "auto"). Default: None (uses Groq's default).
"""
self.provider = provider.lower()
self.api_key = api_key
self.base_url = base_url
self.model = model
self.reasoning_effort = reasoning_effort
# Default to 'auto' for best performance, users can override to 'on_demand' for free tier
self.groq_service_tier = groq_service_tier or os.getenv(ENV_LLM_GROQ_SERVICE_TIER, "auto")
# Validate provider
valid_providers = ["openai", "groq", "ollama", "gemini", "anthropic", "lmstudio"]
valid_providers = ["openai", "groq", "ollama", "gemini"]
if self.provider not in valid_providers:
raise ValueError(f"Invalid LLM provider: {self.provider}. Must be one of: {', '.join(valid_providers)}")
@@ -96,48 +81,25 @@ class LLMProvider:
self.base_url = "https://api.groq.com/openai/v1"
elif self.provider == "ollama":
self.base_url = "http://localhost:11434/v1"
elif self.provider == "lmstudio":
self.base_url = "http://localhost:1234/v1"
# Validate API key (not needed for ollama or lmstudio)
if self.provider not in ("ollama", "lmstudio") and not self.api_key:
# Validate API key (not needed for ollama)
if self.provider != "ollama" and not self.api_key:
raise ValueError(f"API key not found for {self.provider}")
# Get timeout config (set HINDSIGHT_API_LLM_TIMEOUT for local LLMs that need longer timeouts)
self.timeout = float(os.getenv(ENV_LLM_TIMEOUT, str(DEFAULT_LLM_TIMEOUT)))
# Create client based on provider
self._client = None
self._gemini_client = None
self._anthropic_client = None
if self.provider == "gemini":
self._gemini_client = genai.Client(api_key=self.api_key)
elif self.provider == "anthropic":
from anthropic import AsyncAnthropic
# Only pass base_url if it's set (Anthropic uses default URL otherwise)
anthropic_kwargs = {"api_key": self.api_key}
if self.base_url:
anthropic_kwargs["base_url"] = self.base_url
if self.timeout:
anthropic_kwargs["timeout"] = self.timeout
self._anthropic_client = AsyncAnthropic(**anthropic_kwargs)
elif self.provider in ("ollama", "lmstudio"):
# Use dummy key if not provided for local
api_key = self.api_key or "local"
client_kwargs = {"api_key": api_key, "base_url": self.base_url, "max_retries": 0}
if self.timeout:
client_kwargs["timeout"] = self.timeout
self._client = AsyncOpenAI(**client_kwargs)
self._client = None
elif self.provider == "ollama":
self._client = AsyncOpenAI(api_key="ollama", base_url=self.base_url, max_retries=0)
self._gemini_client = None
else:
# Only pass base_url if it's set (OpenAI uses default URL otherwise)
client_kwargs = {"api_key": self.api_key, "max_retries": 0}
if self.base_url:
client_kwargs["base_url"] = self.base_url
if self.timeout:
client_kwargs["timeout"] = self.timeout
self._client = AsyncOpenAI(**client_kwargs)
self._client = AsyncOpenAI(**client_kwargs) # type: ignore[invalid-argument-type] - dict kwargs
self._gemini_client = None
async def verify_connection(self) -> None:
"""
@@ -173,7 +135,6 @@ class LLMProvider:
initial_backoff: float = 1.0,
max_backoff: float = 60.0,
skip_validation: bool = False,
strict_schema: bool = False,
) -> Any:
"""
Make an LLM API call with retry logic.
@@ -188,7 +149,6 @@ class LLMProvider:
initial_backoff: Initial backoff time in seconds.
max_backoff: Maximum backoff time in seconds.
skip_validation: Return raw JSON without Pydantic validation.
strict_schema: Use strict JSON schema enforcement (OpenAI only). Guarantees all required fields.
Returns:
Parsed response if response_format is provided, otherwise text content.
@@ -206,19 +166,6 @@ class LLMProvider:
messages, response_format, max_retries, initial_backoff, max_backoff, skip_validation, start_time
)
# Handle Anthropic provider separately
if self.provider == "anthropic":
return await self._call_anthropic(
messages,
response_format,
max_completion_tokens,
max_retries,
initial_backoff,
max_backoff,
skip_validation,
start_time,
)
# Handle Ollama with native API for structured output (better schema enforcement)
if self.provider == "ollama" and response_format is not None:
return await self._call_ollama_native(
@@ -268,108 +215,58 @@ class LLMProvider:
# Provider-specific parameters
if self.provider == "groq":
call_params["seed"] = DEFAULT_LLM_SEED
extra_body: dict[str, Any] = {}
# Add service_tier if configured (requires paid plan for flex/auto)
if self.groq_service_tier:
extra_body["service_tier"] = self.groq_service_tier
# Add reasoning parameters for reasoning models
extra_body = {"service_tier": "auto"}
# Only add reasoning parameters for reasoning models
if is_reasoning_model:
extra_body["include_reasoning"] = False
if extra_body:
call_params["extra_body"] = extra_body
call_params["extra_body"] = extra_body
last_exception = None
for attempt in range(max_retries + 1):
try:
if response_format is not None:
schema = None
# Add schema to system message for JSON mode
if hasattr(response_format, "model_json_schema"):
schema = response_format.model_json_schema()
schema_msg = f"\n\nYou must respond with valid JSON matching this schema:\n{json.dumps(schema, indent=2)}"
if strict_schema and schema is not None:
# Use OpenAI's strict JSON schema enforcement
# This guarantees all required fields are returned
call_params["response_format"] = {
"type": "json_schema",
"json_schema": {
"name": "response",
"strict": True,
"schema": schema,
},
}
else:
# Soft enforcement: add schema to prompt and use json_object mode
if schema is not None:
schema_msg = f"\n\nYou must respond with valid JSON matching this schema:\n{json.dumps(schema, indent=2)}"
if call_params["messages"] and call_params["messages"][0].get("role") == "system":
call_params["messages"][0]["content"] += schema_msg
elif call_params["messages"]:
call_params["messages"][0]["content"] = (
schema_msg + "\n\n" + call_params["messages"][0]["content"]
)
if call_params["messages"] and call_params["messages"][0].get("role") == "system":
call_params["messages"][0]["content"] += schema_msg
elif call_params["messages"]:
call_params["messages"][0]["content"] = (
schema_msg + "\n\n" + call_params["messages"][0]["content"]
)
if self.provider not in ("lmstudio", "ollama"):
# LM Studio and Ollama don't support json_object response format reliably
# We rely on the schema in the system message instead
call_params["response_format"] = {"type": "json_object"}
logger.debug(f"Sending request to {self.provider}/{self.model} (timeout={self.timeout})")
call_params["response_format"] = {"type": "json_object"}
response = await self._client.chat.completions.create(**call_params)
logger.debug(f"Received response from {self.provider}/{self.model}")
content = response.choices[0].message.content
# Strip reasoning model thinking tags
# Supports: <think>, <thinking>, <reasoning>, |startthink|/|endthink|
# for reasoning models that embed thinking in their output (e.g., Qwen3, DeepSeek)
if content:
original_len = len(content)
content = re.sub(r"<think>.*?</think>", "", content, flags=re.DOTALL)
content = re.sub(r"<thinking>.*?</thinking>", "", content, flags=re.DOTALL)
content = re.sub(r"<reasoning>.*?</reasoning>", "", content, flags=re.DOTALL)
content = re.sub(r"\|startthink\|.*?\|endthink\|", "", content, flags=re.DOTALL)
content = content.strip()
if len(content) < original_len:
logger.debug(f"Stripped {original_len - len(content)} chars of reasoning tokens")
# For local models, they may wrap JSON in markdown code blocks
if self.provider in ("lmstudio", "ollama"):
clean_content = content
if "```json" in content:
clean_content = content.split("```json")[1].split("```")[0].strip()
elif "```" in content:
clean_content = content.split("```")[1].split("```")[0].strip()
try:
json_data = json.loads(clean_content)
except json.JSONDecodeError:
# Fallback to parsing raw content
json_data = json.loads(content)
else:
# Log raw LLM response for debugging JSON parse issues
try:
json_data = json.loads(content)
except json.JSONDecodeError as json_err:
# Truncate content for logging (first 500 and last 200 chars)
content_preview = content[:500] if content else "<empty>"
if content and len(content) > 700:
content_preview = f"{content[:500]}...TRUNCATED...{content[-200:]}"
logger.warning(
f"JSON parse error from LLM response (attempt {attempt + 1}/{max_retries + 1}): {json_err}\n"
f" Model: {self.provider}/{self.model}\n"
f" Content length: {len(content) if content else 0} chars\n"
f" Content preview: {content_preview!r}\n"
f" Finish reason: {response.choices[0].finish_reason if response.choices else 'unknown'}"
)
# Retry on JSON parse errors - LLM may return valid JSON on next attempt
if attempt < max_retries:
backoff = min(initial_backoff * (2**attempt), max_backoff)
await asyncio.sleep(backoff)
last_exception = json_err
continue
else:
logger.error(f"JSON parse error after {max_retries + 1} attempts, giving up")
raise
# Log raw LLM response for debugging JSON parse issues
try:
json_data = json.loads(content)
except json.JSONDecodeError as json_err:
# Truncate content for logging (first 500 and last 200 chars)
content_preview = content[:500] if content else "<empty>"
if content and len(content) > 700:
content_preview = f"{content[:500]}...TRUNCATED...{content[-200:]}"
logger.warning(
f"JSON parse error from LLM response (attempt {attempt + 1}/{max_retries + 1}): {json_err}\n"
f" Model: {self.provider}/{self.model}\n"
f" Content length: {len(content) if content else 0} chars\n"
f" Content preview: {content_preview!r}\n"
f" Finish reason: {response.choices[0].finish_reason if response.choices else 'unknown'}"
)
# Retry on JSON parse errors - LLM may return valid JSON on next attempt
if attempt < max_retries:
backoff = min(initial_backoff * (2**attempt), max_backoff)
await asyncio.sleep(backoff)
last_exception = json_err
continue
else:
logger.error(f"JSON parse error after {max_retries + 1} attempts, giving up")
raise
if skip_validation:
result = json_data
@@ -442,142 +339,6 @@ class LLMProvider:
raise last_exception
raise RuntimeError("LLM call failed after all retries with no exception captured")
async def _call_anthropic(
self,
messages: list[dict[str, str]],
response_format: Any | None,
max_completion_tokens: int | None,
max_retries: int,
initial_backoff: float,
max_backoff: float,
skip_validation: bool,
start_time: float,
) -> Any:
"""Handle Anthropic-specific API calls."""
from anthropic import APIConnectionError, APIStatusError, RateLimitError
# Convert OpenAI-style messages to Anthropic format
system_prompt = None
anthropic_messages = []
for msg in messages:
role = msg.get("role", "user")
content = msg.get("content", "")
if role == "system":
if system_prompt:
system_prompt += "\n\n" + content
else:
system_prompt = content
else:
anthropic_messages.append({"role": role, "content": content})
# Add JSON schema instruction if response_format is provided
if response_format is not None and hasattr(response_format, "model_json_schema"):
schema = response_format.model_json_schema()
schema_msg = f"\n\nYou must respond with valid JSON matching this schema:\n{json.dumps(schema, indent=2)}"
if system_prompt:
system_prompt += schema_msg
else:
system_prompt = schema_msg
# Prepare parameters
call_params = {
"model": self.model,
"messages": anthropic_messages,
"max_tokens": max_completion_tokens if max_completion_tokens is not None else 4096,
}
if system_prompt:
call_params["system"] = system_prompt
last_exception = None
for attempt in range(max_retries + 1):
try:
response = await self._anthropic_client.messages.create(**call_params)
# Anthropic response content is a list of blocks
content = ""
for block in response.content:
if block.type == "text":
content += block.text
if response_format is not None:
# Models may wrap JSON in markdown code blocks
clean_content = content
if "```json" in content:
clean_content = content.split("```json")[1].split("```")[0].strip()
elif "```" in content:
clean_content = content.split("```")[1].split("```")[0].strip()
try:
json_data = json.loads(clean_content)
except json.JSONDecodeError:
# Fallback to parsing raw content if markdown stripping failed
json_data = json.loads(content)
if skip_validation:
result = json_data
else:
result = response_format.model_validate(json_data)
else:
result = content
# Log slow calls
duration = time.time() - start_time
if duration > 10.0:
input_tokens = response.usage.input_tokens
output_tokens = response.usage.output_tokens
logger.info(
f"slow llm call: model={self.provider}/{self.model}, "
f"input_tokens={input_tokens}, output_tokens={output_tokens}, "
f"time={duration:.3f}s"
)
return result
except json.JSONDecodeError as e:
last_exception = e
if attempt < max_retries:
logger.warning("Anthropic returned invalid JSON, retrying...")
backoff = min(initial_backoff * (2**attempt), max_backoff)
await asyncio.sleep(backoff)
continue
else:
logger.error(f"Anthropic returned invalid JSON after {max_retries + 1} attempts")
raise
except (APIConnectionError, RateLimitError, APIStatusError) as e:
# Fast fail on 401/403
if isinstance(e, APIStatusError) and e.status_code in (401, 403):
logger.error(f"Anthropic auth error (HTTP {e.status_code}), not retrying: {str(e)}")
raise
last_exception = e
if attempt < max_retries:
# Check if it's a rate limit or server error
should_retry = isinstance(e, (APIConnectionError, RateLimitError)) or (
isinstance(e, APIStatusError) and e.status_code >= 500
)
if should_retry:
backoff = min(initial_backoff * (2**attempt), max_backoff)
jitter = backoff * 0.2 * (2 * (time.time() % 1) - 1)
await asyncio.sleep(backoff + jitter)
continue
logger.error(f"Anthropic API error after {max_retries + 1} attempts: {str(e)}")
raise
except Exception as e:
logger.error(f"Unexpected error during Anthropic call: {type(e).__name__}: {str(e)}")
raise
if last_exception:
raise last_exception
raise RuntimeError("Anthropic call failed after all retries")
async def _call_ollama_native(
self,
messages: list[dict[str, str]],
@@ -17,8 +17,6 @@ import uuid
from datetime import UTC, datetime, timedelta
from typing import TYPE_CHECKING, Any
from ..config import get_config
# Context variable for current schema (async-safe, per-task isolation)
_current_schema: contextvars.ContextVar[str] = contextvars.ContextVar("current_schema", default="public")
@@ -374,7 +372,7 @@ class MemoryEngine(MemoryEngineInterface):
result = await validation_coro
if not result.allowed:
raise OperationValidationError(result.reason or "Operation not allowed", result.status_code)
raise OperationValidationError(result.reason or "Operation not allowed")
async def _authenticate_tenant(self, request_context: "RequestContext | None") -> str:
"""
@@ -401,9 +399,7 @@ class MemoryEngine(MemoryEngineInterface):
if request_context is None:
raise AuthenticationError("RequestContext is required when tenant extension is configured")
# Let AuthenticationError propagate - HTTP layer will convert to 401
tenant_context = await self._tenant_extension.authenticate(request_context)
_current_schema.set(tenant_context.schema_name)
return tenant_context.schema_name
@@ -642,17 +638,13 @@ class MemoryEngine(MemoryEngineInterface):
# Run database migrations if enabled
if self._run_migrations:
from ..migrations import ensure_embedding_dimension, run_migrations
from ..migrations import run_migrations
if not self.db_url:
raise ValueError("Database URL is required for migrations")
logger.info("Running database migrations...")
run_migrations(self.db_url)
# Ensure embedding column dimension matches the model's dimension
# This is done after migrations and after embeddings.initialize()
ensure_embedding_dimension(self.db_url, self.embeddings.dimension)
logger.info(f"Connecting to PostgreSQL at {self.db_url}")
# Create connection pool
@@ -2833,16 +2825,13 @@ Guidelines:
Handler for form opinion tasks.
Args:
task_dict: Dict with keys: 'bank_id', 'answer_text', 'query', 'tenant_id'
task_dict: Dict with keys: 'bank_id', 'answer_text', 'query'
"""
bank_id = task_dict["bank_id"]
answer_text = task_dict["answer_text"]
query = task_dict["query"]
tenant_id = task_dict.get("tenant_id")
await self._extract_and_store_opinions_async(
bank_id=bank_id, answer_text=answer_text, query=query, tenant_id=tenant_id
)
await self._extract_and_store_opinions_async(bank_id=bank_id, answer_text=answer_text, query=query)
async def _handle_reinforce_opinion(self, task_dict: dict[str, Any]):
"""
@@ -3087,8 +3076,6 @@ Guidelines:
*,
budget: Budget | None = None,
context: str | None = None,
max_tokens: int = 4096,
response_schema: dict | None = None,
request_context: "RequestContext",
) -> ReflectResult:
"""
@@ -3100,22 +3087,19 @@ Guidelines:
3. Retrieves existing opinions (bank's formed perspectives)
4. Uses LLM to formulate an answer
5. Extracts and stores any new opinions formed during reflection
6. Optionally generates structured output based on response_schema
7. Returns plain text answer and the facts used
6. Returns plain text answer and the facts used
Args:
bank_id: bank identifier
query: Question to answer
budget: Budget level for memory exploration (low=100, mid=300, high=600 units)
context: Additional context string to include in LLM prompt (not used in recall)
response_schema: Optional JSON Schema for structured output
Returns:
ReflectResult containing:
- text: Plain text answer (no markdown)
- based_on: Dict with 'world', 'experience', and 'opinion' fact lists (MemoryFact objects)
- new_opinions: List of newly formed opinions
- structured_output: Optional dict if response_schema was provided
"""
# Use cached LLM config
if self._llm_config is None:
@@ -3193,53 +3177,21 @@ Guidelines:
log_buffer.append(f"[REFLECT {reflect_id}] Prompt: {len(prompt)} chars")
system_message = think_utils.get_system_message(disposition)
messages = [{"role": "system", "content": system_message}, {"role": "user", "content": prompt}]
# Prepare response_format if schema provided
response_format = None
if response_schema is not None:
# Wrapper class to provide Pydantic-like interface for raw JSON schemas
class JsonSchemaWrapper:
def __init__(self, schema: dict):
self._schema = schema
def model_json_schema(self):
return self._schema
response_format = JsonSchemaWrapper(response_schema)
llm_start = time.time()
result = await self._llm_config.call(
messages=messages,
scope="memory_reflect",
max_completion_tokens=max_tokens,
response_format=response_format,
skip_validation=True if response_format else False,
# Don't enforce strict_schema - not all providers support it and may retry forever
# Soft enforcement (schema in prompt + json_object mode) is sufficient
strict_schema=False,
answer_text = await self._llm_config.call(
messages=[{"role": "system", "content": system_message}, {"role": "user", "content": prompt}],
scope="memory_think",
temperature=0.9,
max_completion_tokens=1000,
)
llm_time = time.time() - llm_start
# Handle response based on whether structured output was requested
if response_schema is not None:
structured_output = result
answer_text = "" # Empty for backward compatibility
log_buffer.append(f"[REFLECT {reflect_id}] Structured output generated")
else:
structured_output = None
answer_text = result.strip()
answer_text = answer_text.strip()
# Submit form_opinion task for background processing
# Pass tenant_id from request context for internal authentication in background task
await self._task_backend.submit_task(
{
"type": "form_opinion",
"bank_id": bank_id,
"answer_text": answer_text,
"query": query,
"tenant_id": getattr(request_context, "tenant_id", None) if request_context else None,
}
{"type": "form_opinion", "bank_id": bank_id, "answer_text": answer_text, "query": query}
)
total_time = time.time() - reflect_start
@@ -3253,7 +3205,6 @@ Guidelines:
text=answer_text,
based_on={"world": world_results, "experience": agent_results, "opinion": opinion_results},
new_opinions=[], # Opinions are being extracted asynchronously
structured_output=structured_output,
)
# Call post-operation hook if validator is configured
@@ -3277,9 +3228,7 @@ Guidelines:
return result
async def _extract_and_store_opinions_async(
self, bank_id: str, answer_text: str, query: str, tenant_id: str | None = None
):
async def _extract_and_store_opinions_async(self, bank_id: str, answer_text: str, query: str):
"""
Background task to extract and store opinions from think response.
@@ -3289,7 +3238,6 @@ Guidelines:
bank_id: bank IDentifier
answer_text: The generated answer text
query: The original query
tenant_id: Tenant identifier for internal authentication
"""
try:
# Extract opinions from the answer
@@ -3300,11 +3248,10 @@ Guidelines:
from datetime import datetime
current_time = datetime.now(UTC)
# Use internal context with tenant_id for background authentication
# Extension can check internal=True to bypass normal auth
# Use internal request context for background tasks
from hindsight_api.models import RequestContext
internal_context = RequestContext(tenant_id=tenant_id, internal=True)
internal_context = RequestContext()
for opinion in new_opinions:
await self.retain_async(
bank_id=bank_id,
@@ -3625,7 +3572,7 @@ Guidelines:
self,
bank_id: str,
entity_ids: list[str],
min_facts: int | None = None,
min_facts: int = 5,
conn=None,
request_context: "RequestContext | None" = None,
) -> None:
@@ -3637,16 +3584,12 @@ Guidelines:
Args:
bank_id: Bank identifier
entity_ids: List of entity IDs to process
min_facts: Minimum facts required to regenerate observations (uses config default if None)
min_facts: Minimum facts required to regenerate observations
conn: Optional database connection (for transactional atomicity)
"""
if not bank_id or not entity_ids:
return
# Use config default if min_facts not specified
if min_facts is None:
min_facts = get_config().observation_min_facts
# Convert to UUIDs
entity_uuids = [uuid.UUID(eid) if isinstance(eid, str) else eid for eid in entity_ids]
@@ -123,8 +123,7 @@ class ReflectResult(BaseModel):
Result from a reflect operation.
Contains the formulated answer, the facts it was based on (organized by type),
any new opinions that were formed during the reflection process, and optionally
structured output if a response schema was provided.
and any new opinions that were formed during the reflection process.
"""
model_config = ConfigDict(
@@ -146,7 +145,6 @@ class ReflectResult(BaseModel):
"opinion": [],
},
"new_opinions": ["Machine learning has great potential in healthcare"],
"structured_output": {"summary": "ML in healthcare", "confidence": 0.9},
}
}
)
@@ -156,10 +154,6 @@ class ReflectResult(BaseModel):
description="Facts used to formulate the answer, organized by type (world, experience, opinion)"
)
new_opinions: list[str] = Field(default_factory=list, description="List of newly formed opinions during reflection")
structured_output: dict[str, Any] | None = Field(
default=None,
description="Structured output parsed according to the provided response schema. Only present when response_schema was provided.",
)
class Opinion(BaseModel):
@@ -13,23 +13,16 @@ logger = logging.getLogger(__name__)
async def process_entities_batch(
entity_resolver,
conn,
bank_id: str,
unit_ids: list[str],
facts: list[ProcessedFact],
log_buffer: list[str] = None,
user_entities_per_content: dict[int, list[dict]] = None,
entity_resolver, conn, bank_id: str, unit_ids: list[str], facts: list[ProcessedFact], log_buffer: list[str] = None
) -> list[EntityLink]:
"""
Process entities for all facts and create entity links.
This function:
1. Extracts entity mentions from fact texts
2. Merges user-provided entities with LLM-extracted entities
3. Resolves entity names to canonical entities
4. Creates entity records in the database
5. Returns entity links ready for insertion
2. Resolves entity names to canonical entities
3. Creates entity records in the database
4. Returns entity links ready for insertion
Args:
entity_resolver: EntityResolver instance for entity resolution
@@ -38,7 +31,6 @@ async def process_entities_batch(
unit_ids: List of unit IDs (same length as facts)
facts: List of ProcessedFact objects
log_buffer: Optional buffer for detailed logging
user_entities_per_content: Dict mapping content_index to list of user-provided entities
Returns:
List of EntityLink objects for batch insertion
@@ -49,35 +41,14 @@ async def process_entities_batch(
if len(unit_ids) != len(facts):
raise ValueError(f"Mismatch between unit_ids ({len(unit_ids)}) and facts ({len(facts)})")
user_entities_per_content = user_entities_per_content or {}
# Extract data for link_utils function
fact_texts = [fact.fact_text for fact in facts]
# Use occurred_start if available, otherwise use mentioned_at for entity timestamps
fact_dates = [fact.occurred_start if fact.occurred_start is not None else fact.mentioned_at for fact in facts]
# Convert EntityRef objects to dict format and merge with user-provided entities
entities_per_fact = []
for fact in facts:
# Start with LLM-extracted entities
llm_entities = [{"text": entity.name, "type": "CONCEPT"} for entity in (fact.entities or [])]
# Get user entities for this content (use content_index from fact)
user_entities = user_entities_per_content.get(fact.content_index, [])
# Merge with case-insensitive deduplication
seen_texts = {e["text"].lower() for e in llm_entities}
for user_entity in user_entities:
if user_entity["text"].lower() not in seen_texts:
llm_entities.append(
{
"text": user_entity["text"],
"type": user_entity.get("type", "CONCEPT"),
}
)
seen_texts.add(user_entity["text"].lower())
entities_per_fact.append(llm_entities)
# Convert EntityRef objects to dict format expected by link_utils
entities_per_fact = [
[{"text": entity.name, "type": "CONCEPT"} for entity in (fact.entities or [])] for fact in facts
]
# Use existing link_utils function for entity processing
entity_links = await link_utils.extract_entities_batch_optimized(
@@ -17,44 +17,6 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator
from ..llm_wrapper import LLMConfig, OutputTooLongError
def _infer_temporal_date(fact_text: str, event_date: datetime) -> str | None:
"""
Infer a temporal date from fact text when LLM didn't provide occurred_start.
This is a fallback for when the LLM fails to extract temporal information
from relative time expressions like "last night", "yesterday", etc.
"""
import re
fact_lower = fact_text.lower()
# Map relative time expressions to day offsets
temporal_patterns = {
r"\blast night\b": -1,
r"\byesterday\b": -1,
r"\btoday\b": 0,
r"\bthis morning\b": 0,
r"\bthis afternoon\b": 0,
r"\bthis evening\b": 0,
r"\btonigh?t\b": 0,
r"\btomorrow\b": 1,
r"\blast week\b": -7,
r"\bthis week\b": 0,
r"\bnext week\b": 7,
r"\blast month\b": -30,
r"\bthis month\b": 0,
r"\bnext month\b": 30,
}
for pattern, offset_days in temporal_patterns.items():
if re.search(pattern, fact_lower):
target_date = event_date + timedelta(days=offset_days)
return target_date.replace(hour=0, minute=0, second=0, microsecond=0).isoformat()
# If no relative time expression found, return None
return None
def _sanitize_text(text: str) -> str:
"""
Sanitize text by removing invalid Unicode surrogate characters.
@@ -714,18 +676,13 @@ Text:
if fact_kind == "event":
occurred_start = get_value("occurred_start")
occurred_end = get_value("occurred_end")
# If LLM didn't set temporal fields, try to extract them from the fact text
if not occurred_start:
fact_data["occurred_start"] = _infer_temporal_date(combined_text, event_date)
else:
if occurred_start:
fact_data["occurred_start"] = occurred_start
# For point events: if occurred_end not set, default to occurred_start
if occurred_end:
fact_data["occurred_end"] = occurred_end
elif fact_data.get("occurred_start"):
fact_data["occurred_end"] = fact_data["occurred_start"]
# For point events: if occurred_end not set, default to occurred_start
if occurred_end:
fact_data["occurred_end"] = occurred_end
else:
fact_data["occurred_end"] = occurred_start
# Add entities if present (validate as Entity objects)
# LLM sometimes returns strings instead of {"text": "..."} format
@@ -9,7 +9,6 @@ import time
import uuid
from datetime import UTC, datetime
from ...config import get_config
from ..memory_engine import fq_table
from ..search import observation_utils
from . import embedding_utils
@@ -50,9 +49,8 @@ async def regenerate_observations_batch(
entity_links: Entity links from this batch
log_buffer: Optional log buffer for timing
"""
config = get_config()
TOP_N_ENTITIES = config.observation_top_entities
MIN_FACTS_THRESHOLD = config.observation_min_facts
TOP_N_ENTITIES = 5
MIN_FACTS_THRESHOLD = 5
if not entity_links:
return
@@ -91,7 +91,6 @@ async def retain_batch(
context=item.get("context", ""),
event_date=item.get("event_date") or utcnow(),
metadata=item.get("metadata", {}),
entities=item.get("entities", []),
)
contents.append(content)
@@ -353,18 +352,8 @@ async def retain_batch(
# Process entities
step_start = time.time()
# Build map of content_index -> user entities for merging
user_entities_per_content = {
idx: content.entities for idx, content in enumerate(contents) if content.entities
}
entity_links = await entity_processing.process_entities_batch(
entity_resolver,
conn,
bank_id,
unit_ids,
non_duplicate_facts,
log_buffer,
user_entities_per_content=user_entities_per_content,
entity_resolver, conn, bank_id, unit_ids, non_duplicate_facts, log_buffer
)
log_buffer.append(f"[6] Process entities: {len(entity_links)} links in {time.time() - step_start:.3f}s")
@@ -20,7 +20,6 @@ class RetainContentDict(TypedDict, total=False):
event_date: When the content occurred (optional, defaults to now)
metadata: Custom key-value metadata (optional)
document_id: Document ID for this content item (optional)
entities: User-provided entities to merge with extracted entities (optional)
"""
content: str # Required
@@ -28,7 +27,6 @@ class RetainContentDict(TypedDict, total=False):
event_date: datetime
metadata: dict[str, str]
document_id: str
entities: list[dict[str, str]] # [{"text": "...", "type": "..."}]
def _now_utc() -> datetime:
@@ -48,7 +46,6 @@ class RetainContent:
context: str = ""
event_date: datetime = field(default_factory=_now_utc)
metadata: dict[str, str] = field(default_factory=dict)
entities: list[dict[str, str]] = field(default_factory=list) # User-provided entities
@dataclass
@@ -155,9 +152,6 @@ class ProcessedFact:
# DB fields (set after insertion)
unit_id: UUID | None = None
# Track which content this fact came from (for user entity merging)
content_index: int = 0
@property
def is_duplicate(self) -> bool:
"""Check if this fact was marked as a duplicate."""
@@ -200,7 +194,6 @@ class ProcessedFact:
entities=entities,
causal_relations=extracted_fact.causal_relations,
chunk_id=chunk_id,
content_index=extracted_fact.content_index,
)
@@ -98,14 +98,7 @@ class DefaultExtensionContext(ExtensionContext):
"""Run migrations for a specific schema."""
from hindsight_api.migrations import run_migrations
# Prefer getting URL from memory engine (handles pg0 case where URL is set after init)
db_url = self._database_url
if self._memory_engine is not None:
engine_url = getattr(self._memory_engine, "db_url", None)
if engine_url:
db_url = engine_url
run_migrations(db_url, schema=schema)
run_migrations(self._database_url, schema=schema)
def get_memory_engine(self) -> "MemoryEngineInterface":
"""Get the memory engine interface."""
@@ -17,9 +17,8 @@ if TYPE_CHECKING:
class OperationValidationError(Exception):
"""Raised when an operation fails validation."""
def __init__(self, reason: str, status_code: int = 403):
def __init__(self, reason: str):
self.reason = reason
self.status_code = status_code
super().__init__(f"Operation validation failed: {reason}")
@@ -29,7 +28,6 @@ class ValidationResult:
allowed: bool
reason: str | None = None
status_code: int = 403 # Default to Forbidden
@classmethod
def accept(cls) -> "ValidationResult":
@@ -37,9 +35,9 @@ class ValidationResult:
return cls(allowed=True)
@classmethod
def reject(cls, reason: str, status_code: int = 403) -> "ValidationResult":
"""Create a rejected validation result with a reason and HTTP status code."""
return cls(allowed=False, reason=reason, status_code=status_code)
def reject(cls, reason: str) -> "ValidationResult":
"""Create a rejected validation result with a reason."""
return cls(allowed=False, reason=reason)
# =============================================================================
+1 -29
View File
@@ -31,7 +31,6 @@ from .daemon import (
IdleTimeoutMiddleware,
daemonize,
)
from .extensions import DefaultExtensionContext, OperationValidatorExtension, TenantExtension, load_extension
# Filter deprecation warnings from third-party libraries
warnings.filterwarnings("ignore", message="websockets.legacy is deprecated")
@@ -169,8 +168,6 @@ def main():
llm_api_key=config.llm_api_key,
llm_model=config.llm_model,
llm_base_url=config.llm_base_url,
llm_max_concurrent=config.llm_max_concurrent,
llm_timeout=config.llm_timeout,
embeddings_provider=config.embeddings_provider,
embeddings_local_model=config.embeddings_local_model,
embeddings_tei_url=config.embeddings_tei_url,
@@ -182,8 +179,6 @@ def main():
log_level=args.log_level,
mcp_enabled=config.mcp_enabled,
graph_retriever=config.graph_retriever,
observation_min_facts=config.observation_min_facts,
observation_top_entities=config.observation_top_entities,
skip_llm_verification=config.skip_llm_verification,
lazy_reranker=config.lazy_reranker,
)
@@ -196,31 +191,8 @@ def main():
signal.signal(signal.SIGINT, _signal_handler)
signal.signal(signal.SIGTERM, _signal_handler)
# Load operation validator extension if configured
operation_validator = load_extension("OPERATION_VALIDATOR", OperationValidatorExtension)
if operation_validator:
import logging
logging.info(f"Loaded operation validator: {operation_validator.__class__.__name__}")
# Load tenant extension if configured
tenant_extension = load_extension("TENANT", TenantExtension)
if tenant_extension:
import logging
logging.info(f"Loaded tenant extension: {tenant_extension.__class__.__name__}")
# Create MemoryEngine (reads configuration from environment)
_memory = MemoryEngine(operation_validator=operation_validator, tenant_extension=tenant_extension)
# Set extension context on tenant extension (needed for schema provisioning)
if tenant_extension:
extension_context = DefaultExtensionContext(
database_url=config.database_url,
memory_engine=_memory,
)
tenant_extension.set_context(extension_context)
logging.info("Extension context set on tenant extension")
_memory = MemoryEngine()
# Create FastAPI app
app = create_app(
-128
View File
@@ -229,131 +229,3 @@ def check_migration_status(
except Exception as e:
logger.warning(f"Unable to check migration status: {e}")
return None, None
def ensure_embedding_dimension(
database_url: str,
required_dimension: int,
schema: str | None = None,
) -> None:
"""
Ensure the embedding column dimension matches the model's dimension.
This function checks the current vector column dimension in the database
and adjusts it if necessary:
- If dimensions match: no action needed
- If dimensions differ and table is empty: ALTER COLUMN to new dimension
- If dimensions differ and table has data: raise error with migration guidance
Args:
database_url: SQLAlchemy database URL
required_dimension: The embedding dimension required by the model
schema: Target PostgreSQL schema name (None for public)
Raises:
RuntimeError: If dimension mismatch with existing data
"""
schema_name = schema or "public"
engine = create_engine(database_url)
with engine.connect() as conn:
# Check if memory_units table exists
table_exists = conn.execute(
text("""
SELECT EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_schema = :schema AND table_name = 'memory_units'
)
"""),
{"schema": schema_name},
).scalar()
if not table_exists:
logger.debug(f"memory_units table does not exist in schema '{schema_name}', skipping dimension check")
return
# Get current column dimension from pg_attribute
# pgvector stores dimension in atttypmod
current_dim = conn.execute(
text("""
SELECT atttypmod
FROM pg_attribute a
JOIN pg_class c ON a.attrelid = c.oid
JOIN pg_namespace n ON c.relnamespace = n.oid
WHERE n.nspname = :schema
AND c.relname = 'memory_units'
AND a.attname = 'embedding'
"""),
{"schema": schema_name},
).scalar()
if current_dim is None:
logger.warning("Could not determine current embedding dimension, skipping check")
return
# pgvector stores dimension directly in atttypmod (no offset like other types)
current_dimension = current_dim
if current_dimension == required_dimension:
logger.debug(f"Embedding dimension OK: {current_dimension}")
return
logger.info(
f"Embedding dimension mismatch: database has {current_dimension}, model requires {required_dimension}"
)
# Check if table has data
row_count = conn.execute(
text(f"SELECT COUNT(*) FROM {schema_name}.memory_units WHERE embedding IS NOT NULL")
).scalar()
if row_count > 0:
raise RuntimeError(
f"Cannot change embedding dimension from {current_dimension} to {required_dimension}: "
f"memory_units table contains {row_count} rows with embeddings. "
f"To change dimensions, you must either:\n"
f" 1. Re-embed all data: DELETE FROM {schema_name}.memory_units; then restart\n"
f" 2. Use a model with {current_dimension}-dimensional embeddings"
)
# Table is empty, safe to alter column
logger.info(f"Altering embedding column dimension from {current_dimension} to {required_dimension}")
# Drop the HNSW index on embedding column if it exists
# Only drop indexes that use 'hnsw' and reference the 'embedding' column
conn.execute(
text(f"""
DO $$
DECLARE idx_name TEXT;
BEGIN
FOR idx_name IN
SELECT indexname FROM pg_indexes
WHERE schemaname = '{schema_name}'
AND tablename = 'memory_units'
AND indexdef LIKE '%hnsw%'
AND indexdef LIKE '%embedding%'
LOOP
EXECUTE 'DROP INDEX IF EXISTS {schema_name}.' || idx_name;
END LOOP;
END $$;
""")
)
# Alter the column type
conn.execute(
text(f"ALTER TABLE {schema_name}.memory_units ALTER COLUMN embedding TYPE vector({required_dimension})")
)
conn.commit()
# Recreate the HNSW index
conn.execute(
text(f"""
CREATE INDEX IF NOT EXISTS idx_memory_units_embedding_hnsw
ON {schema_name}.memory_units
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64)
""")
)
conn.commit()
logger.info(f"Successfully changed embedding dimension to {required_dimension}")
+1 -6
View File
@@ -18,9 +18,6 @@ class RequestContext:
"""
api_key: str | None = None
api_key_id: str | None = None # UUID of the API key used for authentication
tenant_id: str | None = None # Tenant identifier (set by extension after auth)
internal: bool = False # True for background/internal operations (not user-visible)
from pgvector.sqlalchemy import Vector
@@ -41,8 +38,6 @@ from sqlalchemy.dialects.postgresql import JSONB, TIMESTAMP, UUID
from sqlalchemy.ext.asyncio import AsyncAttrs
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
from .config import EMBEDDING_DIMENSION
class Base(AsyncAttrs, DeclarativeBase):
"""Base class for all models."""
@@ -83,7 +78,7 @@ class MemoryUnit(Base):
bank_id: Mapped[str] = mapped_column(Text, nullable=False)
document_id: Mapped[str | None] = mapped_column(Text)
text: Mapped[str] = mapped_column(Text, nullable=False)
embedding = mapped_column(Vector(EMBEDDING_DIMENSION)) # pgvector type
embedding = mapped_column(Vector(384)) # pgvector type
context: Mapped[str | None] = mapped_column(Text)
event_date: Mapped[datetime] = mapped_column(
TIMESTAMP(timezone=True), nullable=False
+1 -2
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "hindsight-api"
version = "0.2.1"
version = "0.1.14"
description = "Hindsight: Agent Memory That Works Like Human Memory"
readme = "README.md"
requires-python = ">=3.11"
@@ -37,7 +37,6 @@ dependencies = [
"opentelemetry-exporter-prometheus>=0.41b0",
"dateparser>=1.2.2",
"google-genai>=1.0.0",
"anthropic>=0.40.0",
]
[project.optional-dependencies]
@@ -1,428 +0,0 @@
"""
Tests for custom embedding dimensions and automatic dimension detection.
Uses isolated PostgreSQL schemas to avoid affecting other tests.
Includes tests for:
- Automatic embedding dimension detection and database schema adjustment
- OpenAI embeddings provider with 1536 dimensions
"""
import asyncio
import os
import pytest
from datetime import datetime
from sqlalchemy import create_engine, text
from hindsight_api import MemoryEngine, RequestContext
from hindsight_api.engine.embeddings import LocalSTEmbeddings, OpenAIEmbeddings
from hindsight_api.engine.cross_encoder import LocalSTCrossEncoder
from hindsight_api.engine.query_analyzer import DateparserQueryAnalyzer
from hindsight_api.extensions import TenantExtension, TenantContext
from hindsight_api.migrations import run_migrations, ensure_embedding_dimension
# =============================================================================
# Shared Utilities
# =============================================================================
class SchemaTenantExtension(TenantExtension):
"""Tenant extension that routes all requests to a specific schema (for testing)."""
def __init__(self, schema_name: str):
self.schema_name = schema_name
async def authenticate(self, request_context: RequestContext) -> TenantContext:
return TenantContext(schema_name=self.schema_name)
def get_test_schema(prefix: str, worker_id: str) -> str:
"""Get unique schema name per xdist worker."""
if worker_id == "master" or not worker_id:
return prefix
return f"{prefix}_{worker_id}"
def create_isolated_schema(db_url: str, schema_name: str, dimension: int | None = None):
"""Create an isolated schema with migrations and optional dimension adjustment."""
engine = create_engine(db_url)
# Create schema (drop first if exists from previous failed run)
with engine.connect() as conn:
conn.execute(text(f"DROP SCHEMA IF EXISTS {schema_name} CASCADE"))
conn.execute(text(f"CREATE SCHEMA {schema_name}"))
conn.commit()
# Run migrations in the isolated schema
run_migrations(db_url, schema=schema_name)
# Adjust embedding dimension if specified
if dimension is not None:
ensure_embedding_dimension(db_url, dimension, schema=schema_name)
def drop_schema(db_url: str, schema_name: str):
"""Drop an isolated schema."""
engine = create_engine(db_url)
with engine.connect() as conn:
conn.execute(text(f"DROP SCHEMA IF EXISTS {schema_name} CASCADE"))
conn.commit()
def get_column_dimension(db_url: str, schema: str = "public") -> int | None:
"""Get the current embedding column dimension from the database."""
engine = create_engine(db_url)
with engine.connect() as conn:
result = conn.execute(
text("""
SELECT atttypmod
FROM pg_attribute a
JOIN pg_class c ON a.attrelid = c.oid
JOIN pg_namespace n ON c.relnamespace = n.oid
WHERE n.nspname = :schema
AND c.relname = 'memory_units'
AND a.attname = 'embedding'
"""),
{"schema": schema},
).scalar()
return result
def get_row_count(db_url: str, schema: str = "public") -> int:
"""Get the number of rows with embeddings in memory_units."""
engine = create_engine(db_url)
with engine.connect() as conn:
return conn.execute(
text(f"SELECT COUNT(*) FROM {schema}.memory_units WHERE embedding IS NOT NULL")
).scalar()
def insert_test_embedding(db_url: str, schema: str, dimension: int):
"""Insert a test row with a dummy embedding."""
engine = create_engine(db_url)
embedding = [0.1] * dimension
embedding_str = "[" + ",".join(str(x) for x in embedding) + "]"
with engine.connect() as conn:
conn.execute(
text(f"""
INSERT INTO {schema}.memory_units (bank_id, text, embedding, event_date, fact_type)
VALUES ('test-bank', 'test text', '{embedding_str}'::vector, NOW(), 'world')
""")
)
conn.commit()
def clear_embeddings(db_url: str, schema: str):
"""Clear all rows from memory_units."""
engine = create_engine(db_url)
with engine.connect() as conn:
conn.execute(text(f"DELETE FROM {schema}.memory_units"))
conn.commit()
# =============================================================================
# Embedding Dimension Tests (Local Embeddings)
# =============================================================================
@pytest.fixture(scope="class")
def dimension_test_schema(pg0_db_url, worker_id):
"""Create an isolated schema for dimension tests."""
schema_name = get_test_schema("test_embed_dim", worker_id)
create_isolated_schema(pg0_db_url, schema_name)
yield pg0_db_url, schema_name
drop_schema(pg0_db_url, schema_name)
class TestEmbeddingDimension:
"""Tests for embedding dimension detection and adjustment."""
def test_dimension_matches_no_change(self, dimension_test_schema):
"""When dimension matches, no changes should be made."""
db_url, schema = dimension_test_schema
# Get initial dimension (should be 384 from migration)
initial_dim = get_column_dimension(db_url, schema)
assert initial_dim == 384, f"Expected 384, got {initial_dim}"
# Call ensure_embedding_dimension with matching dimension
ensure_embedding_dimension(db_url, 384, schema=schema)
# Dimension should still be 384
assert get_column_dimension(db_url, schema) == 384
def test_dimension_change_empty_table(self, dimension_test_schema):
"""When table is empty, dimension can be changed."""
db_url, schema = dimension_test_schema
# Ensure table is empty
clear_embeddings(db_url, schema)
assert get_row_count(db_url, schema) == 0
# Change dimension to 768
ensure_embedding_dimension(db_url, 768, schema=schema)
# Verify dimension changed
new_dim = get_column_dimension(db_url, schema)
assert new_dim == 768, f"Expected 768, got {new_dim}"
# Change back to 384 for other tests
ensure_embedding_dimension(db_url, 384, schema=schema)
assert get_column_dimension(db_url, schema) == 384
def test_dimension_change_blocked_with_data(self, dimension_test_schema):
"""When table has data, dimension change should be blocked."""
db_url, schema = dimension_test_schema
# Ensure table is empty first
clear_embeddings(db_url, schema)
# Insert a test row with 384-dim embedding
insert_test_embedding(db_url, schema, 384)
assert get_row_count(db_url, schema) == 1
# Try to change dimension - should raise error
with pytest.raises(RuntimeError) as exc_info:
ensure_embedding_dimension(db_url, 768, schema=schema)
assert "Cannot change embedding dimension" in str(exc_info.value)
assert "1 rows with embeddings" in str(exc_info.value)
# Dimension should be unchanged
assert get_column_dimension(db_url, schema) == 384
# Cleanup
clear_embeddings(db_url, schema)
def test_local_embeddings_dimension_detection(self, embeddings):
"""Test that LocalSTEmbeddings correctly detects dimension."""
# Initialize embeddings if not already done
loop = asyncio.new_event_loop()
try:
loop.run_until_complete(embeddings.initialize())
finally:
loop.close()
# bge-small-en-v1.5 produces 384-dim embeddings
assert embeddings.dimension == 384
# Verify by generating an actual embedding
result = embeddings.encode(["test"])
assert len(result) == 1
assert len(result[0]) == 384
# =============================================================================
# OpenAI Embeddings Tests
# =============================================================================
def has_openai_api_key() -> bool:
"""Check if OpenAI API key is available."""
return bool(os.environ.get("HINDSIGHT_API_EMBEDDINGS_OPENAI_API_KEY"))
def get_openai_api_key() -> str:
"""Get OpenAI API key from environment."""
return os.environ.get("HINDSIGHT_API_EMBEDDINGS_OPENAI_API_KEY", "")
@pytest.fixture(scope="module")
def openai_embeddings():
"""Create OpenAI embeddings instance."""
if not has_openai_api_key():
pytest.skip("OpenAI API key not available (set HINDSIGHT_API_EMBEDDINGS_OPENAI_API_KEY)")
embeddings = OpenAIEmbeddings(
api_key=get_openai_api_key(),
model="text-embedding-3-small",
)
loop = asyncio.new_event_loop()
try:
loop.run_until_complete(embeddings.initialize())
finally:
loop.close()
return embeddings
@pytest.fixture(scope="module")
def openai_test_schema(pg0_db_url, worker_id, openai_embeddings):
"""Create an isolated schema for OpenAI embedding tests."""
schema_name = get_test_schema("test_openai_embed", worker_id)
create_isolated_schema(pg0_db_url, schema_name, dimension=openai_embeddings.dimension)
yield pg0_db_url, schema_name
drop_schema(pg0_db_url, schema_name)
@pytest.fixture
def cross_encoder():
"""Provide a cross encoder for tests."""
return LocalSTCrossEncoder()
@pytest.fixture
def query_analyzer():
"""Provide a query analyzer for tests."""
return DateparserQueryAnalyzer()
@pytest.fixture
def test_bank_id():
"""Provide a unique bank ID for this test run."""
return f"openai_test_{datetime.now().timestamp()}"
@pytest.fixture
def request_context():
"""Provide a default RequestContext for tests."""
return RequestContext()
class TestOpenAIEmbeddings:
"""Tests for OpenAI embeddings provider."""
def test_openai_embeddings_initialization(self, openai_embeddings):
"""Test that OpenAI embeddings initializes correctly."""
assert openai_embeddings.dimension == 1536
assert openai_embeddings.provider_name == "openai"
def test_openai_embeddings_encode(self, openai_embeddings):
"""Test that OpenAI embeddings can encode text."""
texts = ["Hello, world!", "This is a test."]
embeddings = openai_embeddings.encode(texts)
assert len(embeddings) == 2
assert len(embeddings[0]) == 1536
assert len(embeddings[1]) == 1536
assert all(isinstance(x, float) for x in embeddings[0])
@pytest.mark.asyncio
async def test_openai_embeddings_retain_recall(
self,
openai_test_schema,
openai_embeddings,
cross_encoder,
query_analyzer,
test_bank_id,
request_context,
):
"""Test retain and recall operations with OpenAI embeddings."""
db_url, schema_name = openai_test_schema
memory = MemoryEngine(
db_url=db_url,
memory_llm_provider=os.getenv("HINDSIGHT_API_LLM_PROVIDER", "groq"),
memory_llm_api_key=os.getenv("HINDSIGHT_API_LLM_API_KEY"),
memory_llm_model=os.getenv("HINDSIGHT_API_LLM_MODEL", "openai/gpt-oss-120b"),
memory_llm_base_url=os.getenv("HINDSIGHT_API_LLM_BASE_URL") or None,
embeddings=openai_embeddings,
cross_encoder=cross_encoder,
query_analyzer=query_analyzer,
pool_min_size=1,
pool_max_size=3,
run_migrations=False,
tenant_extension=SchemaTenantExtension(schema_name),
)
try:
await memory.initialize()
# Store some memories
await memory.retain_async(
bank_id=test_bank_id,
content="Alice works as a software engineer at Google.",
context="career discussion",
request_context=request_context,
)
await memory.retain_async(
bank_id=test_bank_id,
content="Bob is a data scientist specializing in machine learning.",
context="team introductions",
request_context=request_context,
)
# Recall memories
result = await memory.recall_async(
bank_id=test_bank_id,
query="Who works in technology?",
request_context=request_context,
)
assert result is not None
assert len(result.results) > 0
memory_texts = [m.text for m in result.results]
assert any(
"Alice" in text or "Bob" in text or "software" in text or "data scientist" in text
for text in memory_texts
), f"Expected to find relevant memories, got: {memory_texts}"
finally:
try:
if memory._pool and not memory._pool._closing:
await memory.close()
except Exception:
pass
@pytest.mark.asyncio
async def test_openai_embeddings_batch_retain(
self,
openai_test_schema,
openai_embeddings,
cross_encoder,
query_analyzer,
test_bank_id,
request_context,
):
"""Test batch retain with OpenAI embeddings."""
db_url, schema_name = openai_test_schema
memory = MemoryEngine(
db_url=db_url,
memory_llm_provider=os.getenv("HINDSIGHT_API_LLM_PROVIDER", "groq"),
memory_llm_api_key=os.getenv("HINDSIGHT_API_LLM_API_KEY"),
memory_llm_model=os.getenv("HINDSIGHT_API_LLM_MODEL", "openai/gpt-oss-120b"),
memory_llm_base_url=os.getenv("HINDSIGHT_API_LLM_BASE_URL") or None,
embeddings=openai_embeddings,
cross_encoder=cross_encoder,
query_analyzer=query_analyzer,
pool_min_size=1,
pool_max_size=3,
run_migrations=False,
tenant_extension=SchemaTenantExtension(schema_name),
)
try:
await memory.initialize()
contents = [
{"content": "Python is my favorite programming language.", "context": "preferences"},
{"content": "I prefer dark mode for all my applications.", "context": "preferences"},
{"content": "Coffee is essential for morning productivity.", "context": "habits"},
]
result = await memory.retain_batch_async(
bank_id=test_bank_id,
contents=contents,
request_context=request_context,
)
assert len(result) == 3
recall_result = await memory.recall_async(
bank_id=test_bank_id,
query="What are my preferences?",
request_context=request_context,
)
assert recall_result is not None
assert len(recall_result.results) > 0
finally:
try:
if memory._pool and not memory._pool._closing:
await memory.close()
except Exception:
pass
@@ -402,8 +402,6 @@ I'm planning to visit Tokyo next month.
Ideally: If conversation is on August 14, 2023 and text says "last night",
the date field should be August 13. We accept 13 or 14 as LLM may vary.
Retries up to 3 times to account for LLM inconsistencies.
"""
text = """
Melanie: Hey Caroline! Last night was amazing! We celebrated my daughter's birthday
@@ -412,69 +410,41 @@ with a concert surrounded by music, joy and the warm summer breeze.
context = "Conversation between Melanie and Caroline"
llm_config = LLMConfig.for_memory()
event_date = datetime(2023, 8, 14, 14, 24)
last_error = None
max_retries = 3
facts, _ = await extract_facts_from_text(
text=text,
event_date=event_date,
context=context,
llm_config=llm_config,
agent_name="Melanie"
)
for attempt in range(max_retries):
try:
facts, _ = await extract_facts_from_text(
text=text,
event_date=event_date,
context=context,
llm_config=llm_config,
agent_name="Melanie"
)
assert len(facts) > 0, "Should extract at least one fact"
assert len(facts) > 0, "Should extract at least one fact"
birthday_fact = None
for fact in facts:
if "birthday" in fact.fact.lower() or "concert" in fact.fact.lower():
birthday_fact = fact
break
birthday_fact = None
for fact in facts:
if "birthday" in fact.fact.lower() or "concert" in fact.fact.lower():
birthday_fact = fact
break
assert birthday_fact is not None, "Should extract fact about birthday celebration"
assert birthday_fact is not None, "Should extract fact about birthday celebration"
fact_date_str = birthday_fact.occurred_start
assert fact_date_str is not None, "occurred_start should not be None for temporal events"
fact_date_str = birthday_fact.occurred_start
assert fact_date_str is not None, "occurred_start should not be None for temporal events"
if 'T' in fact_date_str:
fact_date = datetime.fromisoformat(fact_date_str.replace('Z', '+00:00'))
else:
fact_date = datetime.fromisoformat(fact_date_str)
if 'T' in fact_date_str:
fact_date = datetime.fromisoformat(fact_date_str.replace('Z', '+00:00'))
else:
fact_date = datetime.fromisoformat(fact_date_str)
assert fact_date.year == 2023, "Year should be 2023"
assert fact_date.month == 8, "Month should be August"
# Accept day 13 (ideal: last night) or 14 (conversation date) as valid
assert fact_date.day in (13, 14), (
f"Day should be 13 or 14 (around Aug 14 event), but got {fact_date.day}."
)
# If we reach here, test passed
return
except AssertionError as e:
last_error = e
if attempt < max_retries - 1:
print(f"Test attempt {attempt + 1} failed: {e}. Retrying...")
continue
else:
# Last attempt failed, re-raise the error
raise e
except Exception as e:
last_error = e
if attempt < max_retries - 1:
print(f"Test attempt {attempt + 1} failed with exception: {e}. Retrying...")
continue
else:
# Last attempt failed, re-raise the error
raise e
# Should not reach here, but just in case
if last_error:
raise last_error
assert fact_date.year == 2023, "Year should be 2023"
assert fact_date.month == 8, "Month should be August"
# Accept day 13 (ideal: last night) or 14 (conversation date) as valid
assert fact_date.day in (13, 14), (
f"Day should be 13 or 14 (around Aug 14 event), but got {fact_date.day}."
)
@pytest.mark.asyncio
async def test_date_field_calculation_yesterday(self):
@@ -428,66 +428,6 @@ async def test_document_deletion(api_client):
assert response.status_code == 404
@pytest.mark.asyncio
async def test_document_deletion_with_slashes_in_id(api_client):
"""
Test document deletion when document_id contains forward slashes.
Regression test for https://github.com/vectorize-io/hindsight/issues/92
Document IDs with slashes (e.g., "folder/file.md") should work correctly
for all operations including creation, listing, retrieval, and deletion.
"""
import urllib.parse
test_bank_id = f"doc_slash_test_{datetime.now().timestamp()}"
document_id_with_slash = "reports/quarterly/q1-2024.md"
try:
# 1. Create a document with slashes in its ID
response = await api_client.post(
f"/v1/default/banks/{test_bank_id}/memories",
json={
"items": [
{
"content": "The Q1 2024 report shows significant growth in user engagement.",
"context": "quarterly report",
"document_id": document_id_with_slash
}
]
}
)
assert response.status_code == 200, f"Failed to create document: {response.text}"
# 2. Verify document exists via list endpoint
response = await api_client.get(f"/v1/default/banks/{test_bank_id}/documents")
assert response.status_code == 200
documents = response.json()
doc_ids = [doc["id"] for doc in documents["items"]]
assert document_id_with_slash in doc_ids, f"Document should be in list: {doc_ids}"
# 3. Delete the document (slashes in document_id should work with :path converter)
encoded_doc_id = urllib.parse.quote(document_id_with_slash, safe="")
response = await api_client.delete(
f"/v1/default/banks/{test_bank_id}/documents/{encoded_doc_id}"
)
assert response.status_code == 200, (
f"Failed to delete document with slashes in ID. "
f"Status: {response.status_code}, Response: {response.text}"
)
# Verify document is deleted
response = await api_client.get(f"/v1/default/banks/{test_bank_id}/documents")
assert response.status_code == 200
documents = response.json()
doc_ids = [doc["id"] for doc in documents["items"]]
assert document_id_with_slash not in doc_ids, "Document should be deleted"
finally:
# Cleanup - delete the bank
await api_client.delete(f"/v1/default/banks/{test_bank_id}")
@pytest.mark.asyncio
async def test_async_retain(api_client):
"""Test asynchronous retain functionality.
@@ -668,167 +608,3 @@ async def test_async_retain_parallel(api_client):
assert response.status_code == 200
results = response.json()["results"]
assert len(results) > 0, f"Should find memories for document {i}"
@pytest.mark.asyncio
async def test_reflect_structured_output(api_client):
"""Test reflect endpoint with structured output via response_schema.
When response_schema is provided, the reflect endpoint should return
both the natural language text response and a structured_output field
containing the response parsed according to the provided JSON schema.
"""
test_bank_id = f"reflect_structured_test_{datetime.now().timestamp()}"
# Store some memories to reflect on
response = await api_client.post(
f"/v1/default/banks/{test_bank_id}/memories",
json={
"items": [
{
"content": "Alice is a senior machine learning engineer with 8 years of experience.",
"context": "team member info"
},
{
"content": "Bob is a junior data scientist who joined last month.",
"context": "team member info"
},
{
"content": "The team uses Python and TensorFlow for most projects.",
"context": "tech stack"
}
]
}
)
assert response.status_code == 200
# Define a JSON schema for structured output
response_schema = {
"type": "object",
"properties": {
"team_members": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {"type": "string"},
"role": {"type": "string"},
"experience_level": {"type": "string"}
}
}
},
"technologies": {
"type": "array",
"items": {"type": "string"}
},
"summary": {"type": "string"}
},
"required": ["team_members", "summary"]
}
# Call reflect with response_schema
response = await api_client.post(
f"/v1/default/banks/{test_bank_id}/reflect",
json={
"query": "Give me an overview of the team and their tech stack",
"response_schema": response_schema
}
)
assert response.status_code == 200
result = response.json()
# Verify text field exists (empty when using structured output)
assert "text" in result
assert result["text"] == ""
# Verify structured output exists and has expected structure
assert "structured_output" in result
assert result["structured_output"] is not None
structured = result["structured_output"]
assert "team_members" in structured
assert "summary" in structured
assert isinstance(structured["team_members"], list)
assert isinstance(structured["summary"], str)
# Verify team members have the expected fields
if len(structured["team_members"]) > 0:
member = structured["team_members"][0]
assert "name" in member or "role" in member # At least some fields should be present
@pytest.mark.asyncio
async def test_reflect_without_structured_output(api_client):
"""Test that reflect works normally without response_schema.
When response_schema is not provided, the structured_output field
should be null/None in the response.
"""
test_bank_id = f"reflect_no_structured_test_{datetime.now().timestamp()}"
# Store a memory
response = await api_client.post(
f"/v1/default/banks/{test_bank_id}/memories",
json={
"items": [
{
"content": "The project deadline is next Friday.",
"context": "project timeline"
}
]
}
)
assert response.status_code == 200
# Call reflect without response_schema
response = await api_client.post(
f"/v1/default/banks/{test_bank_id}/reflect",
json={
"query": "When is the project deadline?"
}
)
assert response.status_code == 200
result = response.json()
# Verify response has text but structured_output is null
assert "text" in result
assert len(result["text"]) > 0
assert result.get("structured_output") is None
@pytest.mark.asyncio
async def test_reflect_with_max_tokens(api_client):
"""Test reflect endpoint with custom max_tokens parameter.
The max_tokens parameter controls the maximum tokens for the LLM response.
"""
test_bank_id = f"reflect_max_tokens_test_{datetime.now().timestamp()}"
# Store a memory
response = await api_client.post(
f"/v1/default/banks/{test_bank_id}/memories",
json={
"items": [
{
"content": "Python is a popular programming language for data science and machine learning.",
"context": "tech"
}
]
}
)
assert response.status_code == 200
# Call reflect with custom max_tokens
response = await api_client.post(
f"/v1/default/banks/{test_bank_id}/reflect",
json={
"query": "What is Python used for?",
"max_tokens": 500
}
)
assert response.status_code == 200
result = response.json()
# Verify response has text
assert "text" in result
assert len(result["text"]) > 0
@@ -0,0 +1,177 @@
"""
Integration test for the MCP (Model Context Protocol) server.
Tests MCP endpoints by starting a FastAPI server with MCP enabled and using the MCP client.
Note: MCP server is integrated with the web server. These tests require HINDSIGHT_API_MCP_ENABLED=true.
"""
import asyncio
import pytest
import pytest_asyncio
import httpx
from mcp import ClientSession
from mcp.client.sse import sse_client
from hindsight_api.api import create_app
@pytest_asyncio.fixture
async def mcp_server(memory):
"""Start the FastAPI app with MCP enabled and return the SSE URL."""
# Memory is already initialized by the conftest fixture (with migrations)
app = create_app(
memory,
initialize_memory=False,
mcp_api_enabled=True
)
# Use httpx to create a test server
transport = httpx.ASGITransport(app=app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
# The MCP SSE endpoint is at /mcp/sse
# We need to yield the base URL for sse_client to connect
# However, sse_client expects a real URL, not a test client
# So we'll start a real server on a random port
pass
# For now, skip these tests as they require a real server
# The sse_client doesn't work with ASGI test transport
pytest.skip("MCP tests require a real running server. Run: HINDSIGHT_API_MCP_ENABLED=true uvicorn hindsight_api.api:app")
@pytest.mark.asyncio
async def test_mcp_server_tools_via_sse(mcp_server):
"""Test MCP server tools via SSE transport using proper MCP client."""
sse_url = mcp_server
async with sse_client(sse_url) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
# Test 1: List tools
tools_list = await session.list_tools()
print(f"Tools: {tools_list}")
tool_names = [t.name for t in tools_list.tools]
assert "hindsight_search" in tool_names
assert "hindsight_put" in tool_names
# Test 2: Call hindsight_put
put_result = await session.call_tool(
"hindsight_put",
arguments={
"content": "User loves Python programming",
"context": "programming_preferences",
"explanation": "Storing user's programming language preference"
}
)
print(f"Put result: {put_result}")
assert put_result is not None
# Wait a bit for indexing
await asyncio.sleep(1)
# Test 3: Call hindsight_search
search_result = await session.call_tool(
"hindsight_search",
arguments={
"query": "What programming languages does the user like?",
"max_tokens": 4096,
"explanation": "Searching for programming preferences"
}
)
print(f"Search result: {search_result}")
assert search_result is not None
@pytest.mark.asyncio
async def test_multiple_concurrent_requests(mcp_server):
"""Test multiple concurrent requests from a single session."""
sse_url = mcp_server
async with sse_client(sse_url) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
# Fire off 10 concurrent search requests from same session
async def make_search(idx):
try:
result = await session.call_tool(
"hindsight_search",
arguments={
"query": f"test query {idx}",
"explanation": f"Concurrent test {idx}"
}
)
return idx, "success", result
except Exception as e:
return idx, "error", str(e)
tasks = [make_search(i) for i in range(10)]
results = await asyncio.gather(*tasks, return_exceptions=True)
# Check results
successes = 0
failures = 0
for result in results:
if isinstance(result, Exception):
print(f"Request failed with exception: {result}")
failures += 1
else:
idx, status, data = result
if status == "success":
successes += 1
else:
print(f"Request {idx} failed: {data}")
failures += 1
print(f"Successes: {successes}, Failures: {failures}")
# We expect all requests to succeed
assert successes >= 8, f"Too many failures: {failures}/10"
@pytest.mark.asyncio
async def test_race_condition_with_rapid_requests(mcp_server):
"""Test rapid-fire requests with multiple sessions to trigger race condition."""
sse_url = mcp_server
async def rapid_session_search(idx):
"""Create a new session and immediately make a request."""
try:
async with sse_client(sse_url) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
# Make request immediately after initialization
result = await session.call_tool(
"hindsight_search",
arguments={
"query": f"rapid query {idx}",
"max_tokens": 2048
}
)
return idx, "success", result
except Exception as e:
return idx, "error", str(e)
# Fire 20 requests with minimal delay, each with its own session
tasks = [rapid_session_search(i) for i in range(20)]
results = await asyncio.gather(*tasks)
# Analyze results
errors = []
for idx, status, data in results:
if status == "error":
errors.append((idx, data))
if errors:
print(f"Found {len(errors)} errors:")
for idx, error_msg in errors:
print(f" Request {idx}: {error_msg}")
# Most requests should succeed
assert len(errors) < 5, f"Too many errors: {len(errors)}/20"
if __name__ == "__main__":
pytest.main([__file__, "-v", "-s"])
-68
View File
@@ -1778,71 +1778,3 @@ async def test_temporal_links_within_same_batch(memory, request_context):
finally:
await memory.delete_bank(bank_id, request_context=request_context)
@pytest.mark.asyncio
async def test_user_provided_entities(memory, request_context):
"""
Test that user-provided entities are merged with auto-extracted entities.
This tests the feature added in PR #91 where users can provide entities
via the 'entities' field in the retain request. These should be combined
with LLM-extracted entities, with case-insensitive deduplication.
"""
bank_id = f"test_user_entities_{datetime.now(timezone.utc).timestamp()}"
try:
# Store content with user-provided entities
# The content mentions "Alice" which LLM might extract,
# but we also provide "ProjectX" and "ACME Corp" which may not be in the text
contents = [
{
"content": "Alice completed the quarterly report.",
"context": "work update",
"entities": [
{"text": "ProjectX", "type": "PROJECT"},
{"text": "ACME Corp", "type": "ORG"},
{"text": "Alice"}, # May also be extracted by LLM (dedup test)
],
}
]
result = await memory.retain_batch_async(
bank_id=bank_id,
contents=contents,
request_context=request_context,
)
# Flatten the list of lists
unit_ids = [uid for sublist in result for uid in sublist]
assert len(unit_ids) > 0, "Should have created at least one fact"
logger.info(f"Created {len(unit_ids)} facts with user-provided entities")
# Query entity links to verify user-provided entities were stored
async with memory._pool.acquire() as conn:
# Get all entities linked to our facts via the unit_entities junction table
entity_rows = await conn.fetch(
"""
SELECT DISTINCT e.canonical_name
FROM entities e
JOIN unit_entities ue ON e.id = ue.entity_id
WHERE ue.unit_id::text = ANY($1)
""",
unit_ids
)
entity_names = {row['canonical_name'].lower() for row in entity_rows}
logger.info(f"Found entities linked to facts: {[row['canonical_name'] for row in entity_rows]}")
# Verify user-provided entities are present
assert "projectx" in entity_names, "User-provided entity 'ProjectX' should be linked"
assert "acme corp" in entity_names, "User-provided entity 'ACME Corp' should be linked"
# Alice should be present (either from LLM extraction or user-provided)
assert "alice" in entity_names, "Entity 'Alice' should be linked"
logger.info("✓ User-provided entities successfully merged with extracted entities")
finally:
await memory.delete_bank(bank_id, request_context=request_context)
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "hindsight-cli"
version = "0.2.1"
version = "0.1.14"
edition = "2021"
authors = ["Hindsight Team"]
description = "A beautiful CLI for Hindsight - semantic memory system"
-2
View File
@@ -354,9 +354,7 @@ impl App {
query: query_text,
budget: Some(query_budget),
context: None,
max_tokens: 4096,
include: None,
response_schema: None,
};
let result = client.reflect(&bank_id, &request, false)
-18
View File
@@ -10,7 +10,6 @@ use crate::ui;
// Import types from generated client
use hindsight_client::types::{Budget, ChunkIncludeOptions, IncludeOptions};
use serde_json;
// Helper function to parse budget string to Budget enum
fn parse_budget(budget: &str) -> Budget {
@@ -87,8 +86,6 @@ pub fn reflect(
query: String,
budget: String,
context: Option<String>,
max_tokens: Option<i64>,
schema_path: Option<PathBuf>,
verbose: bool,
output_format: OutputFormat,
) -> Result<()> {
@@ -98,24 +95,11 @@ pub fn reflect(
None
};
// Load and parse schema if provided
let response_schema = if let Some(path) = schema_path {
let schema_content = fs::read_to_string(&path)
.with_context(|| format!("Failed to read schema file: {}", path.display()))?;
let schema: serde_json::Map<String, serde_json::Value> = serde_json::from_str(&schema_content)
.with_context(|| format!("Failed to parse JSON schema from: {}", path.display()))?;
Some(schema)
} else {
None
};
let request = ReflectRequest {
query,
budget: Some(parse_budget(&budget)),
context,
max_tokens: max_tokens.unwrap_or(4096),
include: None,
response_schema,
};
let response = client.reflect(agent_id, &request, verbose);
@@ -161,7 +145,6 @@ pub fn retain(
metadata: None,
timestamp: None,
document_id: Some(doc_id.clone()),
entities: None,
};
let request = RetainRequest {
@@ -271,7 +254,6 @@ pub fn retain_files(
metadata: None,
timestamp: None,
document_id: Some(doc_id),
entities: None,
});
pb.inc(1);
+2 -10
View File
@@ -206,14 +206,6 @@ enum MemoryCommands {
/// Additional context
#[arg(short = 'c', long)]
context: Option<String>,
/// Maximum tokens for the response (server default: 4096)
#[arg(short = 'm', long)]
max_tokens: Option<i64>,
/// Path to JSON schema file for structured output
#[arg(short = 's', long)]
schema: Option<PathBuf>,
},
/// Store (retain) a single memory
@@ -429,8 +421,8 @@ fn run() -> Result<()> {
MemoryCommands::Recall { bank_id, query, fact_type, budget, max_tokens, trace, include_chunks, chunk_max_tokens } => {
commands::memory::recall(&client, &bank_id, query, fact_type, budget, max_tokens, trace, include_chunks, chunk_max_tokens, verbose, output_format)
}
MemoryCommands::Reflect { bank_id, query, budget, context, max_tokens, schema } => {
commands::memory::reflect(&client, &bank_id, query, budget, context, max_tokens, schema, verbose, output_format)
MemoryCommands::Reflect { bank_id, query, budget, context } => {
commands::memory::reflect(&client, &bank_id, query, budget, context, verbose, output_format)
}
MemoryCommands::Retain { bank_id, content, doc_id, context, r#async } => {
commands::memory::retain(&client, &bank_id, content, doc_id, context, r#async, verbose, output_format)
-10
View File
@@ -175,16 +175,6 @@ pub fn print_think_response(response: &ReflectResponse) {
if !response.based_on.is_empty() {
println!("{}", dim(&format!("Based on {} memory units", response.based_on.len())));
}
// Display structured output if present
if let Some(structured) = &response.structured_output {
println!();
println!("{}", gradient_text("─── Structured Output ───"));
println!();
if let Ok(json) = serde_json::to_string_pretty(structured) {
println!("{}", json);
}
}
}
pub fn print_trace_info(trace: &serde_json::Map<String, serde_json::Value>) {
@@ -9,6 +9,54 @@ hindsight_client_api/api/operations_api.py
hindsight_client_api/api_client.py
hindsight_client_api/api_response.py
hindsight_client_api/configuration.py
hindsight_client_api/docs/AddBackgroundRequest.md
hindsight_client_api/docs/BackgroundResponse.md
hindsight_client_api/docs/BankListItem.md
hindsight_client_api/docs/BankListResponse.md
hindsight_client_api/docs/BankProfileResponse.md
hindsight_client_api/docs/BankStatsResponse.md
hindsight_client_api/docs/BanksApi.md
hindsight_client_api/docs/Budget.md
hindsight_client_api/docs/CancelOperationResponse.md
hindsight_client_api/docs/ChunkData.md
hindsight_client_api/docs/ChunkIncludeOptions.md
hindsight_client_api/docs/ChunkResponse.md
hindsight_client_api/docs/CreateBankRequest.md
hindsight_client_api/docs/DeleteDocumentResponse.md
hindsight_client_api/docs/DeleteResponse.md
hindsight_client_api/docs/DispositionTraits.md
hindsight_client_api/docs/DocumentResponse.md
hindsight_client_api/docs/DocumentsApi.md
hindsight_client_api/docs/EntitiesApi.md
hindsight_client_api/docs/EntityDetailResponse.md
hindsight_client_api/docs/EntityIncludeOptions.md
hindsight_client_api/docs/EntityListItem.md
hindsight_client_api/docs/EntityListResponse.md
hindsight_client_api/docs/EntityObservationResponse.md
hindsight_client_api/docs/EntityStateResponse.md
hindsight_client_api/docs/GraphDataResponse.md
hindsight_client_api/docs/HTTPValidationError.md
hindsight_client_api/docs/IncludeOptions.md
hindsight_client_api/docs/ListDocumentsResponse.md
hindsight_client_api/docs/ListMemoryUnitsResponse.md
hindsight_client_api/docs/MemoryApi.md
hindsight_client_api/docs/MemoryItem.md
hindsight_client_api/docs/MonitoringApi.md
hindsight_client_api/docs/OperationResponse.md
hindsight_client_api/docs/OperationsApi.md
hindsight_client_api/docs/OperationsListResponse.md
hindsight_client_api/docs/RecallRequest.md
hindsight_client_api/docs/RecallResponse.md
hindsight_client_api/docs/RecallResult.md
hindsight_client_api/docs/ReflectFact.md
hindsight_client_api/docs/ReflectIncludeOptions.md
hindsight_client_api/docs/ReflectRequest.md
hindsight_client_api/docs/ReflectResponse.md
hindsight_client_api/docs/RetainRequest.md
hindsight_client_api/docs/RetainResponse.md
hindsight_client_api/docs/UpdateDispositionRequest.md
hindsight_client_api/docs/ValidationError.md
hindsight_client_api/docs/ValidationErrorLocInner.md
hindsight_client_api/exceptions.py
hindsight_client_api/models/__init__.py
hindsight_client_api/models/add_background_request.py
@@ -29,7 +77,6 @@ hindsight_client_api/models/disposition_traits.py
hindsight_client_api/models/document_response.py
hindsight_client_api/models/entity_detail_response.py
hindsight_client_api/models/entity_include_options.py
hindsight_client_api/models/entity_input.py
hindsight_client_api/models/entity_list_item.py
hindsight_client_api/models/entity_list_response.py
hindsight_client_api/models/entity_observation_response.py
@@ -55,4 +102,53 @@ hindsight_client_api/models/update_disposition_request.py
hindsight_client_api/models/validation_error.py
hindsight_client_api/models/validation_error_loc_inner.py
hindsight_client_api/rest.py
hindsight_client_api/test/__init__.py
hindsight_client_api/test/test_add_background_request.py
hindsight_client_api/test/test_background_response.py
hindsight_client_api/test/test_bank_list_item.py
hindsight_client_api/test/test_bank_list_response.py
hindsight_client_api/test/test_bank_profile_response.py
hindsight_client_api/test/test_bank_stats_response.py
hindsight_client_api/test/test_banks_api.py
hindsight_client_api/test/test_budget.py
hindsight_client_api/test/test_cancel_operation_response.py
hindsight_client_api/test/test_chunk_data.py
hindsight_client_api/test/test_chunk_include_options.py
hindsight_client_api/test/test_chunk_response.py
hindsight_client_api/test/test_create_bank_request.py
hindsight_client_api/test/test_delete_document_response.py
hindsight_client_api/test/test_delete_response.py
hindsight_client_api/test/test_disposition_traits.py
hindsight_client_api/test/test_document_response.py
hindsight_client_api/test/test_documents_api.py
hindsight_client_api/test/test_entities_api.py
hindsight_client_api/test/test_entity_detail_response.py
hindsight_client_api/test/test_entity_include_options.py
hindsight_client_api/test/test_entity_list_item.py
hindsight_client_api/test/test_entity_list_response.py
hindsight_client_api/test/test_entity_observation_response.py
hindsight_client_api/test/test_entity_state_response.py
hindsight_client_api/test/test_graph_data_response.py
hindsight_client_api/test/test_http_validation_error.py
hindsight_client_api/test/test_include_options.py
hindsight_client_api/test/test_list_documents_response.py
hindsight_client_api/test/test_list_memory_units_response.py
hindsight_client_api/test/test_memory_api.py
hindsight_client_api/test/test_memory_item.py
hindsight_client_api/test/test_monitoring_api.py
hindsight_client_api/test/test_operation_response.py
hindsight_client_api/test/test_operations_api.py
hindsight_client_api/test/test_operations_list_response.py
hindsight_client_api/test/test_recall_request.py
hindsight_client_api/test/test_recall_response.py
hindsight_client_api/test/test_recall_result.py
hindsight_client_api/test/test_reflect_fact.py
hindsight_client_api/test/test_reflect_include_options.py
hindsight_client_api/test/test_reflect_request.py
hindsight_client_api/test/test_reflect_response.py
hindsight_client_api/test/test_retain_request.py
hindsight_client_api/test/test_retain_response.py
hindsight_client_api/test/test_update_disposition_request.py
hindsight_client_api/test/test_validation_error.py
hindsight_client_api/test/test_validation_error_loc_inner.py
hindsight_client_api_README.md
@@ -1 +1 @@
7.10.0
7.18.0-SNAPSHOT
@@ -74,8 +74,6 @@ class Hindsight:
"""
config = hindsight_client_api.Configuration(host=base_url, access_token=api_key)
self._api_client = hindsight_client_api.ApiClient(config)
if api_key:
self._api_client.set_default_header("Authorization", f"Bearer {api_key}")
self._memory_api = memory_api.MemoryApi(self._api_client)
self._banks_api = banks_api.BanksApi(self._api_client)
@@ -114,7 +112,6 @@ class Hindsight:
context: Optional[str] = None,
document_id: Optional[str] = None,
metadata: Optional[Dict[str, str]] = None,
entities: Optional[List[Dict[str, str]]] = None,
) -> RetainResponse:
"""
Store a single memory (simplified interface).
@@ -126,14 +123,13 @@ class Hindsight:
context: Optional context description
document_id: Optional document ID for grouping
metadata: Optional user-defined metadata
entities: Optional list of entities [{"text": "...", "type": "..."}]
Returns:
RetainResponse with success status
"""
return self.retain_batch(
bank_id=bank_id,
items=[{"content": content, "timestamp": timestamp, "context": context, "metadata": metadata, "entities": entities}],
items=[{"content": content, "timestamp": timestamp, "context": context, "metadata": metadata}],
document_id=document_id,
)
@@ -149,34 +145,24 @@ class Hindsight:
Args:
bank_id: The memory bank ID
items: List of memory items with 'content' and optional 'timestamp', 'context', 'metadata', 'document_id', 'entities'
items: List of memory items with 'content' and optional 'timestamp', 'context', 'metadata', 'document_id'
document_id: Optional document ID for grouping memories (applied to items that don't have their own)
retain_async: If True, process asynchronously in background (default: False)
Returns:
RetainResponse with success status and item count
"""
from hindsight_client_api.models.entity_input import EntityInput
memory_items = []
for item in items:
entities = None
if item.get("entities"):
entities = [
EntityInput(text=e["text"], type=e.get("type"))
for e in item["entities"]
]
memory_items.append(
memory_item.MemoryItem(
content=item["content"],
timestamp=item.get("timestamp"),
context=item.get("context"),
metadata=item.get("metadata"),
# Use item's document_id if provided, otherwise fall back to batch-level document_id
document_id=item.get("document_id") or document_id,
entities=entities,
)
memory_items = [
memory_item.MemoryItem(
content=item["content"],
timestamp=item.get("timestamp"),
context=item.get("context"),
metadata=item.get("metadata"),
# Use item's document_id if provided, otherwise fall back to batch-level document_id
document_id=item.get("document_id") or document_id,
)
for item in items
]
request_obj = retain_request.RetainRequest(
items=memory_items,
@@ -243,8 +229,6 @@ class Hindsight:
query: str,
budget: str = "low",
context: Optional[str] = None,
max_tokens: Optional[int] = None,
response_schema: Optional[Dict[str, Any]] = None,
) -> ReflectResponse:
"""
Generate a contextual answer based on bank identity and memories.
@@ -254,21 +238,14 @@ class Hindsight:
query: The question or prompt
budget: Budget level for reflection - "low", "mid", or "high" (default: "low")
context: Optional additional context
max_tokens: Maximum tokens for the response (server default: 4096)
response_schema: Optional JSON Schema for structured output. When provided,
the response will include a 'structured_output' field with the LLM
response parsed according to this schema.
Returns:
ReflectResponse with answer text, optionally facts used, and optionally
structured_output if response_schema was provided
ReflectResponse with answer text and optionally facts used
"""
request_obj = reflect_request.ReflectRequest(
query=query,
budget=budget,
context=context,
max_tokens=max_tokens,
response_schema=response_schema,
)
return _run_async(self._memory_api.reflect(bank_id, request_obj))
@@ -326,34 +303,24 @@ class Hindsight:
Args:
bank_id: The memory bank ID
items: List of memory items with 'content' and optional 'timestamp', 'context', 'metadata', 'document_id', 'entities'
items: List of memory items with 'content' and optional 'timestamp', 'context', 'metadata', 'document_id'
document_id: Optional document ID for grouping memories (applied to items that don't have their own)
retain_async: If True, process asynchronously in background (default: False)
Returns:
RetainResponse with success status and item count
"""
from hindsight_client_api.models.entity_input import EntityInput
memory_items = []
for item in items:
entities = None
if item.get("entities"):
entities = [
EntityInput(text=e["text"], type=e.get("type"))
for e in item["entities"]
]
memory_items.append(
memory_item.MemoryItem(
content=item["content"],
timestamp=item.get("timestamp"),
context=item.get("context"),
metadata=item.get("metadata"),
# Use item's document_id if provided, otherwise fall back to batch-level document_id
document_id=item.get("document_id") or document_id,
entities=entities,
)
memory_items = [
memory_item.MemoryItem(
content=item["content"],
timestamp=item.get("timestamp"),
context=item.get("context"),
metadata=item.get("metadata"),
# Use item's document_id if provided, otherwise fall back to batch-level document_id
document_id=item.get("document_id") or document_id,
)
for item in items
]
request_obj = retain_request.RetainRequest(
items=memory_items,
@@ -370,7 +337,6 @@ class Hindsight:
context: Optional[str] = None,
document_id: Optional[str] = None,
metadata: Optional[Dict[str, str]] = None,
entities: Optional[List[Dict[str, str]]] = None,
) -> RetainResponse:
"""
Store a single memory (async).
@@ -382,14 +348,13 @@ class Hindsight:
context: Optional context description
document_id: Optional document ID for grouping
metadata: Optional user-defined metadata
entities: Optional list of entities [{"text": "...", "type": "..."}]
Returns:
RetainResponse with success status
"""
return await self.aretain_batch(
bank_id=bank_id,
items=[{"content": content, "timestamp": timestamp, "context": context, "metadata": metadata, "entities": entities}],
items=[{"content": content, "timestamp": timestamp, "context": context, "metadata": metadata}],
document_id=document_id,
)
@@ -16,66 +16,127 @@
__version__ = "0.0.7"
# Define package exports
__all__ = [
"BanksApi",
"DocumentsApi",
"EntitiesApi",
"MemoryApi",
"MonitoringApi",
"OperationsApi",
"ApiResponse",
"ApiClient",
"Configuration",
"OpenApiException",
"ApiTypeError",
"ApiValueError",
"ApiKeyError",
"ApiAttributeError",
"ApiException",
"AddBackgroundRequest",
"BackgroundResponse",
"BankListItem",
"BankListResponse",
"BankProfileResponse",
"BankStatsResponse",
"Budget",
"CancelOperationResponse",
"ChunkData",
"ChunkIncludeOptions",
"ChunkResponse",
"CreateBankRequest",
"DeleteDocumentResponse",
"DeleteResponse",
"DispositionTraits",
"DocumentResponse",
"EntityDetailResponse",
"EntityIncludeOptions",
"EntityListItem",
"EntityListResponse",
"EntityObservationResponse",
"EntityStateResponse",
"GraphDataResponse",
"HTTPValidationError",
"IncludeOptions",
"ListDocumentsResponse",
"ListMemoryUnitsResponse",
"MemoryItem",
"OperationResponse",
"OperationsListResponse",
"RecallRequest",
"RecallResponse",
"RecallResult",
"ReflectFact",
"ReflectIncludeOptions",
"ReflectRequest",
"ReflectResponse",
"RetainRequest",
"RetainResponse",
"UpdateDispositionRequest",
"ValidationError",
"ValidationErrorLocInner",
]
# import apis into sdk package
from hindsight_client_api.api.banks_api import BanksApi
from hindsight_client_api.api.documents_api import DocumentsApi
from hindsight_client_api.api.entities_api import EntitiesApi
from hindsight_client_api.api.memory_api import MemoryApi
from hindsight_client_api.api.monitoring_api import MonitoringApi
from hindsight_client_api.api.operations_api import OperationsApi
from hindsight_client_api.api.banks_api import BanksApi as BanksApi
from hindsight_client_api.api.documents_api import DocumentsApi as DocumentsApi
from hindsight_client_api.api.entities_api import EntitiesApi as EntitiesApi
from hindsight_client_api.api.memory_api import MemoryApi as MemoryApi
from hindsight_client_api.api.monitoring_api import MonitoringApi as MonitoringApi
from hindsight_client_api.api.operations_api import OperationsApi as OperationsApi
# import ApiClient
from hindsight_client_api.api_response import ApiResponse
from hindsight_client_api.api_client import ApiClient
from hindsight_client_api.configuration import Configuration
from hindsight_client_api.exceptions import OpenApiException
from hindsight_client_api.exceptions import ApiTypeError
from hindsight_client_api.exceptions import ApiValueError
from hindsight_client_api.exceptions import ApiKeyError
from hindsight_client_api.exceptions import ApiAttributeError
from hindsight_client_api.exceptions import ApiException
from hindsight_client_api.api_response import ApiResponse as ApiResponse
from hindsight_client_api.api_client import ApiClient as ApiClient
from hindsight_client_api.configuration import Configuration as Configuration
from hindsight_client_api.exceptions import OpenApiException as OpenApiException
from hindsight_client_api.exceptions import ApiTypeError as ApiTypeError
from hindsight_client_api.exceptions import ApiValueError as ApiValueError
from hindsight_client_api.exceptions import ApiKeyError as ApiKeyError
from hindsight_client_api.exceptions import ApiAttributeError as ApiAttributeError
from hindsight_client_api.exceptions import ApiException as ApiException
# import models into sdk package
from hindsight_client_api.models.add_background_request import AddBackgroundRequest
from hindsight_client_api.models.background_response import BackgroundResponse
from hindsight_client_api.models.bank_list_item import BankListItem
from hindsight_client_api.models.bank_list_response import BankListResponse
from hindsight_client_api.models.bank_profile_response import BankProfileResponse
from hindsight_client_api.models.bank_stats_response import BankStatsResponse
from hindsight_client_api.models.budget import Budget
from hindsight_client_api.models.cancel_operation_response import CancelOperationResponse
from hindsight_client_api.models.chunk_data import ChunkData
from hindsight_client_api.models.chunk_include_options import ChunkIncludeOptions
from hindsight_client_api.models.chunk_response import ChunkResponse
from hindsight_client_api.models.create_bank_request import CreateBankRequest
from hindsight_client_api.models.delete_document_response import DeleteDocumentResponse
from hindsight_client_api.models.delete_response import DeleteResponse
from hindsight_client_api.models.disposition_traits import DispositionTraits
from hindsight_client_api.models.document_response import DocumentResponse
from hindsight_client_api.models.entity_detail_response import EntityDetailResponse
from hindsight_client_api.models.entity_include_options import EntityIncludeOptions
from hindsight_client_api.models.entity_input import EntityInput
from hindsight_client_api.models.entity_list_item import EntityListItem
from hindsight_client_api.models.entity_list_response import EntityListResponse
from hindsight_client_api.models.entity_observation_response import EntityObservationResponse
from hindsight_client_api.models.entity_state_response import EntityStateResponse
from hindsight_client_api.models.graph_data_response import GraphDataResponse
from hindsight_client_api.models.http_validation_error import HTTPValidationError
from hindsight_client_api.models.include_options import IncludeOptions
from hindsight_client_api.models.list_documents_response import ListDocumentsResponse
from hindsight_client_api.models.list_memory_units_response import ListMemoryUnitsResponse
from hindsight_client_api.models.memory_item import MemoryItem
from hindsight_client_api.models.operation_response import OperationResponse
from hindsight_client_api.models.operations_list_response import OperationsListResponse
from hindsight_client_api.models.recall_request import RecallRequest
from hindsight_client_api.models.recall_response import RecallResponse
from hindsight_client_api.models.recall_result import RecallResult
from hindsight_client_api.models.reflect_fact import ReflectFact
from hindsight_client_api.models.reflect_include_options import ReflectIncludeOptions
from hindsight_client_api.models.reflect_request import ReflectRequest
from hindsight_client_api.models.reflect_response import ReflectResponse
from hindsight_client_api.models.retain_request import RetainRequest
from hindsight_client_api.models.retain_response import RetainResponse
from hindsight_client_api.models.update_disposition_request import UpdateDispositionRequest
from hindsight_client_api.models.validation_error import ValidationError
from hindsight_client_api.models.validation_error_loc_inner import ValidationErrorLocInner
from hindsight_client_api.models.add_background_request import AddBackgroundRequest as AddBackgroundRequest
from hindsight_client_api.models.background_response import BackgroundResponse as BackgroundResponse
from hindsight_client_api.models.bank_list_item import BankListItem as BankListItem
from hindsight_client_api.models.bank_list_response import BankListResponse as BankListResponse
from hindsight_client_api.models.bank_profile_response import BankProfileResponse as BankProfileResponse
from hindsight_client_api.models.bank_stats_response import BankStatsResponse as BankStatsResponse
from hindsight_client_api.models.budget import Budget as Budget
from hindsight_client_api.models.cancel_operation_response import CancelOperationResponse as CancelOperationResponse
from hindsight_client_api.models.chunk_data import ChunkData as ChunkData
from hindsight_client_api.models.chunk_include_options import ChunkIncludeOptions as ChunkIncludeOptions
from hindsight_client_api.models.chunk_response import ChunkResponse as ChunkResponse
from hindsight_client_api.models.create_bank_request import CreateBankRequest as CreateBankRequest
from hindsight_client_api.models.delete_document_response import DeleteDocumentResponse as DeleteDocumentResponse
from hindsight_client_api.models.delete_response import DeleteResponse as DeleteResponse
from hindsight_client_api.models.disposition_traits import DispositionTraits as DispositionTraits
from hindsight_client_api.models.document_response import DocumentResponse as DocumentResponse
from hindsight_client_api.models.entity_detail_response import EntityDetailResponse as EntityDetailResponse
from hindsight_client_api.models.entity_include_options import EntityIncludeOptions as EntityIncludeOptions
from hindsight_client_api.models.entity_list_item import EntityListItem as EntityListItem
from hindsight_client_api.models.entity_list_response import EntityListResponse as EntityListResponse
from hindsight_client_api.models.entity_observation_response import EntityObservationResponse as EntityObservationResponse
from hindsight_client_api.models.entity_state_response import EntityStateResponse as EntityStateResponse
from hindsight_client_api.models.graph_data_response import GraphDataResponse as GraphDataResponse
from hindsight_client_api.models.http_validation_error import HTTPValidationError as HTTPValidationError
from hindsight_client_api.models.include_options import IncludeOptions as IncludeOptions
from hindsight_client_api.models.list_documents_response import ListDocumentsResponse as ListDocumentsResponse
from hindsight_client_api.models.list_memory_units_response import ListMemoryUnitsResponse as ListMemoryUnitsResponse
from hindsight_client_api.models.memory_item import MemoryItem as MemoryItem
from hindsight_client_api.models.operation_response import OperationResponse as OperationResponse
from hindsight_client_api.models.operations_list_response import OperationsListResponse as OperationsListResponse
from hindsight_client_api.models.recall_request import RecallRequest as RecallRequest
from hindsight_client_api.models.recall_response import RecallResponse as RecallResponse
from hindsight_client_api.models.recall_result import RecallResult as RecallResult
from hindsight_client_api.models.reflect_fact import ReflectFact as ReflectFact
from hindsight_client_api.models.reflect_include_options import ReflectIncludeOptions as ReflectIncludeOptions
from hindsight_client_api.models.reflect_request import ReflectRequest as ReflectRequest
from hindsight_client_api.models.reflect_response import ReflectResponse as ReflectResponse
from hindsight_client_api.models.retain_request import RetainRequest as RetainRequest
from hindsight_client_api.models.retain_response import RetainResponse as RetainResponse
from hindsight_client_api.models.update_disposition_request import UpdateDispositionRequest as UpdateDispositionRequest
from hindsight_client_api.models.validation_error import ValidationError as ValidationError
from hindsight_client_api.models.validation_error_loc_inner import ValidationErrorLocInner as ValidationErrorLocInner
@@ -21,6 +21,7 @@ import mimetypes
import os
import re
import tempfile
import uuid
from urllib.parse import quote
from typing import Tuple, Optional, List, Dict, Union
@@ -359,6 +360,8 @@ class ApiClient:
return obj.get_secret_value()
elif isinstance(obj, self.PRIMITIVE_TYPES):
return obj
elif isinstance(obj, uuid.UUID):
return str(obj)
elif isinstance(obj, list):
return [
self.sanitize_for_serialization(sub_obj) for sub_obj in obj
@@ -385,6 +388,10 @@ class ApiClient:
else:
obj_dict = obj.__dict__
if isinstance(obj_dict, list):
# here we handle instances that can either be a list or something else, and only became a real list by calling to_dict()
return self.sanitize_for_serialization(obj_dict)
return {
key: self.sanitize_for_serialization(val)
for key, val in obj_dict.items()
@@ -407,7 +414,7 @@ class ApiClient:
data = json.loads(response_text)
except ValueError:
data = response_text
elif re.match(r'^application/(json|[\w!#$&.+-^_]+\+json)\s*(;|$)', content_type, re.IGNORECASE):
elif re.match(r'^application/(json|[\w!#$&.+\-^_]+\+json)\s*(;|$)', content_type, re.IGNORECASE):
if response_text == "":
data = ""
else:
@@ -456,13 +463,13 @@ class ApiClient:
if klass in self.PRIMITIVE_TYPES:
return self.__deserialize_primitive(data, klass)
elif klass == object:
elif klass is object:
return self.__deserialize_object(data)
elif klass == datetime.date:
elif klass is datetime.date:
return self.__deserialize_date(data)
elif klass == datetime.datetime:
elif klass is datetime.datetime:
return self.__deserialize_datetime(data)
elif klass == decimal.Decimal:
elif klass is decimal.Decimal:
return decimal.Decimal(data)
elif issubclass(klass, Enum):
return self.__deserialize_enum(data, klass)
@@ -520,7 +527,7 @@ class ApiClient:
if k in collection_formats:
collection_format = collection_formats[k]
if collection_format == 'multi':
new_params.extend((k, str(value)) for value in v)
new_params.extend((k, quote(str(value))) for value in v)
else:
if collection_format == 'ssv':
delimiter = ' '
@@ -17,7 +17,7 @@ import http.client as httplib
import logging
from logging import FileHandler
import sys
from typing import Any, ClassVar, Dict, List, Literal, Optional, TypedDict
from typing import Any, ClassVar, Dict, List, Literal, Optional, TypedDict, Union
from typing_extensions import NotRequired, Self
import urllib3
@@ -159,6 +159,10 @@ class Configuration:
:param ssl_ca_cert: str - the path to a file of concatenated CA certificates
in PEM format.
:param retries: Number of retries for API requests.
:param ca_cert_data: verify the peer using concatenated CA certificate data
in PEM (str) or DER (bytes) format.
:param cert_file: the path to a client certificate file, for mTLS.
:param key_file: the path to a client key file, for mTLS.
"""
@@ -172,13 +176,16 @@ class Configuration:
username: Optional[str]=None,
password: Optional[str]=None,
access_token: Optional[str]=None,
server_index: Optional[int]=None,
server_index: Optional[int]=None,
server_variables: Optional[ServerVariablesT]=None,
server_operation_index: Optional[Dict[int, int]]=None,
server_operation_variables: Optional[Dict[int, ServerVariablesT]]=None,
ignore_operation_servers: bool=False,
ssl_ca_cert: Optional[str]=None,
retries: Optional[int] = None,
ca_cert_data: Optional[Union[str, bytes]] = None,
cert_file: Optional[str]=None,
key_file: Optional[str]=None,
*,
debug: Optional[bool] = None,
) -> None:
@@ -256,10 +263,14 @@ class Configuration:
self.ssl_ca_cert = ssl_ca_cert
"""Set this to customize the certificate file to verify the peer.
"""
self.cert_file = None
self.ca_cert_data = ca_cert_data
"""Set this to verify the peer using PEM (str) or DER (bytes)
certificate data.
"""
self.cert_file = cert_file
"""client certificate file
"""
self.key_file = None
self.key_file = key_file
"""client key file
"""
self.assert_hostname = None
@@ -0,0 +1,31 @@
# AddBackgroundRequest
Request model for adding/merging background information.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**content** | **str** | New background information to add or merge |
**update_disposition** | **bool** | If true, infer disposition traits from the merged background (default: true) | [optional] [default to True]
## Example
```python
from hindsight_client_api.models.add_background_request import AddBackgroundRequest
# TODO update the JSON string below
json = "{}"
# create an instance of AddBackgroundRequest from a JSON string
add_background_request_instance = AddBackgroundRequest.from_json(json)
# print the JSON string representation of the object
print(AddBackgroundRequest.to_json())
# convert the object into a dict
add_background_request_dict = add_background_request_instance.to_dict()
# create an instance of AddBackgroundRequest from a dict
add_background_request_from_dict = AddBackgroundRequest.from_dict(add_background_request_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -0,0 +1,31 @@
# BackgroundResponse
Response model for background update.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**background** | **str** | |
**disposition** | [**DispositionTraits**](DispositionTraits.md) | | [optional]
## Example
```python
from hindsight_client_api.models.background_response import BackgroundResponse
# TODO update the JSON string below
json = "{}"
# create an instance of BackgroundResponse from a JSON string
background_response_instance = BackgroundResponse.from_json(json)
# print the JSON string representation of the object
print(BackgroundResponse.to_json())
# convert the object into a dict
background_response_dict = background_response_instance.to_dict()
# create an instance of BackgroundResponse from a dict
background_response_from_dict = BackgroundResponse.from_dict(background_response_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -0,0 +1,35 @@
# BankListItem
Bank list item with profile summary.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**bank_id** | **str** | |
**name** | **str** | | [optional]
**disposition** | [**DispositionTraits**](DispositionTraits.md) | |
**background** | **str** | | [optional]
**created_at** | **str** | | [optional]
**updated_at** | **str** | | [optional]
## Example
```python
from hindsight_client_api.models.bank_list_item import BankListItem
# TODO update the JSON string below
json = "{}"
# create an instance of BankListItem from a JSON string
bank_list_item_instance = BankListItem.from_json(json)
# print the JSON string representation of the object
print(BankListItem.to_json())
# convert the object into a dict
bank_list_item_dict = bank_list_item_instance.to_dict()
# create an instance of BankListItem from a dict
bank_list_item_from_dict = BankListItem.from_dict(bank_list_item_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -0,0 +1,30 @@
# BankListResponse
Response model for listing all banks.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**banks** | [**List[BankListItem]**](BankListItem.md) | |
## Example
```python
from hindsight_client_api.models.bank_list_response import BankListResponse
# TODO update the JSON string below
json = "{}"
# create an instance of BankListResponse from a JSON string
bank_list_response_instance = BankListResponse.from_json(json)
# print the JSON string representation of the object
print(BankListResponse.to_json())
# convert the object into a dict
bank_list_response_dict = bank_list_response_instance.to_dict()
# create an instance of BankListResponse from a dict
bank_list_response_from_dict = BankListResponse.from_dict(bank_list_response_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -0,0 +1,33 @@
# BankProfileResponse
Response model for bank profile.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**bank_id** | **str** | |
**name** | **str** | |
**disposition** | [**DispositionTraits**](DispositionTraits.md) | |
**background** | **str** | |
## Example
```python
from hindsight_client_api.models.bank_profile_response import BankProfileResponse
# TODO update the JSON string below
json = "{}"
# create an instance of BankProfileResponse from a JSON string
bank_profile_response_instance = BankProfileResponse.from_json(json)
# print the JSON string representation of the object
print(BankProfileResponse.to_json())
# convert the object into a dict
bank_profile_response_dict = bank_profile_response_instance.to_dict()
# create an instance of BankProfileResponse from a dict
bank_profile_response_from_dict = BankProfileResponse.from_dict(bank_profile_response_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -0,0 +1,39 @@
# BankStatsResponse
Response model for bank statistics endpoint.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**bank_id** | **str** | |
**total_nodes** | **int** | |
**total_links** | **int** | |
**total_documents** | **int** | |
**nodes_by_fact_type** | **Dict[str, int]** | |
**links_by_link_type** | **Dict[str, int]** | |
**links_by_fact_type** | **Dict[str, int]** | |
**links_breakdown** | **Dict[str, Dict[str, int]]** | |
**pending_operations** | **int** | |
**failed_operations** | **int** | |
## Example
```python
from hindsight_client_api.models.bank_stats_response import BankStatsResponse
# TODO update the JSON string below
json = "{}"
# create an instance of BankStatsResponse from a JSON string
bank_stats_response_instance = BankStatsResponse.from_json(json)
# print the JSON string representation of the object
print(BankStatsResponse.to_json())
# convert the object into a dict
bank_stats_response_dict = bank_stats_response_instance.to_dict()
# create an instance of BankStatsResponse from a dict
bank_stats_response_from_dict = BankStatsResponse.from_dict(bank_stats_response_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -0,0 +1,517 @@
# hindsight_client_api.BanksApi
All URIs are relative to *http://localhost*
Method | HTTP request | Description
------------- | ------------- | -------------
[**add_bank_background**](BanksApi.md#add_bank_background) | **POST** /v1/default/banks/{bank_id}/background | Add/merge memory bank background
[**create_or_update_bank**](BanksApi.md#create_or_update_bank) | **PUT** /v1/default/banks/{bank_id} | Create or update memory bank
[**delete_bank**](BanksApi.md#delete_bank) | **DELETE** /v1/default/banks/{bank_id} | Delete memory bank
[**get_agent_stats**](BanksApi.md#get_agent_stats) | **GET** /v1/default/banks/{bank_id}/stats | Get statistics for memory bank
[**get_bank_profile**](BanksApi.md#get_bank_profile) | **GET** /v1/default/banks/{bank_id}/profile | Get memory bank profile
[**list_banks**](BanksApi.md#list_banks) | **GET** /v1/default/banks | List all memory banks
[**update_bank_disposition**](BanksApi.md#update_bank_disposition) | **PUT** /v1/default/banks/{bank_id}/profile | Update memory bank disposition
# **add_bank_background**
> BackgroundResponse add_bank_background(bank_id, add_background_request, authorization=authorization)
Add/merge memory bank background
Add new background information or merge with existing. LLM intelligently resolves conflicts, normalizes to first person, and optionally infers disposition traits.
### Example
```python
import hindsight_client_api
from hindsight_client_api.models.add_background_request import AddBackgroundRequest
from hindsight_client_api.models.background_response import BackgroundResponse
from hindsight_client_api.rest import ApiException
from pprint import pprint
# Defining the host is optional and defaults to http://localhost
# See configuration.py for a list of all supported configuration parameters.
configuration = hindsight_client_api.Configuration(
host = "http://localhost"
)
# Enter a context with an instance of the API client
async with hindsight_client_api.ApiClient(configuration) as api_client:
# Create an instance of the API class
api_instance = hindsight_client_api.BanksApi(api_client)
bank_id = 'bank_id_example' # str |
add_background_request = hindsight_client_api.AddBackgroundRequest() # AddBackgroundRequest |
authorization = 'authorization_example' # str | (optional)
try:
# Add/merge memory bank background
api_response = await api_instance.add_bank_background(bank_id, add_background_request, authorization=authorization)
print("The response of BanksApi->add_bank_background:\n")
pprint(api_response)
except Exception as e:
print("Exception when calling BanksApi->add_bank_background: %s\n" % e)
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**bank_id** | **str**| |
**add_background_request** | [**AddBackgroundRequest**](AddBackgroundRequest.md)| |
**authorization** | **str**| | [optional]
### Return type
[**BackgroundResponse**](BackgroundResponse.md)
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: application/json
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
**200** | Successful Response | - |
**422** | Validation Error | - |
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **create_or_update_bank**
> BankProfileResponse create_or_update_bank(bank_id, create_bank_request, authorization=authorization)
Create or update memory bank
Create a new agent or update existing agent with disposition and background. Auto-fills missing fields with defaults.
### Example
```python
import hindsight_client_api
from hindsight_client_api.models.bank_profile_response import BankProfileResponse
from hindsight_client_api.models.create_bank_request import CreateBankRequest
from hindsight_client_api.rest import ApiException
from pprint import pprint
# Defining the host is optional and defaults to http://localhost
# See configuration.py for a list of all supported configuration parameters.
configuration = hindsight_client_api.Configuration(
host = "http://localhost"
)
# Enter a context with an instance of the API client
async with hindsight_client_api.ApiClient(configuration) as api_client:
# Create an instance of the API class
api_instance = hindsight_client_api.BanksApi(api_client)
bank_id = 'bank_id_example' # str |
create_bank_request = hindsight_client_api.CreateBankRequest() # CreateBankRequest |
authorization = 'authorization_example' # str | (optional)
try:
# Create or update memory bank
api_response = await api_instance.create_or_update_bank(bank_id, create_bank_request, authorization=authorization)
print("The response of BanksApi->create_or_update_bank:\n")
pprint(api_response)
except Exception as e:
print("Exception when calling BanksApi->create_or_update_bank: %s\n" % e)
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**bank_id** | **str**| |
**create_bank_request** | [**CreateBankRequest**](CreateBankRequest.md)| |
**authorization** | **str**| | [optional]
### Return type
[**BankProfileResponse**](BankProfileResponse.md)
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: application/json
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
**200** | Successful Response | - |
**422** | Validation Error | - |
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **delete_bank**
> DeleteResponse delete_bank(bank_id, authorization=authorization)
Delete memory bank
Delete an entire memory bank including all memories, entities, documents, and the bank profile itself. This is a destructive operation that cannot be undone.
### Example
```python
import hindsight_client_api
from hindsight_client_api.models.delete_response import DeleteResponse
from hindsight_client_api.rest import ApiException
from pprint import pprint
# Defining the host is optional and defaults to http://localhost
# See configuration.py for a list of all supported configuration parameters.
configuration = hindsight_client_api.Configuration(
host = "http://localhost"
)
# Enter a context with an instance of the API client
async with hindsight_client_api.ApiClient(configuration) as api_client:
# Create an instance of the API class
api_instance = hindsight_client_api.BanksApi(api_client)
bank_id = 'bank_id_example' # str |
authorization = 'authorization_example' # str | (optional)
try:
# Delete memory bank
api_response = await api_instance.delete_bank(bank_id, authorization=authorization)
print("The response of BanksApi->delete_bank:\n")
pprint(api_response)
except Exception as e:
print("Exception when calling BanksApi->delete_bank: %s\n" % e)
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**bank_id** | **str**| |
**authorization** | **str**| | [optional]
### Return type
[**DeleteResponse**](DeleteResponse.md)
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
**200** | Successful Response | - |
**422** | Validation Error | - |
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **get_agent_stats**
> BankStatsResponse get_agent_stats(bank_id)
Get statistics for memory bank
Get statistics about nodes and links for a specific agent
### Example
```python
import hindsight_client_api
from hindsight_client_api.models.bank_stats_response import BankStatsResponse
from hindsight_client_api.rest import ApiException
from pprint import pprint
# Defining the host is optional and defaults to http://localhost
# See configuration.py for a list of all supported configuration parameters.
configuration = hindsight_client_api.Configuration(
host = "http://localhost"
)
# Enter a context with an instance of the API client
async with hindsight_client_api.ApiClient(configuration) as api_client:
# Create an instance of the API class
api_instance = hindsight_client_api.BanksApi(api_client)
bank_id = 'bank_id_example' # str |
try:
# Get statistics for memory bank
api_response = await api_instance.get_agent_stats(bank_id)
print("The response of BanksApi->get_agent_stats:\n")
pprint(api_response)
except Exception as e:
print("Exception when calling BanksApi->get_agent_stats: %s\n" % e)
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**bank_id** | **str**| |
### Return type
[**BankStatsResponse**](BankStatsResponse.md)
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
**200** | Successful Response | - |
**422** | Validation Error | - |
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **get_bank_profile**
> BankProfileResponse get_bank_profile(bank_id, authorization=authorization)
Get memory bank profile
Get disposition traits and background for a memory bank. Auto-creates agent with defaults if not exists.
### Example
```python
import hindsight_client_api
from hindsight_client_api.models.bank_profile_response import BankProfileResponse
from hindsight_client_api.rest import ApiException
from pprint import pprint
# Defining the host is optional and defaults to http://localhost
# See configuration.py for a list of all supported configuration parameters.
configuration = hindsight_client_api.Configuration(
host = "http://localhost"
)
# Enter a context with an instance of the API client
async with hindsight_client_api.ApiClient(configuration) as api_client:
# Create an instance of the API class
api_instance = hindsight_client_api.BanksApi(api_client)
bank_id = 'bank_id_example' # str |
authorization = 'authorization_example' # str | (optional)
try:
# Get memory bank profile
api_response = await api_instance.get_bank_profile(bank_id, authorization=authorization)
print("The response of BanksApi->get_bank_profile:\n")
pprint(api_response)
except Exception as e:
print("Exception when calling BanksApi->get_bank_profile: %s\n" % e)
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**bank_id** | **str**| |
**authorization** | **str**| | [optional]
### Return type
[**BankProfileResponse**](BankProfileResponse.md)
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
**200** | Successful Response | - |
**422** | Validation Error | - |
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **list_banks**
> BankListResponse list_banks(authorization=authorization)
List all memory banks
Get a list of all agents with their profiles
### Example
```python
import hindsight_client_api
from hindsight_client_api.models.bank_list_response import BankListResponse
from hindsight_client_api.rest import ApiException
from pprint import pprint
# Defining the host is optional and defaults to http://localhost
# See configuration.py for a list of all supported configuration parameters.
configuration = hindsight_client_api.Configuration(
host = "http://localhost"
)
# Enter a context with an instance of the API client
async with hindsight_client_api.ApiClient(configuration) as api_client:
# Create an instance of the API class
api_instance = hindsight_client_api.BanksApi(api_client)
authorization = 'authorization_example' # str | (optional)
try:
# List all memory banks
api_response = await api_instance.list_banks(authorization=authorization)
print("The response of BanksApi->list_banks:\n")
pprint(api_response)
except Exception as e:
print("Exception when calling BanksApi->list_banks: %s\n" % e)
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**authorization** | **str**| | [optional]
### Return type
[**BankListResponse**](BankListResponse.md)
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
**200** | Successful Response | - |
**422** | Validation Error | - |
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **update_bank_disposition**
> BankProfileResponse update_bank_disposition(bank_id, update_disposition_request, authorization=authorization)
Update memory bank disposition
Update bank's disposition traits (skepticism, literalism, empathy)
### Example
```python
import hindsight_client_api
from hindsight_client_api.models.bank_profile_response import BankProfileResponse
from hindsight_client_api.models.update_disposition_request import UpdateDispositionRequest
from hindsight_client_api.rest import ApiException
from pprint import pprint
# Defining the host is optional and defaults to http://localhost
# See configuration.py for a list of all supported configuration parameters.
configuration = hindsight_client_api.Configuration(
host = "http://localhost"
)
# Enter a context with an instance of the API client
async with hindsight_client_api.ApiClient(configuration) as api_client:
# Create an instance of the API class
api_instance = hindsight_client_api.BanksApi(api_client)
bank_id = 'bank_id_example' # str |
update_disposition_request = hindsight_client_api.UpdateDispositionRequest() # UpdateDispositionRequest |
authorization = 'authorization_example' # str | (optional)
try:
# Update memory bank disposition
api_response = await api_instance.update_bank_disposition(bank_id, update_disposition_request, authorization=authorization)
print("The response of BanksApi->update_bank_disposition:\n")
pprint(api_response)
except Exception as e:
print("Exception when calling BanksApi->update_bank_disposition: %s\n" % e)
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**bank_id** | **str**| |
**update_disposition_request** | [**UpdateDispositionRequest**](UpdateDispositionRequest.md)| |
**authorization** | **str**| | [optional]
### Return type
[**BankProfileResponse**](BankProfileResponse.md)
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: application/json
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
**200** | Successful Response | - |
**422** | Validation Error | - |
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
@@ -0,0 +1,15 @@
# Budget
Budget levels for recall/reflect operations.
## Enum
* `LOW` (value: `'low'`)
* `MID` (value: `'mid'`)
* `HIGH` (value: `'high'`)
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -0,0 +1,32 @@
# CancelOperationResponse
Response model for cancel operation endpoint.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**success** | **bool** | |
**message** | **str** | |
**operation_id** | **str** | |
## Example
```python
from hindsight_client_api.models.cancel_operation_response import CancelOperationResponse
# TODO update the JSON string below
json = "{}"
# create an instance of CancelOperationResponse from a JSON string
cancel_operation_response_instance = CancelOperationResponse.from_json(json)
# print the JSON string representation of the object
print(CancelOperationResponse.to_json())
# convert the object into a dict
cancel_operation_response_dict = cancel_operation_response_instance.to_dict()
# create an instance of CancelOperationResponse from a dict
cancel_operation_response_from_dict = CancelOperationResponse.from_dict(cancel_operation_response_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -0,0 +1,33 @@
# ChunkData
Chunk data for a single chunk.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**id** | **str** | |
**text** | **str** | |
**chunk_index** | **int** | |
**truncated** | **bool** | Whether the chunk text was truncated due to token limits | [optional] [default to False]
## Example
```python
from hindsight_client_api.models.chunk_data import ChunkData
# TODO update the JSON string below
json = "{}"
# create an instance of ChunkData from a JSON string
chunk_data_instance = ChunkData.from_json(json)
# print the JSON string representation of the object
print(ChunkData.to_json())
# convert the object into a dict
chunk_data_dict = chunk_data_instance.to_dict()
# create an instance of ChunkData from a dict
chunk_data_from_dict = ChunkData.from_dict(chunk_data_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -0,0 +1,30 @@
# ChunkIncludeOptions
Options for including chunks in recall results.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**max_tokens** | **int** | Maximum tokens for chunks (chunks may be truncated) | [optional] [default to 8192]
## Example
```python
from hindsight_client_api.models.chunk_include_options import ChunkIncludeOptions
# TODO update the JSON string below
json = "{}"
# create an instance of ChunkIncludeOptions from a JSON string
chunk_include_options_instance = ChunkIncludeOptions.from_json(json)
# print the JSON string representation of the object
print(ChunkIncludeOptions.to_json())
# convert the object into a dict
chunk_include_options_dict = chunk_include_options_instance.to_dict()
# create an instance of ChunkIncludeOptions from a dict
chunk_include_options_from_dict = ChunkIncludeOptions.from_dict(chunk_include_options_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -0,0 +1,35 @@
# ChunkResponse
Response model for get chunk endpoint.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**chunk_id** | **str** | |
**document_id** | **str** | |
**bank_id** | **str** | |
**chunk_index** | **int** | |
**chunk_text** | **str** | |
**created_at** | **str** | |
## Example
```python
from hindsight_client_api.models.chunk_response import ChunkResponse
# TODO update the JSON string below
json = "{}"
# create an instance of ChunkResponse from a JSON string
chunk_response_instance = ChunkResponse.from_json(json)
# print the JSON string representation of the object
print(ChunkResponse.to_json())
# convert the object into a dict
chunk_response_dict = chunk_response_instance.to_dict()
# create an instance of ChunkResponse from a dict
chunk_response_from_dict = ChunkResponse.from_dict(chunk_response_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -0,0 +1,32 @@
# CreateBankRequest
Request model for creating/updating a bank.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**name** | **str** | | [optional]
**disposition** | [**DispositionTraits**](DispositionTraits.md) | | [optional]
**background** | **str** | | [optional]
## Example
```python
from hindsight_client_api.models.create_bank_request import CreateBankRequest
# TODO update the JSON string below
json = "{}"
# create an instance of CreateBankRequest from a JSON string
create_bank_request_instance = CreateBankRequest.from_json(json)
# print the JSON string representation of the object
print(CreateBankRequest.to_json())
# convert the object into a dict
create_bank_request_dict = create_bank_request_instance.to_dict()
# create an instance of CreateBankRequest from a dict
create_bank_request_from_dict = CreateBankRequest.from_dict(create_bank_request_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -0,0 +1,33 @@
# DeleteDocumentResponse
Response model for delete document endpoint.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**success** | **bool** | |
**message** | **str** | |
**document_id** | **str** | |
**memory_units_deleted** | **int** | |
## Example
```python
from hindsight_client_api.models.delete_document_response import DeleteDocumentResponse
# TODO update the JSON string below
json = "{}"
# create an instance of DeleteDocumentResponse from a JSON string
delete_document_response_instance = DeleteDocumentResponse.from_json(json)
# print the JSON string representation of the object
print(DeleteDocumentResponse.to_json())
# convert the object into a dict
delete_document_response_dict = delete_document_response_instance.to_dict()
# create an instance of DeleteDocumentResponse from a dict
delete_document_response_from_dict = DeleteDocumentResponse.from_dict(delete_document_response_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -0,0 +1,32 @@
# DeleteResponse
Response model for delete operations.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**success** | **bool** | |
**message** | **str** | | [optional]
**deleted_count** | **int** | | [optional]
## Example
```python
from hindsight_client_api.models.delete_response import DeleteResponse
# TODO update the JSON string below
json = "{}"
# create an instance of DeleteResponse from a JSON string
delete_response_instance = DeleteResponse.from_json(json)
# print the JSON string representation of the object
print(DeleteResponse.to_json())
# convert the object into a dict
delete_response_dict = delete_response_instance.to_dict()
# create an instance of DeleteResponse from a dict
delete_response_from_dict = DeleteResponse.from_dict(delete_response_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -0,0 +1,32 @@
# DispositionTraits
Disposition traits that influence how memories are formed and interpreted.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**skepticism** | **int** | How skeptical vs trusting (1&#x3D;trusting, 5&#x3D;skeptical) |
**literalism** | **int** | How literally to interpret information (1&#x3D;flexible, 5&#x3D;literal) |
**empathy** | **int** | How much to consider emotional context (1&#x3D;detached, 5&#x3D;empathetic) |
## Example
```python
from hindsight_client_api.models.disposition_traits import DispositionTraits
# TODO update the JSON string below
json = "{}"
# create an instance of DispositionTraits from a JSON string
disposition_traits_instance = DispositionTraits.from_json(json)
# print the JSON string representation of the object
print(DispositionTraits.to_json())
# convert the object into a dict
disposition_traits_dict = disposition_traits_instance.to_dict()
# create an instance of DispositionTraits from a dict
disposition_traits_from_dict = DispositionTraits.from_dict(disposition_traits_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -0,0 +1,36 @@
# DocumentResponse
Response model for get document endpoint.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**id** | **str** | |
**bank_id** | **str** | |
**original_text** | **str** | |
**content_hash** | **str** | |
**created_at** | **str** | |
**updated_at** | **str** | |
**memory_unit_count** | **int** | |
## Example
```python
from hindsight_client_api.models.document_response import DocumentResponse
# TODO update the JSON string below
json = "{}"
# create an instance of DocumentResponse from a JSON string
document_response_instance = DocumentResponse.from_json(json)
# print the JSON string representation of the object
print(DocumentResponse.to_json())
# convert the object into a dict
document_response_dict = document_response_instance.to_dict()
# create an instance of DocumentResponse from a dict
document_response_from_dict = DocumentResponse.from_dict(document_response_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -0,0 +1,313 @@
# hindsight_client_api.DocumentsApi
All URIs are relative to *http://localhost*
Method | HTTP request | Description
------------- | ------------- | -------------
[**delete_document**](DocumentsApi.md#delete_document) | **DELETE** /v1/default/banks/{bank_id}/documents/{document_id} | Delete a document
[**get_chunk**](DocumentsApi.md#get_chunk) | **GET** /v1/default/chunks/{chunk_id} | Get chunk details
[**get_document**](DocumentsApi.md#get_document) | **GET** /v1/default/banks/{bank_id}/documents/{document_id} | Get document details
[**list_documents**](DocumentsApi.md#list_documents) | **GET** /v1/default/banks/{bank_id}/documents | List documents
# **delete_document**
> DeleteDocumentResponse delete_document(bank_id, document_id, authorization=authorization)
Delete a document
Delete a document and all its associated memory units and links.
This will cascade delete:
- The document itself
- All memory units extracted from this document
- All links (temporal, semantic, entity) associated with those memory units
This operation cannot be undone.
### Example
```python
import hindsight_client_api
from hindsight_client_api.models.delete_document_response import DeleteDocumentResponse
from hindsight_client_api.rest import ApiException
from pprint import pprint
# Defining the host is optional and defaults to http://localhost
# See configuration.py for a list of all supported configuration parameters.
configuration = hindsight_client_api.Configuration(
host = "http://localhost"
)
# Enter a context with an instance of the API client
async with hindsight_client_api.ApiClient(configuration) as api_client:
# Create an instance of the API class
api_instance = hindsight_client_api.DocumentsApi(api_client)
bank_id = 'bank_id_example' # str |
document_id = 'document_id_example' # str |
authorization = 'authorization_example' # str | (optional)
try:
# Delete a document
api_response = await api_instance.delete_document(bank_id, document_id, authorization=authorization)
print("The response of DocumentsApi->delete_document:\n")
pprint(api_response)
except Exception as e:
print("Exception when calling DocumentsApi->delete_document: %s\n" % e)
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**bank_id** | **str**| |
**document_id** | **str**| |
**authorization** | **str**| | [optional]
### Return type
[**DeleteDocumentResponse**](DeleteDocumentResponse.md)
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
**200** | Successful Response | - |
**422** | Validation Error | - |
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **get_chunk**
> ChunkResponse get_chunk(chunk_id, authorization=authorization)
Get chunk details
Get a specific chunk by its ID
### Example
```python
import hindsight_client_api
from hindsight_client_api.models.chunk_response import ChunkResponse
from hindsight_client_api.rest import ApiException
from pprint import pprint
# Defining the host is optional and defaults to http://localhost
# See configuration.py for a list of all supported configuration parameters.
configuration = hindsight_client_api.Configuration(
host = "http://localhost"
)
# Enter a context with an instance of the API client
async with hindsight_client_api.ApiClient(configuration) as api_client:
# Create an instance of the API class
api_instance = hindsight_client_api.DocumentsApi(api_client)
chunk_id = 'chunk_id_example' # str |
authorization = 'authorization_example' # str | (optional)
try:
# Get chunk details
api_response = await api_instance.get_chunk(chunk_id, authorization=authorization)
print("The response of DocumentsApi->get_chunk:\n")
pprint(api_response)
except Exception as e:
print("Exception when calling DocumentsApi->get_chunk: %s\n" % e)
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**chunk_id** | **str**| |
**authorization** | **str**| | [optional]
### Return type
[**ChunkResponse**](ChunkResponse.md)
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
**200** | Successful Response | - |
**422** | Validation Error | - |
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **get_document**
> DocumentResponse get_document(bank_id, document_id, authorization=authorization)
Get document details
Get a specific document including its original text
### Example
```python
import hindsight_client_api
from hindsight_client_api.models.document_response import DocumentResponse
from hindsight_client_api.rest import ApiException
from pprint import pprint
# Defining the host is optional and defaults to http://localhost
# See configuration.py for a list of all supported configuration parameters.
configuration = hindsight_client_api.Configuration(
host = "http://localhost"
)
# Enter a context with an instance of the API client
async with hindsight_client_api.ApiClient(configuration) as api_client:
# Create an instance of the API class
api_instance = hindsight_client_api.DocumentsApi(api_client)
bank_id = 'bank_id_example' # str |
document_id = 'document_id_example' # str |
authorization = 'authorization_example' # str | (optional)
try:
# Get document details
api_response = await api_instance.get_document(bank_id, document_id, authorization=authorization)
print("The response of DocumentsApi->get_document:\n")
pprint(api_response)
except Exception as e:
print("Exception when calling DocumentsApi->get_document: %s\n" % e)
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**bank_id** | **str**| |
**document_id** | **str**| |
**authorization** | **str**| | [optional]
### Return type
[**DocumentResponse**](DocumentResponse.md)
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
**200** | Successful Response | - |
**422** | Validation Error | - |
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **list_documents**
> ListDocumentsResponse list_documents(bank_id, q=q, limit=limit, offset=offset, authorization=authorization)
List documents
List documents with pagination and optional search. Documents are the source content from which memory units are extracted.
### Example
```python
import hindsight_client_api
from hindsight_client_api.models.list_documents_response import ListDocumentsResponse
from hindsight_client_api.rest import ApiException
from pprint import pprint
# Defining the host is optional and defaults to http://localhost
# See configuration.py for a list of all supported configuration parameters.
configuration = hindsight_client_api.Configuration(
host = "http://localhost"
)
# Enter a context with an instance of the API client
async with hindsight_client_api.ApiClient(configuration) as api_client:
# Create an instance of the API class
api_instance = hindsight_client_api.DocumentsApi(api_client)
bank_id = 'bank_id_example' # str |
q = 'q_example' # str | (optional)
limit = 100 # int | (optional) (default to 100)
offset = 0 # int | (optional) (default to 0)
authorization = 'authorization_example' # str | (optional)
try:
# List documents
api_response = await api_instance.list_documents(bank_id, q=q, limit=limit, offset=offset, authorization=authorization)
print("The response of DocumentsApi->list_documents:\n")
pprint(api_response)
except Exception as e:
print("Exception when calling DocumentsApi->list_documents: %s\n" % e)
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**bank_id** | **str**| |
**q** | **str**| | [optional]
**limit** | **int**| | [optional] [default to 100]
**offset** | **int**| | [optional] [default to 0]
**authorization** | **str**| | [optional]
### Return type
[**ListDocumentsResponse**](ListDocumentsResponse.md)
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
**200** | Successful Response | - |
**422** | Validation Error | - |
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
@@ -0,0 +1,230 @@
# hindsight_client_api.EntitiesApi
All URIs are relative to *http://localhost*
Method | HTTP request | Description
------------- | ------------- | -------------
[**get_entity**](EntitiesApi.md#get_entity) | **GET** /v1/default/banks/{bank_id}/entities/{entity_id} | Get entity details
[**list_entities**](EntitiesApi.md#list_entities) | **GET** /v1/default/banks/{bank_id}/entities | List entities
[**regenerate_entity_observations**](EntitiesApi.md#regenerate_entity_observations) | **POST** /v1/default/banks/{bank_id}/entities/{entity_id}/regenerate | Regenerate entity observations
# **get_entity**
> EntityDetailResponse get_entity(bank_id, entity_id, authorization=authorization)
Get entity details
Get detailed information about an entity including observations (mental model).
### Example
```python
import hindsight_client_api
from hindsight_client_api.models.entity_detail_response import EntityDetailResponse
from hindsight_client_api.rest import ApiException
from pprint import pprint
# Defining the host is optional and defaults to http://localhost
# See configuration.py for a list of all supported configuration parameters.
configuration = hindsight_client_api.Configuration(
host = "http://localhost"
)
# Enter a context with an instance of the API client
async with hindsight_client_api.ApiClient(configuration) as api_client:
# Create an instance of the API class
api_instance = hindsight_client_api.EntitiesApi(api_client)
bank_id = 'bank_id_example' # str |
entity_id = 'entity_id_example' # str |
authorization = 'authorization_example' # str | (optional)
try:
# Get entity details
api_response = await api_instance.get_entity(bank_id, entity_id, authorization=authorization)
print("The response of EntitiesApi->get_entity:\n")
pprint(api_response)
except Exception as e:
print("Exception when calling EntitiesApi->get_entity: %s\n" % e)
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**bank_id** | **str**| |
**entity_id** | **str**| |
**authorization** | **str**| | [optional]
### Return type
[**EntityDetailResponse**](EntityDetailResponse.md)
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
**200** | Successful Response | - |
**422** | Validation Error | - |
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **list_entities**
> EntityListResponse list_entities(bank_id, limit=limit, authorization=authorization)
List entities
List all entities (people, organizations, etc.) known by the bank, ordered by mention count.
### Example
```python
import hindsight_client_api
from hindsight_client_api.models.entity_list_response import EntityListResponse
from hindsight_client_api.rest import ApiException
from pprint import pprint
# Defining the host is optional and defaults to http://localhost
# See configuration.py for a list of all supported configuration parameters.
configuration = hindsight_client_api.Configuration(
host = "http://localhost"
)
# Enter a context with an instance of the API client
async with hindsight_client_api.ApiClient(configuration) as api_client:
# Create an instance of the API class
api_instance = hindsight_client_api.EntitiesApi(api_client)
bank_id = 'bank_id_example' # str |
limit = 100 # int | Maximum number of entities to return (optional) (default to 100)
authorization = 'authorization_example' # str | (optional)
try:
# List entities
api_response = await api_instance.list_entities(bank_id, limit=limit, authorization=authorization)
print("The response of EntitiesApi->list_entities:\n")
pprint(api_response)
except Exception as e:
print("Exception when calling EntitiesApi->list_entities: %s\n" % e)
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**bank_id** | **str**| |
**limit** | **int**| Maximum number of entities to return | [optional] [default to 100]
**authorization** | **str**| | [optional]
### Return type
[**EntityListResponse**](EntityListResponse.md)
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
**200** | Successful Response | - |
**422** | Validation Error | - |
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **regenerate_entity_observations**
> EntityDetailResponse regenerate_entity_observations(bank_id, entity_id, authorization=authorization)
Regenerate entity observations
Regenerate observations for an entity based on all facts mentioning it.
### Example
```python
import hindsight_client_api
from hindsight_client_api.models.entity_detail_response import EntityDetailResponse
from hindsight_client_api.rest import ApiException
from pprint import pprint
# Defining the host is optional and defaults to http://localhost
# See configuration.py for a list of all supported configuration parameters.
configuration = hindsight_client_api.Configuration(
host = "http://localhost"
)
# Enter a context with an instance of the API client
async with hindsight_client_api.ApiClient(configuration) as api_client:
# Create an instance of the API class
api_instance = hindsight_client_api.EntitiesApi(api_client)
bank_id = 'bank_id_example' # str |
entity_id = 'entity_id_example' # str |
authorization = 'authorization_example' # str | (optional)
try:
# Regenerate entity observations
api_response = await api_instance.regenerate_entity_observations(bank_id, entity_id, authorization=authorization)
print("The response of EntitiesApi->regenerate_entity_observations:\n")
pprint(api_response)
except Exception as e:
print("Exception when calling EntitiesApi->regenerate_entity_observations: %s\n" % e)
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**bank_id** | **str**| |
**entity_id** | **str**| |
**authorization** | **str**| | [optional]
### Return type
[**EntityDetailResponse**](EntityDetailResponse.md)
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
**200** | Successful Response | - |
**422** | Validation Error | - |
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
@@ -0,0 +1,36 @@
# EntityDetailResponse
Response model for entity detail endpoint.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**id** | **str** | |
**canonical_name** | **str** | |
**mention_count** | **int** | |
**first_seen** | **str** | | [optional]
**last_seen** | **str** | | [optional]
**metadata** | **Dict[str, object]** | | [optional]
**observations** | [**List[EntityObservationResponse]**](EntityObservationResponse.md) | |
## Example
```python
from hindsight_client_api.models.entity_detail_response import EntityDetailResponse
# TODO update the JSON string below
json = "{}"
# create an instance of EntityDetailResponse from a JSON string
entity_detail_response_instance = EntityDetailResponse.from_json(json)
# print the JSON string representation of the object
print(EntityDetailResponse.to_json())
# convert the object into a dict
entity_detail_response_dict = entity_detail_response_instance.to_dict()
# create an instance of EntityDetailResponse from a dict
entity_detail_response_from_dict = EntityDetailResponse.from_dict(entity_detail_response_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -0,0 +1,30 @@
# EntityIncludeOptions
Options for including entity observations in recall results.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**max_tokens** | **int** | Maximum tokens for entity observations | [optional] [default to 500]
## Example
```python
from hindsight_client_api.models.entity_include_options import EntityIncludeOptions
# TODO update the JSON string below
json = "{}"
# create an instance of EntityIncludeOptions from a JSON string
entity_include_options_instance = EntityIncludeOptions.from_json(json)
# print the JSON string representation of the object
print(EntityIncludeOptions.to_json())
# convert the object into a dict
entity_include_options_dict = entity_include_options_instance.to_dict()
# create an instance of EntityIncludeOptions from a dict
entity_include_options_from_dict = EntityIncludeOptions.from_dict(entity_include_options_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -0,0 +1,35 @@
# EntityListItem
Entity list item with summary.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**id** | **str** | |
**canonical_name** | **str** | |
**mention_count** | **int** | |
**first_seen** | **str** | | [optional]
**last_seen** | **str** | | [optional]
**metadata** | **Dict[str, object]** | | [optional]
## Example
```python
from hindsight_client_api.models.entity_list_item import EntityListItem
# TODO update the JSON string below
json = "{}"
# create an instance of EntityListItem from a JSON string
entity_list_item_instance = EntityListItem.from_json(json)
# print the JSON string representation of the object
print(EntityListItem.to_json())
# convert the object into a dict
entity_list_item_dict = entity_list_item_instance.to_dict()
# create an instance of EntityListItem from a dict
entity_list_item_from_dict = EntityListItem.from_dict(entity_list_item_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -0,0 +1,30 @@
# EntityListResponse
Response model for entity list endpoint.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**items** | [**List[EntityListItem]**](EntityListItem.md) | |
## Example
```python
from hindsight_client_api.models.entity_list_response import EntityListResponse
# TODO update the JSON string below
json = "{}"
# create an instance of EntityListResponse from a JSON string
entity_list_response_instance = EntityListResponse.from_json(json)
# print the JSON string representation of the object
print(EntityListResponse.to_json())
# convert the object into a dict
entity_list_response_dict = entity_list_response_instance.to_dict()
# create an instance of EntityListResponse from a dict
entity_list_response_from_dict = EntityListResponse.from_dict(entity_list_response_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -0,0 +1,31 @@
# EntityObservationResponse
An observation about an entity.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**text** | **str** | |
**mentioned_at** | **str** | | [optional]
## Example
```python
from hindsight_client_api.models.entity_observation_response import EntityObservationResponse
# TODO update the JSON string below
json = "{}"
# create an instance of EntityObservationResponse from a JSON string
entity_observation_response_instance = EntityObservationResponse.from_json(json)
# print the JSON string representation of the object
print(EntityObservationResponse.to_json())
# convert the object into a dict
entity_observation_response_dict = entity_observation_response_instance.to_dict()
# create an instance of EntityObservationResponse from a dict
entity_observation_response_from_dict = EntityObservationResponse.from_dict(entity_observation_response_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -0,0 +1,32 @@
# EntityStateResponse
Current mental model of an entity.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**entity_id** | **str** | |
**canonical_name** | **str** | |
**observations** | [**List[EntityObservationResponse]**](EntityObservationResponse.md) | |
## Example
```python
from hindsight_client_api.models.entity_state_response import EntityStateResponse
# TODO update the JSON string below
json = "{}"
# create an instance of EntityStateResponse from a JSON string
entity_state_response_instance = EntityStateResponse.from_json(json)
# print the JSON string representation of the object
print(EntityStateResponse.to_json())
# convert the object into a dict
entity_state_response_dict = entity_state_response_instance.to_dict()
# create an instance of EntityStateResponse from a dict
entity_state_response_from_dict = EntityStateResponse.from_dict(entity_state_response_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -0,0 +1,33 @@
# GraphDataResponse
Response model for graph data endpoint.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**nodes** | **List[Dict[str, object]]** | |
**edges** | **List[Dict[str, object]]** | |
**table_rows** | **List[Dict[str, object]]** | |
**total_units** | **int** | |
## Example
```python
from hindsight_client_api.models.graph_data_response import GraphDataResponse
# TODO update the JSON string below
json = "{}"
# create an instance of GraphDataResponse from a JSON string
graph_data_response_instance = GraphDataResponse.from_json(json)
# print the JSON string representation of the object
print(GraphDataResponse.to_json())
# convert the object into a dict
graph_data_response_dict = graph_data_response_instance.to_dict()
# create an instance of GraphDataResponse from a dict
graph_data_response_from_dict = GraphDataResponse.from_dict(graph_data_response_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -0,0 +1,29 @@
# HTTPValidationError
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**detail** | [**List[ValidationError]**](ValidationError.md) | | [optional]
## Example
```python
from hindsight_client_api.models.http_validation_error import HTTPValidationError
# TODO update the JSON string below
json = "{}"
# create an instance of HTTPValidationError from a JSON string
http_validation_error_instance = HTTPValidationError.from_json(json)
# print the JSON string representation of the object
print(HTTPValidationError.to_json())
# convert the object into a dict
http_validation_error_dict = http_validation_error_instance.to_dict()
# create an instance of HTTPValidationError from a dict
http_validation_error_from_dict = HTTPValidationError.from_dict(http_validation_error_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -0,0 +1,31 @@
# IncludeOptions
Options for including additional data in recall results.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**entities** | [**EntityIncludeOptions**](EntityIncludeOptions.md) | | [optional]
**chunks** | [**ChunkIncludeOptions**](ChunkIncludeOptions.md) | | [optional]
## Example
```python
from hindsight_client_api.models.include_options import IncludeOptions
# TODO update the JSON string below
json = "{}"
# create an instance of IncludeOptions from a JSON string
include_options_instance = IncludeOptions.from_json(json)
# print the JSON string representation of the object
print(IncludeOptions.to_json())
# convert the object into a dict
include_options_dict = include_options_instance.to_dict()
# create an instance of IncludeOptions from a dict
include_options_from_dict = IncludeOptions.from_dict(include_options_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -0,0 +1,33 @@
# ListDocumentsResponse
Response model for list documents endpoint.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**items** | **List[Dict[str, object]]** | |
**total** | **int** | |
**limit** | **int** | |
**offset** | **int** | |
## Example
```python
from hindsight_client_api.models.list_documents_response import ListDocumentsResponse
# TODO update the JSON string below
json = "{}"
# create an instance of ListDocumentsResponse from a JSON string
list_documents_response_instance = ListDocumentsResponse.from_json(json)
# print the JSON string representation of the object
print(ListDocumentsResponse.to_json())
# convert the object into a dict
list_documents_response_dict = list_documents_response_instance.to_dict()
# create an instance of ListDocumentsResponse from a dict
list_documents_response_from_dict = ListDocumentsResponse.from_dict(list_documents_response_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -0,0 +1,33 @@
# ListMemoryUnitsResponse
Response model for list memory units endpoint.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**items** | **List[Dict[str, object]]** | |
**total** | **int** | |
**limit** | **int** | |
**offset** | **int** | |
## Example
```python
from hindsight_client_api.models.list_memory_units_response import ListMemoryUnitsResponse
# TODO update the JSON string below
json = "{}"
# create an instance of ListMemoryUnitsResponse from a JSON string
list_memory_units_response_instance = ListMemoryUnitsResponse.from_json(json)
# print the JSON string representation of the object
print(ListMemoryUnitsResponse.to_json())
# convert the object into a dict
list_memory_units_response_dict = list_memory_units_response_instance.to_dict()
# create an instance of ListMemoryUnitsResponse from a dict
list_memory_units_response_from_dict = ListMemoryUnitsResponse.from_dict(list_memory_units_response_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -0,0 +1,499 @@
# hindsight_client_api.MemoryApi
All URIs are relative to *http://localhost*
Method | HTTP request | Description
------------- | ------------- | -------------
[**clear_bank_memories**](MemoryApi.md#clear_bank_memories) | **DELETE** /v1/default/banks/{bank_id}/memories | Clear memory bank memories
[**get_graph**](MemoryApi.md#get_graph) | **GET** /v1/default/banks/{bank_id}/graph | Get memory graph data
[**list_memories**](MemoryApi.md#list_memories) | **GET** /v1/default/banks/{bank_id}/memories/list | List memory units
[**recall_memories**](MemoryApi.md#recall_memories) | **POST** /v1/default/banks/{bank_id}/memories/recall | Recall memory
[**reflect**](MemoryApi.md#reflect) | **POST** /v1/default/banks/{bank_id}/reflect | Reflect and generate answer
[**retain_memories**](MemoryApi.md#retain_memories) | **POST** /v1/default/banks/{bank_id}/memories | Retain memories
# **clear_bank_memories**
> DeleteResponse clear_bank_memories(bank_id, type=type, authorization=authorization)
Clear memory bank memories
Delete memory units for a memory bank. Optionally filter by type (world, experience, opinion) to delete only specific types. This is a destructive operation that cannot be undone. The bank profile (disposition and background) will be preserved.
### Example
```python
import hindsight_client_api
from hindsight_client_api.models.delete_response import DeleteResponse
from hindsight_client_api.rest import ApiException
from pprint import pprint
# Defining the host is optional and defaults to http://localhost
# See configuration.py for a list of all supported configuration parameters.
configuration = hindsight_client_api.Configuration(
host = "http://localhost"
)
# Enter a context with an instance of the API client
async with hindsight_client_api.ApiClient(configuration) as api_client:
# Create an instance of the API class
api_instance = hindsight_client_api.MemoryApi(api_client)
bank_id = 'bank_id_example' # str |
type = 'type_example' # str | Optional fact type filter (world, experience, opinion) (optional)
authorization = 'authorization_example' # str | (optional)
try:
# Clear memory bank memories
api_response = await api_instance.clear_bank_memories(bank_id, type=type, authorization=authorization)
print("The response of MemoryApi->clear_bank_memories:\n")
pprint(api_response)
except Exception as e:
print("Exception when calling MemoryApi->clear_bank_memories: %s\n" % e)
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**bank_id** | **str**| |
**type** | **str**| Optional fact type filter (world, experience, opinion) | [optional]
**authorization** | **str**| | [optional]
### Return type
[**DeleteResponse**](DeleteResponse.md)
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
**200** | Successful Response | - |
**422** | Validation Error | - |
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **get_graph**
> GraphDataResponse get_graph(bank_id, type=type, authorization=authorization)
Get memory graph data
Retrieve graph data for visualization, optionally filtered by type (world/experience/opinion). Limited to 1000 most recent items.
### Example
```python
import hindsight_client_api
from hindsight_client_api.models.graph_data_response import GraphDataResponse
from hindsight_client_api.rest import ApiException
from pprint import pprint
# Defining the host is optional and defaults to http://localhost
# See configuration.py for a list of all supported configuration parameters.
configuration = hindsight_client_api.Configuration(
host = "http://localhost"
)
# Enter a context with an instance of the API client
async with hindsight_client_api.ApiClient(configuration) as api_client:
# Create an instance of the API class
api_instance = hindsight_client_api.MemoryApi(api_client)
bank_id = 'bank_id_example' # str |
type = 'type_example' # str | (optional)
authorization = 'authorization_example' # str | (optional)
try:
# Get memory graph data
api_response = await api_instance.get_graph(bank_id, type=type, authorization=authorization)
print("The response of MemoryApi->get_graph:\n")
pprint(api_response)
except Exception as e:
print("Exception when calling MemoryApi->get_graph: %s\n" % e)
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**bank_id** | **str**| |
**type** | **str**| | [optional]
**authorization** | **str**| | [optional]
### Return type
[**GraphDataResponse**](GraphDataResponse.md)
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
**200** | Successful Response | - |
**422** | Validation Error | - |
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **list_memories**
> ListMemoryUnitsResponse list_memories(bank_id, type=type, q=q, limit=limit, offset=offset, authorization=authorization)
List memory units
List memory units with pagination and optional full-text search. Supports filtering by type. Results are sorted by most recent first (mentioned_at DESC, then created_at DESC).
### Example
```python
import hindsight_client_api
from hindsight_client_api.models.list_memory_units_response import ListMemoryUnitsResponse
from hindsight_client_api.rest import ApiException
from pprint import pprint
# Defining the host is optional and defaults to http://localhost
# See configuration.py for a list of all supported configuration parameters.
configuration = hindsight_client_api.Configuration(
host = "http://localhost"
)
# Enter a context with an instance of the API client
async with hindsight_client_api.ApiClient(configuration) as api_client:
# Create an instance of the API class
api_instance = hindsight_client_api.MemoryApi(api_client)
bank_id = 'bank_id_example' # str |
type = 'type_example' # str | (optional)
q = 'q_example' # str | (optional)
limit = 100 # int | (optional) (default to 100)
offset = 0 # int | (optional) (default to 0)
authorization = 'authorization_example' # str | (optional)
try:
# List memory units
api_response = await api_instance.list_memories(bank_id, type=type, q=q, limit=limit, offset=offset, authorization=authorization)
print("The response of MemoryApi->list_memories:\n")
pprint(api_response)
except Exception as e:
print("Exception when calling MemoryApi->list_memories: %s\n" % e)
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**bank_id** | **str**| |
**type** | **str**| | [optional]
**q** | **str**| | [optional]
**limit** | **int**| | [optional] [default to 100]
**offset** | **int**| | [optional] [default to 0]
**authorization** | **str**| | [optional]
### Return type
[**ListMemoryUnitsResponse**](ListMemoryUnitsResponse.md)
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
**200** | Successful Response | - |
**422** | Validation Error | - |
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **recall_memories**
> RecallResponse recall_memories(bank_id, recall_request, authorization=authorization)
Recall memory
Recall memory using semantic similarity and spreading activation.
The type parameter is optional and must be one of:
- `world`: General knowledge about people, places, events, and things that happen
- `experience`: Memories about experience, conversations, actions taken, and tasks performed
- `opinion`: The bank's formed beliefs, perspectives, and viewpoints
Set `include_entities=true` to get entity observations alongside recall results.
### Example
```python
import hindsight_client_api
from hindsight_client_api.models.recall_request import RecallRequest
from hindsight_client_api.models.recall_response import RecallResponse
from hindsight_client_api.rest import ApiException
from pprint import pprint
# Defining the host is optional and defaults to http://localhost
# See configuration.py for a list of all supported configuration parameters.
configuration = hindsight_client_api.Configuration(
host = "http://localhost"
)
# Enter a context with an instance of the API client
async with hindsight_client_api.ApiClient(configuration) as api_client:
# Create an instance of the API class
api_instance = hindsight_client_api.MemoryApi(api_client)
bank_id = 'bank_id_example' # str |
recall_request = hindsight_client_api.RecallRequest() # RecallRequest |
authorization = 'authorization_example' # str | (optional)
try:
# Recall memory
api_response = await api_instance.recall_memories(bank_id, recall_request, authorization=authorization)
print("The response of MemoryApi->recall_memories:\n")
pprint(api_response)
except Exception as e:
print("Exception when calling MemoryApi->recall_memories: %s\n" % e)
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**bank_id** | **str**| |
**recall_request** | [**RecallRequest**](RecallRequest.md)| |
**authorization** | **str**| | [optional]
### Return type
[**RecallResponse**](RecallResponse.md)
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: application/json
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
**200** | Successful Response | - |
**422** | Validation Error | - |
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **reflect**
> ReflectResponse reflect(bank_id, reflect_request, authorization=authorization)
Reflect and generate answer
Reflect and formulate an answer using bank identity, world facts, and opinions.
This endpoint:
1. Retrieves experience (conversations and events)
2. Retrieves world facts relevant to the query
3. Retrieves existing opinions (bank's perspectives)
4. Uses LLM to formulate a contextual answer
5. Extracts and stores any new opinions formed
6. Returns plain text answer, the facts used, and new opinions
### Example
```python
import hindsight_client_api
from hindsight_client_api.models.reflect_request import ReflectRequest
from hindsight_client_api.models.reflect_response import ReflectResponse
from hindsight_client_api.rest import ApiException
from pprint import pprint
# Defining the host is optional and defaults to http://localhost
# See configuration.py for a list of all supported configuration parameters.
configuration = hindsight_client_api.Configuration(
host = "http://localhost"
)
# Enter a context with an instance of the API client
async with hindsight_client_api.ApiClient(configuration) as api_client:
# Create an instance of the API class
api_instance = hindsight_client_api.MemoryApi(api_client)
bank_id = 'bank_id_example' # str |
reflect_request = hindsight_client_api.ReflectRequest() # ReflectRequest |
authorization = 'authorization_example' # str | (optional)
try:
# Reflect and generate answer
api_response = await api_instance.reflect(bank_id, reflect_request, authorization=authorization)
print("The response of MemoryApi->reflect:\n")
pprint(api_response)
except Exception as e:
print("Exception when calling MemoryApi->reflect: %s\n" % e)
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**bank_id** | **str**| |
**reflect_request** | [**ReflectRequest**](ReflectRequest.md)| |
**authorization** | **str**| | [optional]
### Return type
[**ReflectResponse**](ReflectResponse.md)
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: application/json
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
**200** | Successful Response | - |
**422** | Validation Error | - |
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **retain_memories**
> RetainResponse retain_memories(bank_id, retain_request, authorization=authorization)
Retain memories
Retain memory items with automatic fact extraction.
This is the main endpoint for storing memories. It supports both synchronous and asynchronous processing via the `async` parameter.
**Features:**
- Efficient batch processing
- Automatic fact extraction from natural language
- Entity recognition and linking
- Document tracking with automatic upsert (when document_id is provided)
- Temporal and semantic linking
- Optional asynchronous processing
**The system automatically:**
1. Extracts semantic facts from the content
2. Generates embeddings
3. Deduplicates similar facts
4. Creates temporal, semantic, and entity links
5. Tracks document metadata
**When `async=true`:** Returns immediately after queuing. Use the operations endpoint to monitor progress.
**When `async=false` (default):** Waits for processing to complete.
**Note:** If a memory item has a `document_id` that already exists, the old document and its memory units will be deleted before creating new ones (upsert behavior).
### Example
```python
import hindsight_client_api
from hindsight_client_api.models.retain_request import RetainRequest
from hindsight_client_api.models.retain_response import RetainResponse
from hindsight_client_api.rest import ApiException
from pprint import pprint
# Defining the host is optional and defaults to http://localhost
# See configuration.py for a list of all supported configuration parameters.
configuration = hindsight_client_api.Configuration(
host = "http://localhost"
)
# Enter a context with an instance of the API client
async with hindsight_client_api.ApiClient(configuration) as api_client:
# Create an instance of the API class
api_instance = hindsight_client_api.MemoryApi(api_client)
bank_id = 'bank_id_example' # str |
retain_request = hindsight_client_api.RetainRequest() # RetainRequest |
authorization = 'authorization_example' # str | (optional)
try:
# Retain memories
api_response = await api_instance.retain_memories(bank_id, retain_request, authorization=authorization)
print("The response of MemoryApi->retain_memories:\n")
pprint(api_response)
except Exception as e:
print("Exception when calling MemoryApi->retain_memories: %s\n" % e)
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**bank_id** | **str**| |
**retain_request** | [**RetainRequest**](RetainRequest.md)| |
**authorization** | **str**| | [optional]
### Return type
[**RetainResponse**](RetainResponse.md)
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: application/json
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
**200** | Successful Response | - |
**422** | Validation Error | - |
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
@@ -0,0 +1,34 @@
# MemoryItem
Single memory item for retain.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**content** | **str** | |
**timestamp** | **datetime** | | [optional]
**context** | **str** | | [optional]
**metadata** | **Dict[str, str]** | | [optional]
**document_id** | **str** | | [optional]
## Example
```python
from hindsight_client_api.models.memory_item import MemoryItem
# TODO update the JSON string below
json = "{}"
# create an instance of MemoryItem from a JSON string
memory_item_instance = MemoryItem.from_json(json)
# print the JSON string representation of the object
print(MemoryItem.to_json())
# convert the object into a dict
memory_item_dict = memory_item_instance.to_dict()
# create an instance of MemoryItem from a dict
memory_item_from_dict = MemoryItem.from_dict(memory_item_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -0,0 +1,136 @@
# hindsight_client_api.MonitoringApi
All URIs are relative to *http://localhost*
Method | HTTP request | Description
------------- | ------------- | -------------
[**health_endpoint_health_get**](MonitoringApi.md#health_endpoint_health_get) | **GET** /health | Health check endpoint
[**metrics_endpoint_metrics_get**](MonitoringApi.md#metrics_endpoint_metrics_get) | **GET** /metrics | Prometheus metrics endpoint
# **health_endpoint_health_get**
> object health_endpoint_health_get()
Health check endpoint
Checks the health of the API and database connection
### Example
```python
import hindsight_client_api
from hindsight_client_api.rest import ApiException
from pprint import pprint
# Defining the host is optional and defaults to http://localhost
# See configuration.py for a list of all supported configuration parameters.
configuration = hindsight_client_api.Configuration(
host = "http://localhost"
)
# Enter a context with an instance of the API client
async with hindsight_client_api.ApiClient(configuration) as api_client:
# Create an instance of the API class
api_instance = hindsight_client_api.MonitoringApi(api_client)
try:
# Health check endpoint
api_response = await api_instance.health_endpoint_health_get()
print("The response of MonitoringApi->health_endpoint_health_get:\n")
pprint(api_response)
except Exception as e:
print("Exception when calling MonitoringApi->health_endpoint_health_get: %s\n" % e)
```
### Parameters
This endpoint does not need any parameter.
### Return type
**object**
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
**200** | Successful Response | - |
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **metrics_endpoint_metrics_get**
> object metrics_endpoint_metrics_get()
Prometheus metrics endpoint
Exports metrics in Prometheus format for scraping
### Example
```python
import hindsight_client_api
from hindsight_client_api.rest import ApiException
from pprint import pprint
# Defining the host is optional and defaults to http://localhost
# See configuration.py for a list of all supported configuration parameters.
configuration = hindsight_client_api.Configuration(
host = "http://localhost"
)
# Enter a context with an instance of the API client
async with hindsight_client_api.ApiClient(configuration) as api_client:
# Create an instance of the API class
api_instance = hindsight_client_api.MonitoringApi(api_client)
try:
# Prometheus metrics endpoint
api_response = await api_instance.metrics_endpoint_metrics_get()
print("The response of MonitoringApi->metrics_endpoint_metrics_get:\n")
pprint(api_response)
except Exception as e:
print("Exception when calling MonitoringApi->metrics_endpoint_metrics_get: %s\n" % e)
```
### Parameters
This endpoint does not need any parameter.
### Return type
**object**
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
**200** | Successful Response | - |
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
@@ -0,0 +1,36 @@
# OperationResponse
Response model for a single async operation.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**id** | **str** | |
**task_type** | **str** | |
**items_count** | **int** | |
**document_id** | **str** | |
**created_at** | **str** | |
**status** | **str** | |
**error_message** | **str** | |
## Example
```python
from hindsight_client_api.models.operation_response import OperationResponse
# TODO update the JSON string below
json = "{}"
# create an instance of OperationResponse from a JSON string
operation_response_instance = OperationResponse.from_json(json)
# print the JSON string representation of the object
print(OperationResponse.to_json())
# convert the object into a dict
operation_response_dict = operation_response_instance.to_dict()
# create an instance of OperationResponse from a dict
operation_response_from_dict = OperationResponse.from_dict(operation_response_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -0,0 +1,154 @@
# hindsight_client_api.OperationsApi
All URIs are relative to *http://localhost*
Method | HTTP request | Description
------------- | ------------- | -------------
[**cancel_operation**](OperationsApi.md#cancel_operation) | **DELETE** /v1/default/banks/{bank_id}/operations/{operation_id} | Cancel a pending async operation
[**list_operations**](OperationsApi.md#list_operations) | **GET** /v1/default/banks/{bank_id}/operations | List async operations
# **cancel_operation**
> CancelOperationResponse cancel_operation(bank_id, operation_id, authorization=authorization)
Cancel a pending async operation
Cancel a pending async operation by removing it from the queue
### Example
```python
import hindsight_client_api
from hindsight_client_api.models.cancel_operation_response import CancelOperationResponse
from hindsight_client_api.rest import ApiException
from pprint import pprint
# Defining the host is optional and defaults to http://localhost
# See configuration.py for a list of all supported configuration parameters.
configuration = hindsight_client_api.Configuration(
host = "http://localhost"
)
# Enter a context with an instance of the API client
async with hindsight_client_api.ApiClient(configuration) as api_client:
# Create an instance of the API class
api_instance = hindsight_client_api.OperationsApi(api_client)
bank_id = 'bank_id_example' # str |
operation_id = 'operation_id_example' # str |
authorization = 'authorization_example' # str | (optional)
try:
# Cancel a pending async operation
api_response = await api_instance.cancel_operation(bank_id, operation_id, authorization=authorization)
print("The response of OperationsApi->cancel_operation:\n")
pprint(api_response)
except Exception as e:
print("Exception when calling OperationsApi->cancel_operation: %s\n" % e)
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**bank_id** | **str**| |
**operation_id** | **str**| |
**authorization** | **str**| | [optional]
### Return type
[**CancelOperationResponse**](CancelOperationResponse.md)
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
**200** | Successful Response | - |
**422** | Validation Error | - |
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **list_operations**
> OperationsListResponse list_operations(bank_id, authorization=authorization)
List async operations
Get a list of all async operations (pending and failed) for a specific agent, including error messages for failed operations
### Example
```python
import hindsight_client_api
from hindsight_client_api.models.operations_list_response import OperationsListResponse
from hindsight_client_api.rest import ApiException
from pprint import pprint
# Defining the host is optional and defaults to http://localhost
# See configuration.py for a list of all supported configuration parameters.
configuration = hindsight_client_api.Configuration(
host = "http://localhost"
)
# Enter a context with an instance of the API client
async with hindsight_client_api.ApiClient(configuration) as api_client:
# Create an instance of the API class
api_instance = hindsight_client_api.OperationsApi(api_client)
bank_id = 'bank_id_example' # str |
authorization = 'authorization_example' # str | (optional)
try:
# List async operations
api_response = await api_instance.list_operations(bank_id, authorization=authorization)
print("The response of OperationsApi->list_operations:\n")
pprint(api_response)
except Exception as e:
print("Exception when calling OperationsApi->list_operations: %s\n" % e)
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**bank_id** | **str**| |
**authorization** | **str**| | [optional]
### Return type
[**OperationsListResponse**](OperationsListResponse.md)
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
**200** | Successful Response | - |
**422** | Validation Error | - |
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
@@ -0,0 +1,31 @@
# OperationsListResponse
Response model for list operations endpoint.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**bank_id** | **str** | |
**operations** | [**List[OperationResponse]**](OperationResponse.md) | |
## Example
```python
from hindsight_client_api.models.operations_list_response import OperationsListResponse
# TODO update the JSON string below
json = "{}"
# create an instance of OperationsListResponse from a JSON string
operations_list_response_instance = OperationsListResponse.from_json(json)
# print the JSON string representation of the object
print(OperationsListResponse.to_json())
# convert the object into a dict
operations_list_response_dict = operations_list_response_instance.to_dict()
# create an instance of OperationsListResponse from a dict
operations_list_response_from_dict = OperationsListResponse.from_dict(operations_list_response_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -0,0 +1,36 @@
# RecallRequest
Request model for recall endpoint.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**query** | **str** | |
**types** | **List[str]** | | [optional]
**budget** | [**Budget**](Budget.md) | | [optional]
**max_tokens** | **int** | | [optional] [default to 4096]
**trace** | **bool** | | [optional] [default to False]
**query_timestamp** | **str** | | [optional]
**include** | [**IncludeOptions**](IncludeOptions.md) | Options for including additional data (entities are included by default) | [optional]
## Example
```python
from hindsight_client_api.models.recall_request import RecallRequest
# TODO update the JSON string below
json = "{}"
# create an instance of RecallRequest from a JSON string
recall_request_instance = RecallRequest.from_json(json)
# print the JSON string representation of the object
print(RecallRequest.to_json())
# convert the object into a dict
recall_request_dict = recall_request_instance.to_dict()
# create an instance of RecallRequest from a dict
recall_request_from_dict = RecallRequest.from_dict(recall_request_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -0,0 +1,33 @@
# RecallResponse
Response model for recall endpoints.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**results** | [**List[RecallResult]**](RecallResult.md) | |
**trace** | **Dict[str, object]** | | [optional]
**entities** | [**Dict[str, EntityStateResponse]**](EntityStateResponse.md) | | [optional]
**chunks** | [**Dict[str, ChunkData]**](ChunkData.md) | | [optional]
## Example
```python
from hindsight_client_api.models.recall_response import RecallResponse
# TODO update the JSON string below
json = "{}"
# create an instance of RecallResponse from a JSON string
recall_response_instance = RecallResponse.from_json(json)
# print the JSON string representation of the object
print(RecallResponse.to_json())
# convert the object into a dict
recall_response_dict = recall_response_instance.to_dict()
# create an instance of RecallResponse from a dict
recall_response_from_dict = RecallResponse.from_dict(recall_response_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -0,0 +1,40 @@
# RecallResult
Single recall result item.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**id** | **str** | |
**text** | **str** | |
**type** | **str** | | [optional]
**entities** | **List[str]** | | [optional]
**context** | **str** | | [optional]
**occurred_start** | **str** | | [optional]
**occurred_end** | **str** | | [optional]
**mentioned_at** | **str** | | [optional]
**document_id** | **str** | | [optional]
**metadata** | **Dict[str, str]** | | [optional]
**chunk_id** | **str** | | [optional]
## Example
```python
from hindsight_client_api.models.recall_result import RecallResult
# TODO update the JSON string below
json = "{}"
# create an instance of RecallResult from a JSON string
recall_result_instance = RecallResult.from_json(json)
# print the JSON string representation of the object
print(RecallResult.to_json())
# convert the object into a dict
recall_result_dict = recall_result_instance.to_dict()
# create an instance of RecallResult from a dict
recall_result_from_dict = RecallResult.from_dict(recall_result_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -0,0 +1,35 @@
# ReflectFact
A fact used in think response.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**id** | **str** | | [optional]
**text** | **str** | |
**type** | **str** | | [optional]
**context** | **str** | | [optional]
**occurred_start** | **str** | | [optional]
**occurred_end** | **str** | | [optional]
## Example
```python
from hindsight_client_api.models.reflect_fact import ReflectFact
# TODO update the JSON string below
json = "{}"
# create an instance of ReflectFact from a JSON string
reflect_fact_instance = ReflectFact.from_json(json)
# print the JSON string representation of the object
print(ReflectFact.to_json())
# convert the object into a dict
reflect_fact_dict = reflect_fact_instance.to_dict()
# create an instance of ReflectFact from a dict
reflect_fact_from_dict = ReflectFact.from_dict(reflect_fact_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -0,0 +1,30 @@
# ReflectIncludeOptions
Options for including additional data in reflect results.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**facts** | **object** | Options for including facts (based_on) in reflect results. | [optional]
## Example
```python
from hindsight_client_api.models.reflect_include_options import ReflectIncludeOptions
# TODO update the JSON string below
json = "{}"
# create an instance of ReflectIncludeOptions from a JSON string
reflect_include_options_instance = ReflectIncludeOptions.from_json(json)
# print the JSON string representation of the object
print(ReflectIncludeOptions.to_json())
# convert the object into a dict
reflect_include_options_dict = reflect_include_options_instance.to_dict()
# create an instance of ReflectIncludeOptions from a dict
reflect_include_options_from_dict = ReflectIncludeOptions.from_dict(reflect_include_options_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -0,0 +1,33 @@
# ReflectRequest
Request model for reflect endpoint.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**query** | **str** | |
**budget** | [**Budget**](Budget.md) | | [optional]
**context** | **str** | | [optional]
**include** | [**ReflectIncludeOptions**](ReflectIncludeOptions.md) | Options for including additional data (disabled by default) | [optional]
## Example
```python
from hindsight_client_api.models.reflect_request import ReflectRequest
# TODO update the JSON string below
json = "{}"
# create an instance of ReflectRequest from a JSON string
reflect_request_instance = ReflectRequest.from_json(json)
# print the JSON string representation of the object
print(ReflectRequest.to_json())
# convert the object into a dict
reflect_request_dict = reflect_request_instance.to_dict()
# create an instance of ReflectRequest from a dict
reflect_request_from_dict = ReflectRequest.from_dict(reflect_request_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -0,0 +1,31 @@
# ReflectResponse
Response model for think endpoint.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**text** | **str** | |
**based_on** | [**List[ReflectFact]**](ReflectFact.md) | | [optional] [default to []]
## Example
```python
from hindsight_client_api.models.reflect_response import ReflectResponse
# TODO update the JSON string below
json = "{}"
# create an instance of ReflectResponse from a JSON string
reflect_response_instance = ReflectResponse.from_json(json)
# print the JSON string representation of the object
print(ReflectResponse.to_json())
# convert the object into a dict
reflect_response_dict = reflect_response_instance.to_dict()
# create an instance of ReflectResponse from a dict
reflect_response_from_dict = ReflectResponse.from_dict(reflect_response_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -0,0 +1,31 @@
# RetainRequest
Request model for retain endpoint.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**items** | [**List[MemoryItem]**](MemoryItem.md) | |
**var_async** | **bool** | If true, process asynchronously in background. If false, wait for completion (default: false) | [optional] [default to False]
## Example
```python
from hindsight_client_api.models.retain_request import RetainRequest
# TODO update the JSON string below
json = "{}"
# create an instance of RetainRequest from a JSON string
retain_request_instance = RetainRequest.from_json(json)
# print the JSON string representation of the object
print(RetainRequest.to_json())
# convert the object into a dict
retain_request_dict = retain_request_instance.to_dict()
# create an instance of RetainRequest from a dict
retain_request_from_dict = RetainRequest.from_dict(retain_request_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -0,0 +1,33 @@
# RetainResponse
Response model for retain endpoint.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**success** | **bool** | |
**bank_id** | **str** | |
**items_count** | **int** | |
**var_async** | **bool** | Whether the operation was processed asynchronously |
## Example
```python
from hindsight_client_api.models.retain_response import RetainResponse
# TODO update the JSON string below
json = "{}"
# create an instance of RetainResponse from a JSON string
retain_response_instance = RetainResponse.from_json(json)
# print the JSON string representation of the object
print(RetainResponse.to_json())
# convert the object into a dict
retain_response_dict = retain_response_instance.to_dict()
# create an instance of RetainResponse from a dict
retain_response_from_dict = RetainResponse.from_dict(retain_response_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -0,0 +1,30 @@
# UpdateDispositionRequest
Request model for updating disposition traits.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**disposition** | [**DispositionTraits**](DispositionTraits.md) | |
## Example
```python
from hindsight_client_api.models.update_disposition_request import UpdateDispositionRequest
# TODO update the JSON string below
json = "{}"
# create an instance of UpdateDispositionRequest from a JSON string
update_disposition_request_instance = UpdateDispositionRequest.from_json(json)
# print the JSON string representation of the object
print(UpdateDispositionRequest.to_json())
# convert the object into a dict
update_disposition_request_dict = update_disposition_request_instance.to_dict()
# create an instance of UpdateDispositionRequest from a dict
update_disposition_request_from_dict = UpdateDispositionRequest.from_dict(update_disposition_request_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -0,0 +1,31 @@
# ValidationError
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**loc** | [**List[ValidationErrorLocInner]**](ValidationErrorLocInner.md) | |
**msg** | **str** | |
**type** | **str** | |
## Example
```python
from hindsight_client_api.models.validation_error import ValidationError
# TODO update the JSON string below
json = "{}"
# create an instance of ValidationError from a JSON string
validation_error_instance = ValidationError.from_json(json)
# print the JSON string representation of the object
print(ValidationError.to_json())
# convert the object into a dict
validation_error_dict = validation_error_instance.to_dict()
# create an instance of ValidationError from a dict
validation_error_from_dict = ValidationError.from_dict(validation_error_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -0,0 +1,28 @@
# ValidationErrorLocInner
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
## Example
```python
from hindsight_client_api.models.validation_error_loc_inner import ValidationErrorLocInner
# TODO update the JSON string below
json = "{}"
# create an instance of ValidationErrorLocInner from a JSON string
validation_error_loc_inner_instance = ValidationErrorLocInner.from_json(json)
# print the JSON string representation of the object
print(ValidationErrorLocInner.to_json())
# convert the object into a dict
validation_error_loc_inner_dict = validation_error_loc_inner_instance.to_dict()
# create an instance of ValidationErrorLocInner from a dict
validation_error_loc_inner_from_dict = ValidationErrorLocInner.from_dict(validation_error_loc_inner_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
@@ -150,6 +150,13 @@ class ApiException(OpenApiException):
if http_resp.status == 404:
raise NotFoundException(http_resp=http_resp, body=body, data=data)
# Added new conditions for 409 and 422
if http_resp.status == 409:
raise ConflictException(http_resp=http_resp, body=body, data=data)
if http_resp.status == 422:
raise UnprocessableEntityException(http_resp=http_resp, body=body, data=data)
if 500 <= http_resp.status <= 599:
raise ServiceException(http_resp=http_resp, body=body, data=data)
raise ApiException(http_resp=http_resp, body=body, data=data)
@@ -162,8 +169,11 @@ class ApiException(OpenApiException):
error_message += "HTTP response headers: {0}\n".format(
self.headers)
if self.data or self.body:
error_message += "HTTP response body: {0}\n".format(self.data or self.body)
if self.body:
error_message += "HTTP response body: {0}\n".format(self.body)
if self.data:
error_message += "HTTP response data: {0}\n".format(self.data)
return error_message
@@ -188,6 +198,16 @@ class ServiceException(ApiException):
pass
class ConflictException(ApiException):
"""Exception for HTTP 409 Conflict."""
pass
class UnprocessableEntityException(ApiException):
"""Exception for HTTP 422 Unprocessable Entity."""
pass
def render_path(path_to_item):
"""Returns a string representation of a path"""
result = ""
@@ -12,7 +12,6 @@
Do not edit the class manually.
""" # noqa: E501
# import models into model package
from hindsight_client_api.models.add_background_request import AddBackgroundRequest
from hindsight_client_api.models.background_response import BackgroundResponse
@@ -32,7 +31,6 @@ from hindsight_client_api.models.disposition_traits import DispositionTraits
from hindsight_client_api.models.document_response import DocumentResponse
from hindsight_client_api.models.entity_detail_response import EntityDetailResponse
from hindsight_client_api.models.entity_include_options import EntityIncludeOptions
from hindsight_client_api.models.entity_input import EntityInput
from hindsight_client_api.models.entity_list_item import EntityListItem
from hindsight_client_api.models.entity_list_response import EntityListResponse
from hindsight_client_api.models.entity_observation_response import EntityObservationResponse
@@ -57,3 +55,4 @@ from hindsight_client_api.models.retain_response import RetainResponse
from hindsight_client_api.models.update_disposition_request import UpdateDispositionRequest
from hindsight_client_api.models.validation_error import ValidationError
from hindsight_client_api.models.validation_error_loc_inner import ValidationErrorLocInner
@@ -1,94 +0,0 @@
# coding: utf-8
"""
Hindsight HTTP API
HTTP API for Hindsight
The version of the OpenAPI document: 0.1.0
Generated by OpenAPI Generator (https://openapi-generator.tech)
Do not edit the class manually.
""" # noqa: E501
from __future__ import annotations
import pprint
import re # noqa: F401
import json
from pydantic import BaseModel, ConfigDict, Field, StrictStr
from typing import Any, ClassVar, Dict, List, Optional
from typing import Optional, Set
from typing_extensions import Self
class EntityInput(BaseModel):
"""
Entity to associate with retained content.
""" # noqa: E501
text: StrictStr = Field(description="The entity name/text")
type: Optional[StrictStr] = None
__properties: ClassVar[List[str]] = ["text", "type"]
model_config = ConfigDict(
populate_by_name=True,
validate_assignment=True,
protected_namespaces=(),
)
def to_str(self) -> str:
"""Returns the string representation of the model using alias"""
return pprint.pformat(self.model_dump(by_alias=True))
def to_json(self) -> str:
"""Returns the JSON representation of the model using alias"""
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
return json.dumps(self.to_dict())
@classmethod
def from_json(cls, json_str: str) -> Optional[Self]:
"""Create an instance of EntityInput from a JSON string"""
return cls.from_dict(json.loads(json_str))
def to_dict(self) -> Dict[str, Any]:
"""Return the dictionary representation of the model using alias.
This has the following differences from calling pydantic's
`self.model_dump(by_alias=True)`:
* `None` is only added to the output dict for nullable fields that
were set at model initialization. Other fields with value `None`
are ignored.
"""
excluded_fields: Set[str] = set([
])
_dict = self.model_dump(
by_alias=True,
exclude=excluded_fields,
exclude_none=True,
)
# set to None if type (nullable) is None
# and model_fields_set contains the field
if self.type is None and "type" in self.model_fields_set:
_dict['type'] = None
return _dict
@classmethod
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
"""Create an instance of EntityInput from a dict"""
if obj is None:
return None
if not isinstance(obj, dict):
return cls.model_validate(obj)
_obj = cls.model_validate({
"text": obj.get("text"),
"type": obj.get("type")
})
return _obj

Some files were not shown because too many files have changed in this diff Show More