Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a6942fb06 | ||
|
|
b1f8627eed | ||
|
|
8573cc3292 | ||
|
|
eb725002c0 | ||
|
|
2de75062eb |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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/
|
||||
|
||||
@@ -1,155 +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`
|
||||
|
||||
## Key Conventions
|
||||
|
||||
### Code Quality
|
||||
**Always run the lint script after making Python or TypeScript/Node changes:**
|
||||
```bash
|
||||
./scripts/hooks/lint.sh
|
||||
```
|
||||
This runs the same checks as the pre-commit hook (Ruff for Python, ESLint/Prettier for TypeScript).
|
||||
|
||||
### 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
|
||||
|
||||
### Adding New API Configuration Flags
|
||||
|
||||
When adding a new environment variable configuration:
|
||||
|
||||
1. **config.py** (`hindsight-api/hindsight_api/config.py`):
|
||||
- Add `ENV_*` constant for the environment variable name
|
||||
- Add `DEFAULT_*` constant for the default value
|
||||
- Add field to `HindsightConfig` dataclass
|
||||
- Add initialization in `from_env()` method
|
||||
|
||||
2. **main.py** (`hindsight-api/hindsight_api/main.py`):
|
||||
- Add field to the manual `HindsightConfig()` constructor call (search for "CLI override")
|
||||
|
||||
3. **Use the config** in code:
|
||||
```python
|
||||
from ...config import get_config
|
||||
config = get_config()
|
||||
value = config.your_new_field
|
||||
```
|
||||
|
||||
4. **Documentation** (`hindsight-docs/docs/developer/configuration.md`):
|
||||
- Add to appropriate section table with Variable, Description, Default
|
||||
|
||||
## 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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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` |
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
# Admin CLI for Hindsight
|
||||
@@ -1,222 +0,0 @@
|
||||
"""
|
||||
Hindsight Admin CLI - backup and restore operations.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import zipfile
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import asyncpg
|
||||
import typer
|
||||
|
||||
from ..config import HindsightConfig
|
||||
from ..pg0 import parse_pg0_url, resolve_database_url
|
||||
|
||||
|
||||
def _fq_table(table: str, schema: str) -> str:
|
||||
"""Get fully-qualified table name with schema prefix."""
|
||||
return f"{schema}.{table}"
|
||||
|
||||
|
||||
# Setup logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(message)s",
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app = typer.Typer(name="hindsight-admin", help="Hindsight administrative commands")
|
||||
|
||||
# Tables to backup/restore in dependency order
|
||||
# Import must happen in this order due to foreign key constraints
|
||||
BACKUP_TABLES = [
|
||||
"banks",
|
||||
"documents",
|
||||
"entities",
|
||||
"chunks",
|
||||
"memory_units",
|
||||
"unit_entities",
|
||||
"entity_cooccurrences",
|
||||
"memory_links",
|
||||
]
|
||||
|
||||
MANIFEST_VERSION = "1"
|
||||
|
||||
|
||||
async def _backup(database_url: str, output_path: Path, schema: str = "public") -> dict[str, Any]:
|
||||
"""Backup all tables to a zip file using binary COPY protocol."""
|
||||
conn = await asyncpg.connect(database_url)
|
||||
try:
|
||||
tables: dict[str, Any] = {}
|
||||
manifest: dict[str, Any] = {
|
||||
"version": MANIFEST_VERSION,
|
||||
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||
"schema": schema,
|
||||
"tables": tables,
|
||||
}
|
||||
|
||||
# Use a transaction with REPEATABLE READ isolation to get a consistent
|
||||
# snapshot across all tables. This prevents race conditions where
|
||||
# entity_cooccurrences could reference entities created after the
|
||||
# entities table was backed up.
|
||||
async with conn.transaction(isolation="repeatable_read"):
|
||||
with zipfile.ZipFile(output_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||
for i, table in enumerate(BACKUP_TABLES, 1):
|
||||
typer.echo(f" [{i}/{len(BACKUP_TABLES)}] Backing up {table}...", nl=False)
|
||||
|
||||
buffer = io.BytesIO()
|
||||
|
||||
# Use binary COPY for exact type preservation
|
||||
# asyncpg requires schema_name as separate parameter
|
||||
await conn.copy_from_table(table, schema_name=schema, output=buffer, format="binary")
|
||||
|
||||
data = buffer.getvalue()
|
||||
zf.writestr(f"{table}.bin", data)
|
||||
|
||||
# Get row count for manifest
|
||||
qualified_table = _fq_table(table, schema)
|
||||
row_count = await conn.fetchval(f"SELECT COUNT(*) FROM {qualified_table}")
|
||||
tables[table] = {
|
||||
"rows": row_count,
|
||||
"size_bytes": len(data),
|
||||
}
|
||||
|
||||
typer.echo(f" {row_count} rows")
|
||||
|
||||
zf.writestr("manifest.json", json.dumps(manifest, indent=2))
|
||||
|
||||
return manifest
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
|
||||
async def _restore(database_url: str, input_path: Path, schema: str = "public") -> dict[str, Any]:
|
||||
"""Restore all tables from a zip file using binary COPY protocol."""
|
||||
conn = await asyncpg.connect(database_url)
|
||||
try:
|
||||
with zipfile.ZipFile(input_path, "r") as zf:
|
||||
# Read and validate manifest
|
||||
manifest: dict[str, Any] = json.loads(zf.read("manifest.json"))
|
||||
if manifest.get("version") != MANIFEST_VERSION:
|
||||
raise ValueError(f"Unsupported backup version: {manifest.get('version')}")
|
||||
|
||||
# Use a transaction for atomic restore - either all tables are
|
||||
# restored or none are, preventing partial/inconsistent state.
|
||||
async with conn.transaction():
|
||||
typer.echo(" Clearing existing data...")
|
||||
# Truncate tables in reverse order (respects FK constraints)
|
||||
for table in reversed(BACKUP_TABLES):
|
||||
qualified_table = _fq_table(table, schema)
|
||||
await conn.execute(f"TRUNCATE TABLE {qualified_table} CASCADE")
|
||||
|
||||
# Restore tables in forward order
|
||||
for i, table in enumerate(BACKUP_TABLES, 1):
|
||||
filename = f"{table}.bin"
|
||||
if filename not in zf.namelist():
|
||||
typer.echo(f" [{i}/{len(BACKUP_TABLES)}] {table}: skipped (not in backup)")
|
||||
continue
|
||||
|
||||
expected_rows = manifest["tables"].get(table, {}).get("rows", "?")
|
||||
typer.echo(f" [{i}/{len(BACKUP_TABLES)}] Restoring {table}... {expected_rows} rows")
|
||||
|
||||
data = zf.read(filename)
|
||||
buffer = io.BytesIO(data)
|
||||
# asyncpg requires schema_name as separate parameter
|
||||
await conn.copy_to_table(table, schema_name=schema, source=buffer, format="binary")
|
||||
|
||||
# Refresh materialized view
|
||||
typer.echo(" Refreshing materialized views...")
|
||||
await conn.execute(f"REFRESH MATERIALIZED VIEW {_fq_table('memory_units_bm25', schema)}")
|
||||
|
||||
return manifest
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
|
||||
async def _run_backup(db_url: str, output: Path, schema: str = "public") -> dict[str, Any]:
|
||||
"""Resolve database URL and run backup."""
|
||||
is_pg0, instance_name, _ = parse_pg0_url(db_url)
|
||||
if is_pg0:
|
||||
typer.echo(f"Starting embedded PostgreSQL (instance: {instance_name})...")
|
||||
resolved_url = await resolve_database_url(db_url)
|
||||
return await _backup(resolved_url, output, schema)
|
||||
|
||||
|
||||
async def _run_restore(db_url: str, input_file: Path, schema: str = "public") -> dict[str, Any]:
|
||||
"""Resolve database URL and run restore."""
|
||||
is_pg0, instance_name, _ = parse_pg0_url(db_url)
|
||||
if is_pg0:
|
||||
typer.echo(f"Starting embedded PostgreSQL (instance: {instance_name})...")
|
||||
resolved_url = await resolve_database_url(db_url)
|
||||
return await _restore(resolved_url, input_file, schema)
|
||||
|
||||
|
||||
@app.command()
|
||||
def backup(
|
||||
output: Path = typer.Argument(..., help="Output file path (.zip)"),
|
||||
schema: str = typer.Option("public", "--schema", "-s", help="Database schema to backup"),
|
||||
):
|
||||
"""Backup the Hindsight database to a zip file."""
|
||||
config = HindsightConfig.from_env()
|
||||
|
||||
if not config.database_url:
|
||||
typer.echo("Error: Database URL not configured.", err=True)
|
||||
typer.echo("Set HINDSIGHT_API_DATABASE_URL environment variable.", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
if output.suffix != ".zip":
|
||||
output = output.with_suffix(".zip")
|
||||
|
||||
typer.echo(f"Backing up database (schema: {schema}) to {output}...")
|
||||
|
||||
manifest = asyncio.run(_run_backup(config.database_url, output, schema))
|
||||
|
||||
total_rows = sum(t["rows"] for t in manifest["tables"].values())
|
||||
typer.echo(f"Backed up {total_rows} rows across {len(BACKUP_TABLES)} tables")
|
||||
typer.echo(f"Backup saved to {output}")
|
||||
|
||||
|
||||
@app.command()
|
||||
def restore(
|
||||
input_file: Path = typer.Argument(..., help="Input backup file (.zip)"),
|
||||
schema: str = typer.Option("public", "--schema", "-s", help="Database schema to restore to"),
|
||||
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
|
||||
):
|
||||
"""Restore the database from a backup file. WARNING: This deletes all existing data."""
|
||||
config = HindsightConfig.from_env()
|
||||
|
||||
if not config.database_url:
|
||||
typer.echo("Error: Database URL not configured.", err=True)
|
||||
typer.echo("Set HINDSIGHT_API_DATABASE_URL environment variable.", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
if not input_file.exists():
|
||||
typer.echo(f"Error: File not found: {input_file}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
if not yes:
|
||||
typer.confirm(
|
||||
"This will DELETE all existing data and replace it with the backup. Continue?",
|
||||
abort=True,
|
||||
)
|
||||
|
||||
typer.echo(f"Restoring database (schema: {schema}) from {input_file}...")
|
||||
|
||||
manifest = asyncio.run(_run_restore(config.database_url, input_file, schema))
|
||||
|
||||
total_rows = sum(t["rows"] for t in manifest["tables"].values())
|
||||
typer.echo(f"Restored {total_rows} rows across {len(BACKUP_TABLES)} tables")
|
||||
typer.echo("Restore complete")
|
||||
|
||||
|
||||
def main():
|
||||
app()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,11 +8,6 @@ import logging
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
|
||||
from dotenv import find_dotenv, load_dotenv
|
||||
|
||||
# Load .env file, searching current and parent directories (overrides existing env vars)
|
||||
load_dotenv(find_dotenv(usecwd=True), override=True)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Environment variable names
|
||||
@@ -21,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"
|
||||
@@ -43,13 +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"
|
||||
|
||||
# Retain settings
|
||||
ENV_RETAIN_MAX_COMPLETION_TOKENS = "HINDSIGHT_API_RETAIN_MAX_COMPLETION_TOKENS"
|
||||
|
||||
# Optimization flags
|
||||
ENV_SKIP_LLM_VERIFICATION = "HINDSIGHT_API_SKIP_LLM_VERIFICATION"
|
||||
ENV_LAZY_RERANKER = "HINDSIGHT_API_LAZY_RERANKER"
|
||||
@@ -58,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"
|
||||
@@ -76,13 +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
|
||||
|
||||
# Retain settings
|
||||
DEFAULT_RETAIN_MAX_COMPLETION_TOKENS = 64000 # Max tokens for fact extraction LLM call
|
||||
|
||||
# Default MCP tool descriptions (can be customized via env vars)
|
||||
DEFAULT_MCP_RETAIN_DESCRIPTION = """Store important information to long-term memory.
|
||||
|
||||
@@ -103,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
|
||||
@@ -119,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
|
||||
@@ -141,13 +111,6 @@ class HindsightConfig:
|
||||
# Recall
|
||||
graph_retriever: str
|
||||
|
||||
# Observation thresholds
|
||||
observation_min_facts: int
|
||||
observation_top_entities: int
|
||||
|
||||
# Retain settings
|
||||
retain_max_completion_tokens: int
|
||||
|
||||
# Optimization flags
|
||||
skip_llm_verification: bool
|
||||
lazy_reranker: bool
|
||||
@@ -163,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),
|
||||
@@ -183,15 +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))
|
||||
),
|
||||
# Retain settings
|
||||
retain_max_completion_tokens=int(
|
||||
os.getenv(ENV_RETAIN_MAX_COMPLETION_TOKENS, str(DEFAULT_RETAIN_MAX_COMPLETION_TOKENS))
|
||||
),
|
||||
)
|
||||
|
||||
def get_llm_base_url(self) -> str:
|
||||
@@ -204,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 ""
|
||||
|
||||
@@ -238,19 +188,6 @@ class HindsightConfig:
|
||||
logger.info(f"Graph retriever: {self.graph_retriever}")
|
||||
|
||||
|
||||
# Cached config instance
|
||||
_config_cache: HindsightConfig | None = None
|
||||
|
||||
|
||||
def get_config() -> HindsightConfig:
|
||||
"""Get the cached configuration, loading from environment on first call."""
|
||||
global _config_cache
|
||||
if _config_cache is None:
|
||||
_config_cache = HindsightConfig.from_env()
|
||||
return _config_cache
|
||||
|
||||
|
||||
def clear_config_cache() -> None:
|
||||
"""Clear the config cache. Useful for testing or reloading config."""
|
||||
global _config_cache
|
||||
_config_cache = None
|
||||
"""Get the current configuration from environment variables."""
|
||||
return HindsightConfig.from_env()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -132,7 +130,7 @@ if TYPE_CHECKING:
|
||||
|
||||
from enum import Enum
|
||||
|
||||
from ..pg0 import EmbeddedPostgres, parse_pg0_url
|
||||
from ..pg0 import EmbeddedPostgres
|
||||
from .entity_resolver import EntityResolver
|
||||
from .llm_wrapper import LLMConfig
|
||||
from .query_analyzer import QueryAnalyzer
|
||||
@@ -259,14 +257,31 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
memory_llm_base_url = memory_llm_base_url or config.get_llm_base_url() or None
|
||||
# Track pg0 instance (if used)
|
||||
self._pg0: EmbeddedPostgres | None = None
|
||||
self._pg0_instance_name: str | None = None
|
||||
|
||||
# Initialize PostgreSQL connection URL
|
||||
# The actual URL will be set during initialize() after starting the server
|
||||
# Supports: "pg0" (default instance), "pg0://instance-name" (named instance), or regular postgresql:// URL
|
||||
self._use_pg0, self._pg0_instance_name, self._pg0_port = parse_pg0_url(db_url)
|
||||
if self._use_pg0:
|
||||
if db_url == "pg0":
|
||||
self._use_pg0 = True
|
||||
self._pg0_instance_name = "hindsight"
|
||||
self._pg0_port = None # Use default port
|
||||
self.db_url = None
|
||||
elif db_url.startswith("pg0://"):
|
||||
self._use_pg0 = True
|
||||
# Parse instance name and optional port: pg0://instance-name or pg0://instance-name:port
|
||||
url_part = db_url[6:] # Remove "pg0://"
|
||||
if ":" in url_part:
|
||||
self._pg0_instance_name, port_str = url_part.rsplit(":", 1)
|
||||
self._pg0_port = int(port_str)
|
||||
else:
|
||||
self._pg0_instance_name = url_part or "hindsight"
|
||||
self._pg0_port = None # Use default port
|
||||
self.db_url = None
|
||||
else:
|
||||
self._use_pg0 = False
|
||||
self._pg0_instance_name = None
|
||||
self._pg0_port = None
|
||||
self.db_url = db_url
|
||||
|
||||
# Set default base URL if not provided
|
||||
@@ -357,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:
|
||||
"""
|
||||
@@ -384,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
|
||||
|
||||
@@ -625,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
|
||||
@@ -2816,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]):
|
||||
"""
|
||||
@@ -3070,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:
|
||||
"""
|
||||
@@ -3083,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:
|
||||
@@ -3176,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
|
||||
@@ -3236,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
|
||||
@@ -3260,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.
|
||||
|
||||
@@ -3272,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
|
||||
@@ -3283,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,
|
||||
@@ -3608,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:
|
||||
@@ -3620,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(
|
||||
|
||||
@@ -14,48 +14,9 @@ from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
from ...config import get_config
|
||||
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.
|
||||
@@ -110,7 +71,7 @@ class Fact(BaseModel):
|
||||
|
||||
|
||||
class CausalRelation(BaseModel):
|
||||
"""Causal relationship between facts (legacy - embedded in each fact)."""
|
||||
"""Causal relationship between facts."""
|
||||
|
||||
target_fact_index: int = Field(
|
||||
description="Index of the related fact in the facts array (0-based). "
|
||||
@@ -132,36 +93,6 @@ class CausalRelation(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
class TopLevelCausalRelation(BaseModel):
|
||||
"""
|
||||
Causal relationship between two facts (top-level schema).
|
||||
|
||||
This is the preferred format - defined AFTER all facts are extracted,
|
||||
allowing the LLM to see the full list of facts before specifying relationships.
|
||||
"""
|
||||
|
||||
from_fact_index: int = Field(
|
||||
description="Index of the source fact (0-based). The fact that causes/enables/prevents."
|
||||
)
|
||||
to_fact_index: int = Field(
|
||||
description="Index of the target fact (0-based). The fact that is caused/enabled/prevented."
|
||||
)
|
||||
relation_type: Literal["causes", "caused_by", "enables", "prevents"] = Field(
|
||||
description="Type of causal relationship: "
|
||||
"'causes' = source fact directly causes the target fact, "
|
||||
"'caused_by' = source fact was caused by the target fact, "
|
||||
"'enables' = source fact enables/allows the target fact, "
|
||||
"'prevents' = source fact prevents/blocks the target fact"
|
||||
)
|
||||
strength: float = Field(
|
||||
description="Strength of causal relationship (0.0 to 1.0). "
|
||||
"1.0 = direct/strong causation, 0.5 = moderate, 0.3 = weak/indirect",
|
||||
ge=0.0,
|
||||
le=1.0,
|
||||
default=1.0,
|
||||
)
|
||||
|
||||
|
||||
class ExtractedFact(BaseModel):
|
||||
"""A single extracted fact with 5 required dimensions for comprehensive capture."""
|
||||
|
||||
@@ -284,15 +215,9 @@ class ExtractedFact(BaseModel):
|
||||
|
||||
|
||||
class FactExtractionResponse(BaseModel):
|
||||
"""Response containing all extracted facts and their causal relationships."""
|
||||
"""Response containing all extracted facts."""
|
||||
|
||||
facts: list[ExtractedFact] = Field(description="List of extracted factual statements")
|
||||
causal_relationships: list[TopLevelCausalRelation] | None = Field(
|
||||
default=None,
|
||||
description="Causal relationships between facts. Define these AFTER listing all facts. "
|
||||
"Each relationship specifies from_fact_index -> to_fact_index with a relation type. "
|
||||
"Indices must be valid (0 to N-1 where N is the number of facts).",
|
||||
)
|
||||
|
||||
|
||||
def chunk_text(text: str, max_chars: int) -> list[str]:
|
||||
@@ -609,53 +534,7 @@ WHAT TO EXTRACT vs SKIP
|
||||
══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
✅ EXTRACT: User preferences (ALWAYS as separate facts!), feelings, plans, events, relationships, achievements
|
||||
❌ SKIP: Greetings, filler ("thanks", "cool"), purely structural statements
|
||||
|
||||
══════════════════════════════════════════════════════════════════════════
|
||||
CAUSAL RELATIONSHIPS (CRITICAL - DEFINE AFTER ALL FACTS)
|
||||
══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
⚠️ IMPORTANT: Causal relationships are defined at the TOP LEVEL, AFTER listing all facts!
|
||||
|
||||
The `causal_relationships` array goes at the root of your response (NOT inside each fact).
|
||||
This allows you to see all facts first before defining how they relate.
|
||||
|
||||
Format:
|
||||
```json
|
||||
{{
|
||||
"facts": [...all your extracted facts...],
|
||||
"causal_relationships": [
|
||||
{{"from_fact_index": 0, "to_fact_index": 1, "relation_type": "causes", "strength": 0.9}},
|
||||
{{"from_fact_index": 1, "to_fact_index": 2, "relation_type": "enables", "strength": 0.7}}
|
||||
]
|
||||
}}
|
||||
```
|
||||
|
||||
Relationship types:
|
||||
- "causes": Fact A directly causes Fact B (A → B)
|
||||
- "caused_by": Fact A was caused by Fact B (A ← B)
|
||||
- "enables": Fact A enables/allows Fact B to happen
|
||||
- "prevents": Fact A prevents/blocks Fact B from happening
|
||||
|
||||
⚠️ INDEX VALIDATION: If you extract N facts (indices 0 to N-1), both from_fact_index and to_fact_index MUST be in range [0, N-1].
|
||||
|
||||
Example (Event Date: March 15, 2024):
|
||||
Input: "I lost my job in January. Because of that, I couldn't pay rent. So I had to move to a cheaper apartment."
|
||||
|
||||
Facts extracted:
|
||||
- Fact 0: "User lost their job in January due to layoffs"
|
||||
- Fact 1: "User couldn't pay rent because of job loss"
|
||||
- Fact 2: "User moved to a cheaper apartment"
|
||||
|
||||
Causal relationships (at root level):
|
||||
```json
|
||||
"causal_relationships": [
|
||||
{{"from_fact_index": 0, "to_fact_index": 1, "relation_type": "causes", "strength": 1.0}},
|
||||
{{"from_fact_index": 1, "to_fact_index": 2, "relation_type": "causes", "strength": 0.9}}
|
||||
]
|
||||
```
|
||||
|
||||
This creates a chain: Job loss (0) → Can't pay rent (1) → Moved to cheaper apartment (2)"""
|
||||
❌ SKIP: Greetings, filler ("thanks", "cool"), purely structural statements"""
|
||||
|
||||
import logging
|
||||
|
||||
@@ -666,7 +545,6 @@ This creates a chain: Job loss (0) → Can't pay rent (1) → Moved to cheaper a
|
||||
# Retry logic for JSON validation errors
|
||||
max_retries = 2
|
||||
last_error = None
|
||||
config = get_config()
|
||||
|
||||
# Sanitize input text to prevent Unicode encoding errors (e.g., unpaired surrogates)
|
||||
sanitized_chunk = _sanitize_text(chunk)
|
||||
@@ -692,7 +570,7 @@ Text:
|
||||
response_format=FactExtractionResponse,
|
||||
scope="memory_extract_facts",
|
||||
temperature=0.1,
|
||||
max_completion_tokens=config.retain_max_completion_tokens,
|
||||
max_completion_tokens=65000,
|
||||
skip_validation=True, # Get raw JSON, we'll validate leniently
|
||||
)
|
||||
|
||||
@@ -715,9 +593,6 @@ Text:
|
||||
return []
|
||||
|
||||
raw_facts = extraction_response_json.get("facts", [])
|
||||
# Get top-level causal relationships (new schema)
|
||||
top_level_causal_relations = extraction_response_json.get("causal_relationships", [])
|
||||
|
||||
if not raw_facts:
|
||||
logger.debug(
|
||||
f"LLM response missing 'facts' field or returned empty list. "
|
||||
@@ -728,47 +603,6 @@ Text:
|
||||
f"text: {chunk}"
|
||||
)
|
||||
|
||||
# Build a map from fact index to causal relations (from top-level field)
|
||||
# This converts from_fact_index -> [{target_fact_index, relation_type, strength}]
|
||||
causal_relations_by_fact: dict[int, list[dict]] = {}
|
||||
if top_level_causal_relations:
|
||||
num_facts = len(raw_facts)
|
||||
for rel in top_level_causal_relations:
|
||||
if not isinstance(rel, dict):
|
||||
continue
|
||||
from_idx = rel.get("from_fact_index")
|
||||
to_idx = rel.get("to_fact_index")
|
||||
relation_type = rel.get("relation_type")
|
||||
strength = rel.get("strength", 1.0)
|
||||
|
||||
# Validate indices
|
||||
if from_idx is None or to_idx is None or relation_type is None:
|
||||
logger.warning(f"Skipping malformed top-level causal relation: {rel}")
|
||||
continue
|
||||
if from_idx < 0 or from_idx >= num_facts:
|
||||
logger.warning(
|
||||
f"Invalid from_fact_index {from_idx} in top-level causal relation "
|
||||
f"(valid range: 0-{num_facts - 1}). Skipping."
|
||||
)
|
||||
continue
|
||||
if to_idx < 0 or to_idx >= num_facts:
|
||||
logger.warning(
|
||||
f"Invalid to_fact_index {to_idx} in top-level causal relation "
|
||||
f"(valid range: 0-{num_facts - 1}). Skipping."
|
||||
)
|
||||
continue
|
||||
|
||||
# Add to the map for the from_fact_index
|
||||
if from_idx not in causal_relations_by_fact:
|
||||
causal_relations_by_fact[from_idx] = []
|
||||
causal_relations_by_fact[from_idx].append(
|
||||
{
|
||||
"target_fact_index": to_idx,
|
||||
"relation_type": relation_type,
|
||||
"strength": strength,
|
||||
}
|
||||
)
|
||||
|
||||
for i, llm_fact in enumerate(raw_facts):
|
||||
# Skip non-dict entries but track them for retry
|
||||
if not isinstance(llm_fact, dict):
|
||||
@@ -842,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
|
||||
@@ -873,40 +702,19 @@ Text:
|
||||
if validated_entities:
|
||||
fact_data["entities"] = validated_entities
|
||||
|
||||
# Add causal relations from both sources:
|
||||
# 1. Top-level causal_relationships (preferred, new schema)
|
||||
# 2. Per-fact causal_relations (legacy, for backward compatibility)
|
||||
validated_relations = []
|
||||
|
||||
# First, add relations from top-level (already validated above)
|
||||
if i in causal_relations_by_fact:
|
||||
for rel in causal_relations_by_fact[i]:
|
||||
try:
|
||||
validated_relations.append(CausalRelation.model_validate(rel))
|
||||
except Exception as e:
|
||||
logger.warning(f"Invalid top-level causal relation for fact {i}: {rel}: {e}")
|
||||
|
||||
# Then, add any legacy per-fact relations (with index validation)
|
||||
legacy_causal_relations = get_value("causal_relations")
|
||||
if legacy_causal_relations:
|
||||
num_facts = len(raw_facts)
|
||||
for rel in legacy_causal_relations:
|
||||
# Add causal relations if present (validate as CausalRelation objects)
|
||||
# Filter out invalid relations (missing required fields)
|
||||
causal_relations = get_value("causal_relations")
|
||||
if causal_relations:
|
||||
validated_relations = []
|
||||
for rel in causal_relations:
|
||||
if isinstance(rel, dict) and "target_fact_index" in rel and "relation_type" in rel:
|
||||
target_idx = rel.get("target_fact_index")
|
||||
# Validate target index for legacy format too
|
||||
if target_idx is not None and 0 <= target_idx < num_facts:
|
||||
try:
|
||||
validated_relations.append(CausalRelation.model_validate(rel))
|
||||
except Exception as e:
|
||||
logger.warning(f"Invalid causal relation {rel}: {e}")
|
||||
else:
|
||||
logger.warning(
|
||||
f"Invalid target_fact_index {target_idx} in per-fact causal relation "
|
||||
f"from fact {i} (valid range: 0-{num_facts - 1}). Skipping."
|
||||
)
|
||||
|
||||
if validated_relations:
|
||||
fact_data["causal_relations"] = validated_relations
|
||||
try:
|
||||
validated_relations.append(CausalRelation.model_validate(rel))
|
||||
except Exception as e:
|
||||
logger.warning(f"Invalid causal relation {rel}: {e}")
|
||||
if validated_relations:
|
||||
fact_data["causal_relations"] = validated_relations
|
||||
|
||||
# Always set mentioned_at to the event_date (when the conversation/document occurred)
|
||||
fact_data["mentioned_at"] = event_date.isoformat()
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
|
||||
@@ -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,9 +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,
|
||||
retain_max_completion_tokens=config.retain_max_completion_tokens,
|
||||
skip_llm_verification=config.skip_llm_verification,
|
||||
lazy_reranker=config.lazy_reranker,
|
||||
)
|
||||
@@ -197,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(
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -132,56 +132,3 @@ async def stop_embedded_postgres() -> None:
|
||||
global _default_instance
|
||||
if _default_instance:
|
||||
await _default_instance.stop()
|
||||
|
||||
|
||||
def parse_pg0_url(db_url: str) -> tuple[bool, str | None, int | None]:
|
||||
"""
|
||||
Parse a database URL and check if it's a pg0:// embedded database URL.
|
||||
|
||||
Supports:
|
||||
- "pg0" -> default instance "hindsight"
|
||||
- "pg0://instance-name" -> named instance
|
||||
- "pg0://instance-name:port" -> named instance with explicit port
|
||||
- Any other URL (e.g., postgresql://) -> not a pg0 URL
|
||||
|
||||
Args:
|
||||
db_url: The database URL to parse
|
||||
|
||||
Returns:
|
||||
Tuple of (is_pg0, instance_name, port)
|
||||
- is_pg0: True if this is a pg0 URL
|
||||
- instance_name: The instance name (or None if not pg0)
|
||||
- port: The explicit port (or None for auto-assign)
|
||||
"""
|
||||
if db_url == "pg0":
|
||||
return True, "hindsight", None
|
||||
|
||||
if db_url.startswith("pg0://"):
|
||||
url_part = db_url[6:] # Remove "pg0://"
|
||||
if ":" in url_part:
|
||||
instance_name, port_str = url_part.rsplit(":", 1)
|
||||
return True, instance_name or "hindsight", int(port_str)
|
||||
else:
|
||||
return True, url_part or "hindsight", None
|
||||
|
||||
return False, None, None
|
||||
|
||||
|
||||
async def resolve_database_url(db_url: str) -> str:
|
||||
"""
|
||||
Resolve a database URL, handling pg0:// embedded database URLs.
|
||||
|
||||
If the URL is a pg0:// URL, starts the embedded PostgreSQL and returns
|
||||
the actual postgresql:// connection URL. Otherwise, returns the URL unchanged.
|
||||
|
||||
Args:
|
||||
db_url: Database URL (pg0://, pg0, or postgresql://)
|
||||
|
||||
Returns:
|
||||
The resolved postgresql:// connection URL
|
||||
"""
|
||||
is_pg0, instance_name, port = parse_pg0_url(db_url)
|
||||
if is_pg0:
|
||||
pg0 = EmbeddedPostgres(name=instance_name, port=port)
|
||||
return await pg0.ensure_running()
|
||||
return db_url
|
||||
|
||||
@@ -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,8 +37,6 @@ dependencies = [
|
||||
"opentelemetry-exporter-prometheus>=0.41b0",
|
||||
"dateparser>=1.2.2",
|
||||
"google-genai>=1.0.0",
|
||||
"anthropic>=0.40.0",
|
||||
"typer>=0.9.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
@@ -53,7 +51,6 @@ test = [
|
||||
[project.scripts]
|
||||
hindsight-api = "hindsight_api.main:main"
|
||||
hindsight-local-mcp = "hindsight_api.mcp_local:main"
|
||||
hindsight-admin = "hindsight_api.admin.cli:main"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["hindsight_api"]
|
||||
@@ -77,7 +74,7 @@ log_cli = true
|
||||
log_cli_level = "INFO"
|
||||
log_cli_format = "%(asctime)s - %(levelname)s - %(name)s - %(message)s"
|
||||
log_cli_date_format = "%Y-%m-%d %H:%M:%S"
|
||||
addopts = "--timeout 120 -n 8 --dist loadgroup --durations=10 -v"
|
||||
addopts = "--timeout 120 -n 8 --durations=10 -v"
|
||||
asyncio_mode = "auto"
|
||||
asyncio_default_fixture_loop_scope = "function"
|
||||
log_auto_indent = true
|
||||
|
||||
@@ -1,208 +0,0 @@
|
||||
"""
|
||||
Tests for admin backup and restore functionality.
|
||||
|
||||
Note: These tests run sequentially (not in parallel) because they all
|
||||
manipulate the same database and do full backup/restore operations.
|
||||
"""
|
||||
|
||||
import tempfile
|
||||
import uuid
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from hindsight_api import RequestContext
|
||||
from hindsight_api.admin.cli import _backup, _restore, BACKUP_TABLES
|
||||
|
||||
|
||||
# Run these tests sequentially since they do full DB backup/restore
|
||||
pytestmark = pytest.mark.xdist_group(name="backup_restore")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_backup_restore_roundtrip(memory, pg0_db_url, request_context):
|
||||
"""Test that backup and restore preserves all data correctly."""
|
||||
# Use unique bank ID to avoid conflicts
|
||||
bank_id = f"test-backup-{uuid.uuid4().hex[:8]}"
|
||||
|
||||
# Create some test data
|
||||
await memory.retain_batch_async(
|
||||
bank_id=bank_id,
|
||||
contents=[
|
||||
{"content": "Alice is a software engineer who loves Python."},
|
||||
{"content": "Bob works with Alice on the backend team."},
|
||||
{"content": "The team uses PostgreSQL for their database."},
|
||||
],
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
# Get counts before backup
|
||||
async with memory._pool.acquire() as conn:
|
||||
counts_before = {}
|
||||
for table in BACKUP_TABLES:
|
||||
counts_before[table] = await conn.fetchval(f"SELECT COUNT(*) FROM {table}")
|
||||
|
||||
# Verify we have data
|
||||
assert counts_before["banks"] > 0
|
||||
assert counts_before["memory_units"] > 0
|
||||
|
||||
# Backup to a temp file
|
||||
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as f:
|
||||
backup_path = Path(f.name)
|
||||
|
||||
try:
|
||||
manifest = await _backup(pg0_db_url, backup_path)
|
||||
|
||||
# Verify backup file exists and is valid
|
||||
assert backup_path.exists()
|
||||
assert backup_path.stat().st_size > 0
|
||||
|
||||
# Verify manifest
|
||||
assert manifest["version"] == "1"
|
||||
assert "created_at" in manifest
|
||||
for table in BACKUP_TABLES:
|
||||
assert table in manifest["tables"]
|
||||
assert manifest["tables"][table]["rows"] == counts_before[table]
|
||||
|
||||
# Verify zip contents
|
||||
with zipfile.ZipFile(backup_path, "r") as zf:
|
||||
assert "manifest.json" in zf.namelist()
|
||||
for table in BACKUP_TABLES:
|
||||
assert f"{table}.bin" in zf.namelist()
|
||||
|
||||
# Clear all data
|
||||
async with memory._pool.acquire() as conn:
|
||||
for table in reversed(BACKUP_TABLES):
|
||||
await conn.execute(f"TRUNCATE TABLE {table} CASCADE")
|
||||
|
||||
# Verify data is gone
|
||||
async with memory._pool.acquire() as conn:
|
||||
for table in BACKUP_TABLES:
|
||||
count = await conn.fetchval(f"SELECT COUNT(*) FROM {table}")
|
||||
assert count == 0, f"Table {table} should be empty after truncate"
|
||||
|
||||
# Restore from backup
|
||||
await _restore(pg0_db_url, backup_path)
|
||||
|
||||
# Verify counts match original
|
||||
async with memory._pool.acquire() as conn:
|
||||
for table in BACKUP_TABLES:
|
||||
count = await conn.fetchval(f"SELECT COUNT(*) FROM {table}")
|
||||
assert count == counts_before[table], f"Table {table} count mismatch after restore"
|
||||
|
||||
# Verify data content is preserved
|
||||
async with memory._pool.acquire() as conn:
|
||||
texts = await conn.fetch(
|
||||
"SELECT text FROM memory_units WHERE bank_id = $1",
|
||||
bank_id,
|
||||
)
|
||||
text_content = " ".join(r["text"] for r in texts)
|
||||
assert "Alice" in text_content or "software" in text_content
|
||||
|
||||
finally:
|
||||
# Cleanup
|
||||
if backup_path.exists():
|
||||
backup_path.unlink()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_backup_restore_preserves_all_column_types(memory, pg0_db_url, request_context):
|
||||
"""Test that all column types are preserved: vectors, UUIDs, timestamps, JSONB."""
|
||||
# Use unique bank ID
|
||||
bank_id = f"test-types-{uuid.uuid4().hex[:8]}"
|
||||
|
||||
# Create data with meaningful content that will produce facts
|
||||
await memory.retain_batch_async(
|
||||
bank_id=bank_id,
|
||||
contents=[
|
||||
{"content": "John Smith is a senior engineer at Acme Corp since 2020."},
|
||||
{"content": "The project deadline is December 15th 2024."},
|
||||
],
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
# Get original data with all important column types
|
||||
async with memory._pool.acquire() as conn:
|
||||
# memory_units: UUID (id), Vector (embedding), Timestamp (event_date, created_at), JSONB (metadata)
|
||||
original_unit = await conn.fetchrow(
|
||||
"""SELECT id, embedding, event_date, created_at, metadata, text
|
||||
FROM memory_units WHERE bank_id = $1 LIMIT 1""",
|
||||
bank_id,
|
||||
)
|
||||
|
||||
# entities: UUID (id), Timestamp (first_seen, last_seen), JSONB (metadata)
|
||||
original_entity = await conn.fetchrow(
|
||||
"""SELECT id, first_seen, last_seen, metadata, canonical_name
|
||||
FROM entities WHERE bank_id = $1 LIMIT 1""",
|
||||
bank_id,
|
||||
)
|
||||
|
||||
# banks: JSONB (personality/disposition)
|
||||
original_bank = await conn.fetchrow(
|
||||
"SELECT bank_id, created_at, updated_at FROM banks WHERE bank_id = $1",
|
||||
bank_id,
|
||||
)
|
||||
|
||||
assert original_unit is not None, "Should have created memory units"
|
||||
assert original_unit["embedding"] is not None, "Should have embedding"
|
||||
assert original_unit["id"] is not None, "Should have UUID"
|
||||
assert original_entity is not None, "Should have created entities"
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as f:
|
||||
backup_path = Path(f.name)
|
||||
|
||||
try:
|
||||
await _backup(pg0_db_url, backup_path)
|
||||
|
||||
# Clear all data
|
||||
async with memory._pool.acquire() as conn:
|
||||
for table in reversed(BACKUP_TABLES):
|
||||
await conn.execute(f"TRUNCATE TABLE {table} CASCADE")
|
||||
|
||||
await _restore(pg0_db_url, backup_path)
|
||||
|
||||
# Verify all column types are preserved exactly
|
||||
async with memory._pool.acquire() as conn:
|
||||
restored_unit = await conn.fetchrow(
|
||||
"""SELECT id, embedding, event_date, created_at, metadata, text
|
||||
FROM memory_units WHERE bank_id = $1 LIMIT 1""",
|
||||
bank_id,
|
||||
)
|
||||
|
||||
restored_entity = await conn.fetchrow(
|
||||
"""SELECT id, first_seen, last_seen, metadata, canonical_name
|
||||
FROM entities WHERE bank_id = $1 LIMIT 1""",
|
||||
bank_id,
|
||||
)
|
||||
|
||||
restored_bank = await conn.fetchrow(
|
||||
"SELECT bank_id, created_at, updated_at FROM banks WHERE bank_id = $1",
|
||||
bank_id,
|
||||
)
|
||||
|
||||
# Verify memory_units
|
||||
assert restored_unit is not None, "Should have restored memory unit"
|
||||
assert restored_unit["id"] == original_unit["id"], "UUID should match exactly"
|
||||
assert restored_unit["text"] == original_unit["text"], "Text should match"
|
||||
assert list(restored_unit["embedding"]) == list(original_unit["embedding"]), "Vector embedding should match exactly"
|
||||
assert restored_unit["event_date"] == original_unit["event_date"], "Timestamp should match exactly"
|
||||
assert restored_unit["created_at"] == original_unit["created_at"], "Created timestamp should match"
|
||||
assert restored_unit["metadata"] == original_unit["metadata"], "JSONB metadata should match"
|
||||
|
||||
# Verify entities
|
||||
assert restored_entity is not None, "Should have restored entity"
|
||||
assert restored_entity["id"] == original_entity["id"], "Entity UUID should match"
|
||||
assert restored_entity["canonical_name"] == original_entity["canonical_name"], "Entity name should match"
|
||||
assert restored_entity["first_seen"] == original_entity["first_seen"], "Entity first_seen should match"
|
||||
assert restored_entity["last_seen"] == original_entity["last_seen"], "Entity last_seen should match"
|
||||
assert restored_entity["metadata"] == original_entity["metadata"], "Entity metadata should match"
|
||||
|
||||
# Verify banks
|
||||
assert restored_bank is not None, "Should have restored bank"
|
||||
assert restored_bank["bank_id"] == original_bank["bank_id"], "Bank ID should match"
|
||||
assert restored_bank["created_at"] == original_bank["created_at"], "Bank created_at should match"
|
||||
|
||||
finally:
|
||||
if backup_path.exists():
|
||||
backup_path.unlink()
|
||||
@@ -1,222 +0,0 @@
|
||||
"""
|
||||
Test suite for causal relationship extraction.
|
||||
|
||||
Tests that the fact extraction system correctly identifies and validates
|
||||
causal relationships between facts, with valid indices.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from hindsight_api import LLMConfig
|
||||
from hindsight_api.engine.retain.fact_extraction import extract_facts_from_text
|
||||
|
||||
|
||||
class TestCausalRelationships:
|
||||
"""Tests for causal relationship extraction and validation."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_causal_chain_extraction(self):
|
||||
"""
|
||||
Test that a clear causal chain is extracted with valid relationships.
|
||||
|
||||
Story: Lost job -> couldn't pay rent -> had to move -> found new apartment
|
||||
|
||||
This is a 4-fact causal chain where each fact causes the next.
|
||||
The extracted causal relations should have valid indices (0-3).
|
||||
"""
|
||||
text = """
|
||||
I lost my job at the tech company in January because of layoffs.
|
||||
Because I lost my job, I couldn't pay my rent anymore.
|
||||
Since I couldn't afford rent, I had to move out of my apartment.
|
||||
After searching for weeks, I finally found a cheaper apartment in Brooklyn.
|
||||
"""
|
||||
|
||||
context = "Personal story about housing change"
|
||||
llm_config = LLMConfig.for_memory()
|
||||
|
||||
facts, _ = await extract_facts_from_text(
|
||||
text=text,
|
||||
event_date=datetime(2024, 3, 15),
|
||||
context=context,
|
||||
llm_config=llm_config,
|
||||
agent_name="TestUser"
|
||||
)
|
||||
|
||||
assert len(facts) >= 3, f"Should extract at least 3 facts from the causal chain. Got {len(facts)}"
|
||||
|
||||
# Collect all causal relations from all facts
|
||||
all_causal_relations = []
|
||||
for i, fact in enumerate(facts):
|
||||
if fact.causal_relations:
|
||||
for rel in fact.causal_relations:
|
||||
all_causal_relations.append({
|
||||
"from_fact_index": i,
|
||||
"to_fact_index": rel.target_fact_index,
|
||||
"relation_type": rel.relation_type,
|
||||
"strength": rel.strength,
|
||||
"from_fact_text": fact.fact[:50],
|
||||
})
|
||||
|
||||
# Verify that ALL causal relation indices are valid
|
||||
num_facts = len(facts)
|
||||
invalid_relations = []
|
||||
for rel in all_causal_relations:
|
||||
if rel["to_fact_index"] < 0 or rel["to_fact_index"] >= num_facts:
|
||||
invalid_relations.append(rel)
|
||||
|
||||
assert len(invalid_relations) == 0, (
|
||||
f"Found {len(invalid_relations)} causal relations with invalid indices! "
|
||||
f"Valid range is 0-{num_facts - 1}. "
|
||||
f"Invalid relations: {invalid_relations}"
|
||||
)
|
||||
|
||||
# Should have at least some causal relations extracted
|
||||
assert len(all_causal_relations) >= 2, (
|
||||
f"Should extract at least 2 causal relationships from this clear chain. "
|
||||
f"Got {len(all_causal_relations)}: {all_causal_relations}"
|
||||
)
|
||||
|
||||
# Verify relation types are valid
|
||||
valid_types = {"causes", "caused_by", "enables", "prevents"}
|
||||
for rel in all_causal_relations:
|
||||
assert rel["relation_type"] in valid_types, (
|
||||
f"Invalid relation_type '{rel['relation_type']}'. Must be one of {valid_types}"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_complex_causal_web(self):
|
||||
"""
|
||||
Test a more complex scenario with multiple interconnected causes.
|
||||
|
||||
This tests the LLM's ability to identify multiple causal links and
|
||||
ensure all referenced indices exist.
|
||||
"""
|
||||
text = """
|
||||
The heavy rain caused flooding in the basement.
|
||||
The flooding damaged the electrical system.
|
||||
Because of the electrical damage, we had to call an electrician.
|
||||
The electrician found that the wiring was old and needed replacement.
|
||||
We decided to renovate the entire basement while fixing the wiring.
|
||||
The renovation took three months and cost $15,000.
|
||||
"""
|
||||
|
||||
context = "Home repair story"
|
||||
llm_config = LLMConfig.for_memory()
|
||||
|
||||
facts, _ = await extract_facts_from_text(
|
||||
text=text,
|
||||
event_date=datetime(2024, 6, 1),
|
||||
context=context,
|
||||
llm_config=llm_config,
|
||||
agent_name="TestUser"
|
||||
)
|
||||
|
||||
assert len(facts) >= 4, f"Should extract at least 4 facts. Got {len(facts)}"
|
||||
|
||||
# Validate all causal relation indices
|
||||
num_facts = len(facts)
|
||||
for i, fact in enumerate(facts):
|
||||
if fact.causal_relations:
|
||||
for rel in fact.causal_relations:
|
||||
assert 0 <= rel.target_fact_index < num_facts, (
|
||||
f"Fact {i} has causal relation to invalid index {rel.target_fact_index}. "
|
||||
f"Valid range is 0-{num_facts - 1}. "
|
||||
f"Fact text: {fact.fact[:80]}..."
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_no_self_referencing_causal_relations(self):
|
||||
"""
|
||||
Test that facts don't have causal relations pointing to themselves.
|
||||
"""
|
||||
text = """
|
||||
I started learning Python because I wanted to automate my work tasks.
|
||||
Learning Python led me to discover machine learning.
|
||||
Machine learning fascinated me so much that I changed my career to data science.
|
||||
"""
|
||||
|
||||
context = "Career change story"
|
||||
llm_config = LLMConfig.for_memory()
|
||||
|
||||
facts, _ = await extract_facts_from_text(
|
||||
text=text,
|
||||
event_date=datetime(2024, 1, 1),
|
||||
context=context,
|
||||
llm_config=llm_config,
|
||||
agent_name="TestUser"
|
||||
)
|
||||
|
||||
# Check no fact references itself
|
||||
for i, fact in enumerate(facts):
|
||||
if fact.causal_relations:
|
||||
for rel in fact.causal_relations:
|
||||
assert rel.target_fact_index != i, (
|
||||
f"Fact {i} has a self-referencing causal relation! "
|
||||
f"Fact text: {fact.fact}"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bidirectional_causal_relationships(self):
|
||||
"""
|
||||
Test that bidirectional causal relationships (causes and caused_by)
|
||||
are handled correctly.
|
||||
"""
|
||||
text = """
|
||||
My promotion at work caused me to move to New York.
|
||||
Moving to New York was caused by my promotion at work.
|
||||
The new role enabled me to lead a team of engineers.
|
||||
"""
|
||||
|
||||
context = "Work promotion story"
|
||||
llm_config = LLMConfig.for_memory()
|
||||
|
||||
facts, _ = await extract_facts_from_text(
|
||||
text=text,
|
||||
event_date=datetime(2024, 2, 15),
|
||||
context=context,
|
||||
llm_config=llm_config,
|
||||
agent_name="TestUser"
|
||||
)
|
||||
|
||||
num_facts = len(facts)
|
||||
|
||||
# Validate all indices
|
||||
for i, fact in enumerate(facts):
|
||||
if fact.causal_relations:
|
||||
for rel in fact.causal_relations:
|
||||
assert 0 <= rel.target_fact_index < num_facts, (
|
||||
f"Invalid target_fact_index {rel.target_fact_index} in fact {i}. "
|
||||
f"Valid range: 0-{num_facts - 1}"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_causal_relation_strength_values(self):
|
||||
"""
|
||||
Test that causal relation strength values are within valid range [0.0, 1.0].
|
||||
"""
|
||||
text = """
|
||||
The stock market crash directly caused the company to lay off employees.
|
||||
The layoffs indirectly led to reduced consumer spending in the area.
|
||||
Reduced spending somewhat affected local businesses.
|
||||
"""
|
||||
|
||||
context = "Economic impact story"
|
||||
llm_config = LLMConfig.for_memory()
|
||||
|
||||
facts, _ = await extract_facts_from_text(
|
||||
text=text,
|
||||
event_date=datetime(2024, 4, 1),
|
||||
context=context,
|
||||
llm_config=llm_config,
|
||||
agent_name="TestUser"
|
||||
)
|
||||
|
||||
for i, fact in enumerate(facts):
|
||||
if fact.causal_relations:
|
||||
for rel in fact.causal_relations:
|
||||
assert 0.0 <= rel.strength <= 1.0, (
|
||||
f"Causal relation strength {rel.strength} is outside valid range [0.0, 1.0]. "
|
||||
f"Fact {i}: {fact.fact[:50]}..."
|
||||
)
|
||||
@@ -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"])
|
||||
@@ -173,107 +173,6 @@ async def test_regenerate_entity_observations(memory, request_context):
|
||||
await conn.execute("DELETE FROM entities WHERE bank_id = $1", bank_id)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_manual_regenerate_with_few_facts(memory, request_context):
|
||||
"""
|
||||
Test that manual regeneration works even with fewer than 5 facts.
|
||||
|
||||
This is important because:
|
||||
- Automatic generation during retain requires MIN_FACTS_THRESHOLD (5)
|
||||
- But manual regeneration via API should work with any number of facts
|
||||
- The UI triggers manual regeneration, so it should work regardless of fact count
|
||||
"""
|
||||
bank_id = f"test_manual_regen_{datetime.now(timezone.utc).timestamp()}"
|
||||
|
||||
try:
|
||||
# Store only 2 facts - below the automatic threshold
|
||||
await memory.retain_async(
|
||||
bank_id=bank_id,
|
||||
content="Alice works at Google as a senior software engineer.",
|
||||
context="work info",
|
||||
event_date=datetime(2024, 1, 15, tzinfo=timezone.utc),
|
||||
request_context=request_context,
|
||||
)
|
||||
await memory.retain_async(
|
||||
bank_id=bank_id,
|
||||
content="Alice loves hiking and outdoor photography.",
|
||||
context="hobbies",
|
||||
event_date=datetime(2024, 1, 16, tzinfo=timezone.utc),
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
# Find the Alice entity
|
||||
pool = await memory._get_pool()
|
||||
async with pool.acquire() as conn:
|
||||
entity_row = await conn.fetchrow(
|
||||
"""
|
||||
SELECT id, canonical_name
|
||||
FROM entities
|
||||
WHERE bank_id = $1 AND LOWER(canonical_name) LIKE '%alice%'
|
||||
LIMIT 1
|
||||
""",
|
||||
bank_id
|
||||
)
|
||||
|
||||
assert entity_row is not None, "Alice entity should have been extracted"
|
||||
|
||||
entity_id = str(entity_row['id'])
|
||||
entity_name = entity_row['canonical_name']
|
||||
|
||||
# Check fact count - should be < 5
|
||||
async with pool.acquire() as conn:
|
||||
fact_count = await conn.fetchval(
|
||||
"SELECT COUNT(*) FROM unit_entities WHERE entity_id = $1",
|
||||
entity_row['id']
|
||||
)
|
||||
|
||||
print(f"\n=== Manual Regeneration Test ===")
|
||||
print(f"Entity: {entity_name} (id: {entity_id})")
|
||||
print(f"Linked facts: {fact_count}")
|
||||
|
||||
# Verify we're testing with fewer than the automatic threshold
|
||||
assert fact_count < 5, f"Test requires < 5 facts, but entity has {fact_count}"
|
||||
|
||||
# Before regeneration - should have no observations (auto threshold not met)
|
||||
obs_before = await memory.get_entity_observations(bank_id, entity_id, limit=10, request_context=request_context)
|
||||
print(f"Observations before manual regenerate: {len(obs_before)}")
|
||||
|
||||
# Manually regenerate observations - this should work regardless of fact count
|
||||
created_ids = await memory.regenerate_entity_observations(
|
||||
bank_id=bank_id,
|
||||
entity_id=entity_id,
|
||||
entity_name=entity_name,
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
print(f"Observations created by manual regenerate: {len(created_ids)}")
|
||||
|
||||
# Get observations after regeneration
|
||||
observations = await memory.get_entity_observations(bank_id, entity_id, limit=10, request_context=request_context)
|
||||
print(f"Observations after manual regenerate: {len(observations)}")
|
||||
for obs in observations:
|
||||
print(f" - {obs.text}")
|
||||
|
||||
# Manual regeneration should create observations even with < 5 facts
|
||||
assert len(observations) > 0, \
|
||||
f"Manual regeneration should create observations even with only {fact_count} facts. " \
|
||||
f"The LLM should synthesize at least 1 observation from the available facts."
|
||||
|
||||
# Verify observations contain relevant content
|
||||
obs_texts = " ".join([o.text.lower() for o in observations])
|
||||
assert any(keyword in obs_texts for keyword in ["google", "engineer", "hiking", "photography", "alice"]), \
|
||||
"Observations should contain relevant information about Alice"
|
||||
|
||||
print(f"✓ Manual regeneration works with {fact_count} facts (below automatic threshold of 5)")
|
||||
|
||||
finally:
|
||||
# Cleanup
|
||||
pool = await memory._get_pool()
|
||||
async with pool.acquire() as conn:
|
||||
await conn.execute("DELETE FROM memory_units WHERE bank_id = $1", bank_id)
|
||||
await conn.execute("DELETE FROM entities WHERE bank_id = $1", bank_id)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_with_include_entities(memory, request_context):
|
||||
"""
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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=trusting, 5=skeptical) |
|
||||
**literalism** | **int** | How literally to interpret information (1=flexible, 5=literal) |
|
||||
**empathy** | **int** | How much to consider emotional context (1=detached, 5=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)
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user