prepare for release
This commit is contained in:
parent
943f6e7844
commit
09fbcc2020
45 changed files with 3079 additions and 1783 deletions
259
.github/workflows/release.yml
vendored
259
.github/workflows/release.yml
vendored
|
|
@ -6,8 +6,17 @@ on:
|
||||||
- 'v*'
|
- 'v*'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-python-package:
|
build-python-packages:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- name: hindsight-all
|
||||||
|
path: hindsight
|
||||||
|
- name: hindsight-api
|
||||||
|
path: hindsight-api
|
||||||
|
- name: hindsight-client
|
||||||
|
path: hindsight-clients/python
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
@ -22,15 +31,46 @@ jobs:
|
||||||
with:
|
with:
|
||||||
python-version-file: ".python-version"
|
python-version-file: ".python-version"
|
||||||
|
|
||||||
- name: Build hindsight package
|
- name: Build ${{ matrix.name }} package
|
||||||
working-directory: ./hindsight
|
working-directory: ./${{ matrix.path }}
|
||||||
run: uv build
|
run: uv build
|
||||||
|
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: python-hindsight-dist
|
name: python-${{ matrix.name }}-dist
|
||||||
path: hindsight/dist/*
|
path: ${{ matrix.path }}/dist/*
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
build-typescript-client:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
working-directory: ./hindsight-clients/typescript
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Build TypeScript client
|
||||||
|
working-directory: ./hindsight-clients/typescript
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Pack npm package
|
||||||
|
working-directory: ./hindsight-clients/typescript
|
||||||
|
run: npm pack
|
||||||
|
|
||||||
|
- name: Upload artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: typescript-client-dist
|
||||||
|
path: hindsight-clients/typescript/*.tgz
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
|
|
||||||
build-rust-cli:
|
build-rust-cli:
|
||||||
|
|
@ -101,7 +141,14 @@ jobs:
|
||||||
packages: write
|
packages: write
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
component: [api, control-plane]
|
include:
|
||||||
|
# All images use the same Dockerfile with different --target
|
||||||
|
- target: api-only
|
||||||
|
image_name: hindsight-api
|
||||||
|
- target: cp-only
|
||||||
|
image_name: hindsight-control-plane
|
||||||
|
- target: standalone
|
||||||
|
image_name: hindsight
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
@ -117,6 +164,9 @@ jobs:
|
||||||
docker-images: true
|
docker-images: true
|
||||||
swap-storage: true
|
swap-storage: true
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
|
@ -135,32 +185,21 @@ jobs:
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: ghcr.io/${{ github.repository_owner }}/hindsight-${{ matrix.component }}
|
images: ghcr.io/${{ github.repository_owner }}/${{ matrix.image_name }}
|
||||||
tags: |
|
tags: |
|
||||||
type=semver,pattern={{version}},value=${{ steps.get_version.outputs.VERSION }}
|
type=semver,pattern={{version}},value=${{ steps.get_version.outputs.VERSION }}
|
||||||
type=semver,pattern={{major}}.{{minor}},value=${{ steps.get_version.outputs.VERSION }}
|
type=semver,pattern={{major}}.{{minor}},value=${{ steps.get_version.outputs.VERSION }}
|
||||||
type=semver,pattern={{major}},value=${{ steps.get_version.outputs.VERSION }}
|
type=semver,pattern={{major}},value=${{ steps.get_version.outputs.VERSION }}
|
||||||
type=raw,value=latest
|
type=raw,value=latest
|
||||||
|
|
||||||
- name: Build and push Docker image (api)
|
- name: Build and push Docker image
|
||||||
if: matrix.component == 'api'
|
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: docker/api.Dockerfile
|
file: docker/standalone/Dockerfile
|
||||||
push: true
|
target: ${{ matrix.target }}
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
|
|
||||||
- name: Build and push Docker image (control-plane)
|
|
||||||
if: matrix.component == 'control-plane'
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: docker/control-plane.Dockerfile
|
|
||||||
push: true
|
push: true
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
|
|
@ -192,9 +231,63 @@ jobs:
|
||||||
path: helm-packages/*.tgz
|
path: helm-packages/*.tgz
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
|
|
||||||
|
publish-python-packages:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [build-python-packages]
|
||||||
|
environment: pypi
|
||||||
|
strategy:
|
||||||
|
max-parallel: 1
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
# Order matters: client and api first, then hindsight-all (which depends on them)
|
||||||
|
- name: hindsight-client
|
||||||
|
- name: hindsight-api
|
||||||
|
- name: hindsight-all
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Download ${{ matrix.name }}
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: python-${{ matrix.name }}-dist
|
||||||
|
path: ./dist
|
||||||
|
|
||||||
|
- name: Publish ${{ matrix.name }} to PyPI
|
||||||
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
|
with:
|
||||||
|
packages-dir: ./dist
|
||||||
|
skip-existing: true
|
||||||
|
|
||||||
|
publish-npm-package:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [build-typescript-client]
|
||||||
|
environment: npm
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
working-directory: ./hindsight-clients/typescript
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Build TypeScript client
|
||||||
|
working-directory: ./hindsight-clients/typescript
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Publish to npm
|
||||||
|
working-directory: ./hindsight-clients/typescript
|
||||||
|
run: npm publish --access public
|
||||||
|
env:
|
||||||
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|
||||||
create-github-release:
|
create-github-release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [build-python-package, build-rust-cli, build-docker-images, package-helm-chart]
|
needs: [build-python-packages, build-typescript-client, build-rust-cli, build-docker-images, package-helm-chart]
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
|
|
@ -205,11 +298,29 @@ jobs:
|
||||||
id: get_version
|
id: get_version
|
||||||
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
|
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Download Python package
|
- name: Download hindsight-all
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: python-hindsight-dist
|
name: python-hindsight-all-dist
|
||||||
path: ./artifacts/python-hindsight-dist
|
path: ./artifacts/python-hindsight-all
|
||||||
|
|
||||||
|
- name: Download hindsight-api
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: python-hindsight-api-dist
|
||||||
|
path: ./artifacts/python-hindsight-api
|
||||||
|
|
||||||
|
- name: Download hindsight-client
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: python-hindsight-client-dist
|
||||||
|
path: ./artifacts/python-hindsight-client
|
||||||
|
|
||||||
|
- name: Download TypeScript Client
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: typescript-client-dist
|
||||||
|
path: ./artifacts/typescript-client
|
||||||
|
|
||||||
- name: Download Rust CLI (Linux)
|
- name: Download Rust CLI (Linux)
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
|
|
@ -238,8 +349,12 @@ jobs:
|
||||||
- name: Prepare release assets
|
- name: Prepare release assets
|
||||||
run: |
|
run: |
|
||||||
mkdir -p release-assets
|
mkdir -p release-assets
|
||||||
# Python package
|
# Python packages
|
||||||
cp artifacts/python-hindsight-dist/* release-assets/
|
cp artifacts/python-hindsight-all/* release-assets/
|
||||||
|
cp artifacts/python-hindsight-api/* release-assets/
|
||||||
|
cp artifacts/python-hindsight-client/* release-assets/
|
||||||
|
# TypeScript Client
|
||||||
|
cp artifacts/typescript-client/*.tgz release-assets/
|
||||||
# Rust CLI binaries
|
# Rust CLI binaries
|
||||||
cp artifacts/rust-cli-hindsight-linux-amd64/hindsight-linux-amd64 release-assets/
|
cp artifacts/rust-cli-hindsight-linux-amd64/hindsight-linux-amd64 release-assets/
|
||||||
cp artifacts/rust-cli-hindsight-darwin-amd64/hindsight-darwin-amd64 release-assets/
|
cp artifacts/rust-cli-hindsight-darwin-amd64/hindsight-darwin-amd64 release-assets/
|
||||||
|
|
@ -250,37 +365,61 @@ jobs:
|
||||||
- name: Generate release notes
|
- name: Generate release notes
|
||||||
id: release_notes
|
id: release_notes
|
||||||
run: |
|
run: |
|
||||||
cat << EOF > release-notes.md
|
cat << 'EOF' > release-notes.md
|
||||||
# Hindsight v${{ steps.get_version.outputs.VERSION }}
|
# Hindsight v${{ steps.get_version.outputs.VERSION }}
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -p 8888:8888 -p 9999:9999 \
|
||||||
|
-e HINDSIGHT_API_LLM_PROVIDER=openai \
|
||||||
|
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
|
||||||
|
-e HINDSIGHT_API_LLM_MODEL=gpt-4o-mini \
|
||||||
|
ghcr.io/${{ github.repository_owner }}/hindsight:${{ steps.get_version.outputs.VERSION }}
|
||||||
|
```
|
||||||
|
|
||||||
## 📦 Release Artifacts
|
## 📦 Release Artifacts
|
||||||
|
|
||||||
### Python Package
|
### Docker Images
|
||||||
- \`hindsight-${{ steps.get_version.outputs.VERSION }}-py3-none-any.whl\`
|
- `ghcr.io/${{ github.repository_owner }}/hindsight:${{ steps.get_version.outputs.VERSION }}` - **Standalone all-in-one** (recommended)
|
||||||
- \`hindsight-${{ steps.get_version.outputs.VERSION }}.tar.gz\`
|
- `ghcr.io/${{ github.repository_owner }}/hindsight-api:${{ steps.get_version.outputs.VERSION }}` - API server only
|
||||||
|
- `ghcr.io/${{ github.repository_owner }}/hindsight-control-plane:${{ steps.get_version.outputs.VERSION }}` - Web UI only
|
||||||
|
|
||||||
|
### Python Packages
|
||||||
|
- `hindsight-all` - All-in-one package (includes API + client)
|
||||||
|
- `hindsight-api` - API server
|
||||||
|
- `hindsight-client` - Client library
|
||||||
|
|
||||||
|
### TypeScript/JavaScript
|
||||||
|
- `@hindsight/client` - TypeScript SDK
|
||||||
|
|
||||||
### CLI Binaries
|
### CLI Binaries
|
||||||
- \`hindsight-linux-amd64\` - Linux x86_64
|
- `hindsight-linux-amd64` - Linux x86_64
|
||||||
- \`hindsight-darwin-amd64\` - macOS Intel
|
- `hindsight-darwin-amd64` - macOS Intel
|
||||||
- \`hindsight-darwin-arm64\` - macOS Apple Silicon
|
- `hindsight-darwin-arm64` - macOS Apple Silicon
|
||||||
|
|
||||||
### Helm Chart
|
### Helm Chart
|
||||||
- \`hindsight-${{ steps.get_version.outputs.VERSION }}.tgz\`
|
- `hindsight-${{ steps.get_version.outputs.VERSION }}.tgz`
|
||||||
|
|
||||||
### Docker Images
|
|
||||||
Docker images are published to GitHub Container Registry:
|
|
||||||
- \`ghcr.io/${{ github.repository_owner }}/hindsight-api:${{ steps.get_version.outputs.VERSION }}\`
|
|
||||||
- \`ghcr.io/${{ github.repository_owner }}/hindsight-control-plane:${{ steps.get_version.outputs.VERSION }}\`
|
|
||||||
|
|
||||||
## 🚀 Installation
|
## 🚀 Installation
|
||||||
|
|
||||||
### Python Package
|
### Python
|
||||||
\`\`\`bash
|
```bash
|
||||||
pip install hindsight==${{ steps.get_version.outputs.VERSION }}
|
# All-in-one (recommended)
|
||||||
\`\`\`
|
pip install hindsight-all==${{ steps.get_version.outputs.VERSION }}
|
||||||
|
|
||||||
|
# Or install components separately
|
||||||
|
pip install hindsight-api==${{ steps.get_version.outputs.VERSION }}
|
||||||
|
pip install hindsight-client==${{ steps.get_version.outputs.VERSION }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### TypeScript/JavaScript
|
||||||
|
```bash
|
||||||
|
npm install @hindsight/client@${{ steps.get_version.outputs.VERSION }}
|
||||||
|
```
|
||||||
|
|
||||||
### CLI
|
### CLI
|
||||||
\`\`\`bash
|
```bash
|
||||||
# macOS (Apple Silicon)
|
# macOS (Apple Silicon)
|
||||||
curl -L https://github.com/${{ github.repository }}/releases/download/v${{ steps.get_version.outputs.VERSION }}/hindsight-darwin-arm64 -o hindsight
|
curl -L https://github.com/${{ github.repository }}/releases/download/v${{ steps.get_version.outputs.VERSION }}/hindsight-darwin-arm64 -o hindsight
|
||||||
chmod +x hindsight
|
chmod +x hindsight
|
||||||
|
|
@ -295,25 +434,12 @@ jobs:
|
||||||
curl -L https://github.com/${{ github.repository }}/releases/download/v${{ steps.get_version.outputs.VERSION }}/hindsight-linux-amd64 -o hindsight
|
curl -L https://github.com/${{ github.repository }}/releases/download/v${{ steps.get_version.outputs.VERSION }}/hindsight-linux-amd64 -o hindsight
|
||||||
chmod +x hindsight
|
chmod +x hindsight
|
||||||
sudo mv hindsight /usr/local/bin/
|
sudo mv hindsight /usr/local/bin/
|
||||||
\`\`\`
|
```
|
||||||
|
|
||||||
### Helm Chart
|
### Helm (Kubernetes)
|
||||||
\`\`\`bash
|
```bash
|
||||||
helm install hindsight hindsight-${{ steps.get_version.outputs.VERSION }}.tgz
|
helm install hindsight oci://ghcr.io/${{ github.repository_owner }}/charts/hindsight --version ${{ steps.get_version.outputs.VERSION }}
|
||||||
\`\`\`
|
```
|
||||||
|
|
||||||
### Docker
|
|
||||||
\`\`\`bash
|
|
||||||
# Pull API image
|
|
||||||
docker pull ghcr.io/${{ github.repository_owner }}/hindsight-api:${{ steps.get_version.outputs.VERSION }}
|
|
||||||
|
|
||||||
# Pull Control Plane image
|
|
||||||
docker pull ghcr.io/${{ github.repository_owner }}/hindsight-control-plane:${{ steps.get_version.outputs.VERSION }}
|
|
||||||
|
|
||||||
# Or use latest
|
|
||||||
docker pull ghcr.io/${{ github.repository_owner }}/hindsight-api:latest
|
|
||||||
docker pull ghcr.io/${{ github.repository_owner }}/hindsight-control-plane:latest
|
|
||||||
\`\`\`
|
|
||||||
EOF
|
EOF
|
||||||
cat release-notes.md
|
cat release-notes.md
|
||||||
|
|
||||||
|
|
@ -333,9 +459,10 @@ jobs:
|
||||||
echo "# Release v${{ steps.get_version.outputs.VERSION }} Published Successfully" >> $GITHUB_STEP_SUMMARY
|
echo "# Release v${{ steps.get_version.outputs.VERSION }} Published Successfully" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "## 📦 Components" >> $GITHUB_STEP_SUMMARY
|
echo "## 📦 Components" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- ✅ Python package (hindsight)" >> $GITHUB_STEP_SUMMARY
|
echo "- ✅ Python packages (hindsight-all, hindsight-api, hindsight-client)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- ✅ TypeScript Client (@hindsight/client)" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- ✅ Rust CLI (Linux amd64, macOS amd64, macOS arm64)" >> $GITHUB_STEP_SUMMARY
|
echo "- ✅ Rust CLI (Linux amd64, macOS amd64, macOS arm64)" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- ✅ Docker images (API, Control Plane)" >> $GITHUB_STEP_SUMMARY
|
echo "- ✅ Docker images (standalone, API, Control Plane)" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- ✅ Helm chart" >> $GITHUB_STEP_SUMMARY
|
echo "- ✅ Helm chart" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "🎉 Release is now available at: https://github.com/${{ github.repository }}/releases/tag/v${{ steps.get_version.outputs.VERSION }}" >> $GITHUB_STEP_SUMMARY
|
echo "🎉 Release is now available at: https://github.com/${{ github.repository }}/releases/tag/v${{ steps.get_version.outputs.VERSION }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
|
||||||
67
CONTRIBUTING.md
Normal file
67
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
# Contributing to Hindsight
|
||||||
|
|
||||||
|
Thanks for your interest in contributing to Hindsight!
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
1. Fork and clone the repository
|
||||||
|
2. Install dependencies:
|
||||||
|
```bash
|
||||||
|
cd hindsight-api && uv sync
|
||||||
|
```
|
||||||
|
3. Set up your environment:
|
||||||
|
```bash
|
||||||
|
export OPENAI_API_KEY=your-key
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Running the API locally
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/dev/start-api.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running the Control Plane locally
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/dev/start-control-plane.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running the documentation locally
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/dev/start-docs.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd hindsight-api
|
||||||
|
uv run pytest tests/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Code style
|
||||||
|
|
||||||
|
- Use Python type hints
|
||||||
|
- Follow existing code patterns
|
||||||
|
- Keep functions focused and well-named
|
||||||
|
|
||||||
|
## Pull Requests
|
||||||
|
|
||||||
|
1. Create a feature branch from `main`
|
||||||
|
2. Make your changes
|
||||||
|
3. Run tests to ensure nothing breaks
|
||||||
|
4. Submit a PR with a clear description of changes
|
||||||
|
|
||||||
|
## Reporting Issues
|
||||||
|
|
||||||
|
Open an issue on GitHub with:
|
||||||
|
- Clear description of the problem
|
||||||
|
- Steps to reproduce
|
||||||
|
- Expected vs actual behavior
|
||||||
|
- Environment details (OS, Python version)
|
||||||
|
|
||||||
|
## Questions?
|
||||||
|
|
||||||
|
Open a discussion on GitHub or reach out to the maintainers.
|
||||||
97
README.md
97
README.md
|
|
@ -2,59 +2,96 @@
|
||||||
|
|
||||||
**Long-term memory for AI agents.**
|
**Long-term memory for AI agents.**
|
||||||
|
|
||||||
AI assistants forget everything between sessions. Hindsight fixes that with a memory system that handles temporal reasoning, entity connections, and personality-aware responses.
|
|
||||||
|
|
||||||
## Why Hindsight?
|
## Why Hindsight?
|
||||||
|
|
||||||
- **Temporal queries** — "What did Alice do last spring?" requires more than vector search
|
AI assistants forget everything between sessions. Every conversation starts from zero—no context about who you are, what you've discussed, or what the memory bank has learned. This isn't just inconvenient; it fundamentally limits what AI memory banks can do.
|
||||||
- **Entity connections** — Knowing "Alice works at Google" + "Google is in Mountain View" = "Alice works in Mountain View"
|
|
||||||
- **Agent opinions** — Agents form and recall beliefs with confidence scores
|
|
||||||
- **Personality** — Big Five traits influence how agents process and respond to information
|
|
||||||
|
|
||||||
## 60-seconds step
|
**The problem is harder than it looks:**
|
||||||
|
|
||||||
|
- **Simple vector search isn't enough** — "What did Alice do last spring?" requires temporal reasoning, not just semantic similarity
|
||||||
|
- **Facts get disconnected** — Knowing "Alice works at Google" and "Google is in Mountain View" should let you answer "Where does Alice work?" even if you never stored that directly
|
||||||
|
- **Memory banks need opinions** — A coding assistant that remembers "the user prefers functional programming" should weigh that when making recommendations
|
||||||
|
- **Context matters** — The same information means different things to different memory banks with different personalities
|
||||||
|
|
||||||
|
Hindsight solves these problems with a memory system designed specifically for AI memory banks.
|
||||||
|
|
||||||
|
|
||||||
### 1. Install the Hindsight All package (client + API)
|
## Quick Start
|
||||||
|
|
||||||
|
### Option 1: Docker (recommended)
|
||||||
|
|
||||||
|
Get the full experience with the API and Control Plane UI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export OPENAI_API_KEY=your-key
|
||||||
|
docker run -p 8888:8888 -p 9999:9999 \
|
||||||
|
-e HINDSIGHT_API_LLM_PROVIDER=openai \
|
||||||
|
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
|
||||||
|
-e HINDSIGHT_API_LLM_MODEL=gpt-4o-mini \
|
||||||
|
vectorize/hindsight
|
||||||
|
```
|
||||||
|
|
||||||
|
- **API**: http://localhost:8888
|
||||||
|
- **Control Plane UI**: http://localhost:9999
|
||||||
|
|
||||||
|
Then use the Python client:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install hindsight-client
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
from hindsight import HindsightClient
|
||||||
|
|
||||||
|
client = HindsightClient(base_url="http://localhost:8888")
|
||||||
|
|
||||||
|
# Store memories
|
||||||
|
client.retain(bank_id="my-agent", content="Alice works at Google as a software engineer")
|
||||||
|
client.retain(bank_id="my-agent", content="Alice mentioned she loves hiking in the mountains")
|
||||||
|
|
||||||
|
# Query with temporal reasoning
|
||||||
|
results = client.recall(bank_id="my-agent", query="What does Alice do for work?")
|
||||||
|
|
||||||
|
# Get a synthesized perspective
|
||||||
|
response = client.reflect(bank_id="my-agent", query="Tell me about Alice")
|
||||||
|
print(response.text)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 2: Embedded (no docker/server required)
|
||||||
|
|
||||||
|
For quick prototyping, run everything in-process:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install hindsight-all
|
pip install hindsight-all
|
||||||
|
export OPENAI_API_KEY=your-key
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Import your OpenAI API key
|
|
||||||
```bash
|
|
||||||
export OPENAI_API_KEY=xx
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Run embedded server and client
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import os
|
import os
|
||||||
from hindsight import HindsightServer, HindsightClient
|
from hindsight import HindsightServer, HindsightClient
|
||||||
|
|
||||||
with HindsightServer(llm_provider="openai", llm_model="gpt-5.1-mini", llm_api_key=os.environ["OPENAI_API_KEY"]) as server:
|
with HindsightServer(llm_provider="openai", llm_model="gpt-4o-mini", llm_api_key=os.environ["OPENAI_API_KEY"]) as server:
|
||||||
client = HindsightClient(base_url=server.url)
|
client = HindsightClient(base_url=server.url)
|
||||||
|
|
||||||
# Retain memories
|
client.retain(bank_id="my-user", content="User prefers functional programming")
|
||||||
client.retain(bank_id="my-agent", content="Alice works at Google")
|
response = client.reflect(bank_id="my-user", query="What coding style should I use?")
|
||||||
client.retain(bank_id="my-agent", content="Bob prefers Python over JavaScript")
|
print(response.text)
|
||||||
|
|
||||||
# Recall memories
|
|
||||||
client.recall(bank_id="my-agent", query="What does Alice do?")
|
|
||||||
|
|
||||||
# Get memory perspective
|
|
||||||
client.reflect(bank_id="my-agent", query="Tell me about Alice")
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
Full documentation: [hindsight-docs](./hindsight-docs)
|
Full documentation: [vectorize-io.github.io/hindsight](https://vectorize-io.github.io/hindsight)
|
||||||
|
|
||||||
- [Architecture](./hindsight-docs/docs/developer/architecture.md) — How ingestion, storage, and retrieval work
|
- [Architecture](https://vectorize-io.github.io/hindsight/developer/architecture) — How ingestion, storage, and retrieval work
|
||||||
- [Python Client](./hindsight-docs/docs/sdks/python.md) — Full API reference
|
- [Python Client](https://vectorize-io.github.io/hindsight/sdks/python) — Full API reference
|
||||||
- [API Reference](./hindsight-docs/docs/api-reference/index.md) — REST API endpoints
|
- [API Reference](https://vectorize-io.github.io/hindsight/api-reference) — REST API endpoints
|
||||||
- [Personality](./hindsight-docs/docs/developer/personality.md) — Big Five traits and opinion formation
|
- [Personality](https://vectorize-io.github.io/hindsight/developer/personality) — Big Five traits and opinion formation
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
We welcome contributions! See [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
|
||||||
11
cookbook/README.md
Normal file
11
cookbook/README.md
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
# Hindsight Cookbook
|
||||||
|
|
||||||
|
For the cookbook with detailed examples, tutorials, and integrations, visit:
|
||||||
|
|
||||||
|
**[https://github.com/vectorize-io/hindsight-cookbook](https://github.com/vectorize-io/hindsight-cookbook)**
|
||||||
|
|
||||||
|
The cookbook repository includes:
|
||||||
|
- Integration examples with popular frameworks
|
||||||
|
- Real-world use cases and patterns
|
||||||
|
- Step-by-step tutorials
|
||||||
|
- Best practices and tips
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
# Distributed Hindsight Setup
|
|
||||||
|
|
||||||
Run API and Control Plane as separate containers.
|
|
||||||
|
|
||||||
## Start
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd services
|
|
||||||
docker-compose up
|
|
||||||
```
|
|
||||||
|
|
||||||
Access:
|
|
||||||
- **Control Plane**: http://localhost:3000
|
|
||||||
- **API**: http://localhost:8888
|
|
||||||
|
|
||||||
## What's Running
|
|
||||||
|
|
||||||
Two separate containers:
|
|
||||||
- `api` - Hindsight API with embedded pg0 database
|
|
||||||
- `control-plane` - Web UI
|
|
||||||
|
|
||||||
## Build Images
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./build-all.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Creates:
|
|
||||||
- `hindsight/api:latest`
|
|
||||||
- `hindsight/control-plane:latest`
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
The API uses embedded pg0 by default. Database files are stored in the `api_data` volume.
|
|
||||||
|
|
||||||
To use an external PostgreSQL database, add to `docker-compose.yml`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
api:
|
|
||||||
environment:
|
|
||||||
HINDSIGHT_API_DATABASE_URL: postgresql://user:pass@host:5432/db
|
|
||||||
```
|
|
||||||
|
|
||||||
## Data Persistence
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose down -v # Remove volumes
|
|
||||||
```
|
|
||||||
|
|
||||||
## Why Use This?
|
|
||||||
|
|
||||||
The distributed setup is useful when you want to:
|
|
||||||
- Scale API and UI independently
|
|
||||||
- Use an external database in production
|
|
||||||
- Deploy to Kubernetes/orchestration
|
|
||||||
- Run UI on different infrastructure
|
|
||||||
|
|
||||||
For simple deployments, use the main `docker-compose.yml` (standalone all-in-one).
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
# Dockerfile for Hindsight API (standalone)
|
|
||||||
FROM python:3.11-slim
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Install system dependencies and uv
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
gcc \
|
|
||||||
g++ \
|
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
|
||||||
&& pip install --no-cache-dir uv
|
|
||||||
|
|
||||||
# Copy dependency files and README (required by pyproject.toml)
|
|
||||||
COPY hindsight-api/pyproject.toml ./
|
|
||||||
COPY hindsight-api/README.md ./
|
|
||||||
|
|
||||||
# Sync dependencies (creates lock file if needed)
|
|
||||||
RUN uv sync
|
|
||||||
|
|
||||||
# Copy source code
|
|
||||||
COPY hindsight-api/hindsight_api ./hindsight_api
|
|
||||||
|
|
||||||
# Expose API port
|
|
||||||
EXPOSE 8888
|
|
||||||
|
|
||||||
# Set environment variables
|
|
||||||
ENV HINDSIGHT_API_HOST=0.0.0.0
|
|
||||||
ENV HINDSIGHT_API_PORT=8888
|
|
||||||
ENV HINDSIGHT_API_LOG_LEVEL=info
|
|
||||||
ENV PATH="/app/.venv/bin:$PATH"
|
|
||||||
|
|
||||||
# Run the API server
|
|
||||||
CMD ["python", "-m", "hindsight_api.web.server"]
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "Building Hindsight service images..."
|
|
||||||
|
|
||||||
cd "$(dirname "$0")/../.."
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "Building hindsight-api..."
|
|
||||||
docker build -f docker/services/api.Dockerfile -t hindsight/api:latest .
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "Building hindsight-control-plane..."
|
|
||||||
docker build -f docker/services/control-plane.Dockerfile -t hindsight/control-plane:latest .
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "✅ All service images built successfully!"
|
|
||||||
echo ""
|
|
||||||
echo "Available images:"
|
|
||||||
echo " - hindsight/api:latest"
|
|
||||||
echo " - hindsight/control-plane:latest"
|
|
||||||
echo ""
|
|
||||||
echo "To start all services:"
|
|
||||||
echo " cd docker && docker-compose up"
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
# Dockerfile for Hindsight Control Plane (standalone)
|
|
||||||
FROM node:20-alpine AS sdk-builder
|
|
||||||
|
|
||||||
WORKDIR /app/sdk
|
|
||||||
|
|
||||||
# Build TypeScript SDK
|
|
||||||
COPY hindsight-clients/typescript/package*.json ./
|
|
||||||
RUN npm ci
|
|
||||||
|
|
||||||
COPY hindsight-clients/typescript/ ./
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
# Build Control Plane
|
|
||||||
FROM node:20-alpine AS builder
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy built SDK
|
|
||||||
COPY --from=sdk-builder /app/sdk /app/sdk
|
|
||||||
|
|
||||||
# Install Control Plane dependencies
|
|
||||||
COPY hindsight-control-plane/package*.json ./
|
|
||||||
RUN npm ci
|
|
||||||
|
|
||||||
# Copy Control Plane source
|
|
||||||
COPY hindsight-control-plane/ ./
|
|
||||||
|
|
||||||
# Link SDK for build
|
|
||||||
RUN cd /app/sdk && npm link && cd /app && npm link @hindsight/client
|
|
||||||
|
|
||||||
# Build the Next.js app
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
# Create public directory if it doesn't exist
|
|
||||||
RUN mkdir -p public
|
|
||||||
|
|
||||||
# Production image
|
|
||||||
FROM node:20-alpine
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy built SDK
|
|
||||||
COPY --from=sdk-builder /app/sdk /app/sdk
|
|
||||||
|
|
||||||
# Copy package files and install production dependencies only
|
|
||||||
COPY hindsight-control-plane/package*.json ./
|
|
||||||
RUN npm ci --omit=dev
|
|
||||||
|
|
||||||
# Link SDK for runtime
|
|
||||||
RUN cd /app/sdk && npm link && cd /app && npm link @hindsight/client
|
|
||||||
|
|
||||||
# Copy built app from builder
|
|
||||||
COPY --from=builder /app/.next ./.next
|
|
||||||
COPY --from=builder /app/public ./public
|
|
||||||
COPY --from=builder /app/next.config.ts ./next.config.ts
|
|
||||||
|
|
||||||
# Expose control plane port
|
|
||||||
EXPOSE 3000
|
|
||||||
|
|
||||||
# Set environment variables
|
|
||||||
ENV NODE_ENV=production
|
|
||||||
ENV HINDSIGHT_CP_DATAPLANE_API_URL=http://localhost:8888
|
|
||||||
|
|
||||||
# Run the Next.js server
|
|
||||||
CMD ["npm", "start"]
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
services:
|
|
||||||
api:
|
|
||||||
build:
|
|
||||||
context: ../..
|
|
||||||
dockerfile: docker/services/api.Dockerfile
|
|
||||||
ports:
|
|
||||||
- "8888:8888"
|
|
||||||
environment:
|
|
||||||
# Pass through all HINDSIGHT_* environment variables
|
|
||||||
HINDSIGHT_API_LLM_API_KEY: ${HINDSIGHT_API_LLM_API_KEY:-}
|
|
||||||
HINDSIGHT_API_LLM_MODEL: ${HINDSIGHT_API_LLM_MODEL:-}
|
|
||||||
HINDSIGHT_API_LLM_BASE_URL: ${HINDSIGHT_API_LLM_BASE_URL:-}
|
|
||||||
HINDSIGHT_API_HOST: ${HINDSIGHT_API_HOST:-0.0.0.0}
|
|
||||||
HINDSIGHT_API_PORT: ${HINDSIGHT_API_PORT:-8888}
|
|
||||||
HINDSIGHT_API_LOG_LEVEL: ${HINDSIGHT_API_LOG_LEVEL:-info}
|
|
||||||
HINDSIGHT_API_DATABASE_URL: ${HINDSIGHT_API_DATABASE_URL:-}
|
|
||||||
volumes:
|
|
||||||
- api_data:/app/data
|
|
||||||
networks:
|
|
||||||
- hindsight
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
control-plane:
|
|
||||||
build:
|
|
||||||
context: ../..
|
|
||||||
dockerfile: docker/services/control-plane.Dockerfile
|
|
||||||
ports:
|
|
||||||
- "3000:3000"
|
|
||||||
environment:
|
|
||||||
NODE_ENV: production
|
|
||||||
HINDSIGHT_CP_DATAPLANE_API_URL: http://api:8888
|
|
||||||
depends_on:
|
|
||||||
- api
|
|
||||||
networks:
|
|
||||||
- hindsight
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
api_data:
|
|
||||||
|
|
||||||
networks:
|
|
||||||
hindsight:
|
|
||||||
|
|
@ -1,6 +1,25 @@
|
||||||
# Standalone All-in-One Hindsight Image
|
# Hindsight Docker Image
|
||||||
# API with embedded pg0 + Control Plane
|
# Supports building API-only, Control Plane-only, or both
|
||||||
FROM python:3.11-slim AS api-base
|
#
|
||||||
|
# Build args:
|
||||||
|
# INCLUDE_API=true/false - Include API (default: true)
|
||||||
|
# INCLUDE_CP=true/false - Include Control Plane (default: true)
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# docker build -t hindsight . # Both (standalone)
|
||||||
|
# docker build -t hindsight-api --build-arg INCLUDE_CP=false . # API only
|
||||||
|
# docker build -t hindsight-cp --build-arg INCLUDE_API=false . # Control Plane only
|
||||||
|
|
||||||
|
ARG INCLUDE_API=true
|
||||||
|
ARG INCLUDE_CP=true
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Stage: API Builder
|
||||||
|
# =============================================================================
|
||||||
|
FROM python:3.11-slim AS api-builder
|
||||||
|
|
||||||
|
ARG INCLUDE_API
|
||||||
|
RUN if [ "$INCLUDE_API" != "true" ]; then echo "Skipping API build" && exit 0; fi
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|
@ -21,12 +40,18 @@ WORKDIR /app/api
|
||||||
# Sync dependencies (will create lock file if needed)
|
# Sync dependencies (will create lock file if needed)
|
||||||
RUN uv sync
|
RUN uv sync
|
||||||
|
|
||||||
# Copy source code
|
# Copy source code and alembic migrations
|
||||||
COPY hindsight-api/hindsight_api ./hindsight_api
|
COPY hindsight-api/hindsight_api ./hindsight_api
|
||||||
|
COPY hindsight-api/alembic ./alembic
|
||||||
|
|
||||||
# Build TypeScript SDK
|
# =============================================================================
|
||||||
|
# Stage: SDK Builder (needed for Control Plane)
|
||||||
|
# =============================================================================
|
||||||
FROM node:20-alpine AS sdk-builder
|
FROM node:20-alpine AS sdk-builder
|
||||||
|
|
||||||
|
ARG INCLUDE_CP
|
||||||
|
RUN if [ "$INCLUDE_CP" != "true" ]; then echo "Skipping SDK build" && exit 0; fi
|
||||||
|
|
||||||
WORKDIR /app/sdk
|
WORKDIR /app/sdk
|
||||||
|
|
||||||
COPY hindsight-clients/typescript/package*.json ./
|
COPY hindsight-clients/typescript/package*.json ./
|
||||||
|
|
@ -35,9 +60,14 @@ RUN npm ci
|
||||||
COPY hindsight-clients/typescript/ ./
|
COPY hindsight-clients/typescript/ ./
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# Build Control Plane
|
# =============================================================================
|
||||||
|
# Stage: Control Plane Builder
|
||||||
|
# =============================================================================
|
||||||
FROM node:20-alpine AS cp-builder
|
FROM node:20-alpine AS cp-builder
|
||||||
|
|
||||||
|
ARG INCLUDE_CP
|
||||||
|
RUN if [ "$INCLUDE_CP" != "true" ]; then echo "Skipping CP build" && exit 0; fi
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy built SDK
|
# Copy built SDK
|
||||||
|
|
@ -59,8 +89,120 @@ RUN npm run build
|
||||||
# Create public directory if it doesn't exist
|
# Create public directory if it doesn't exist
|
||||||
RUN mkdir -p public
|
RUN mkdir -p public
|
||||||
|
|
||||||
# Final standalone image
|
# =============================================================================
|
||||||
FROM python:3.11-slim
|
# Stage: Final Image - API Only
|
||||||
|
# =============================================================================
|
||||||
|
FROM python:3.11-slim AS api-only
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install pg0 dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
curl \
|
||||||
|
libxml2 \
|
||||||
|
libssl3 \
|
||||||
|
libgssapi-krb5-2 \
|
||||||
|
libossp-uuid16 \
|
||||||
|
&& apt-get install -y libicu72 || apt-get install -y libicu74 || apt-get install -y libicu* \
|
||||||
|
&& 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
|
||||||
|
COPY --from=api-builder /app/api /app/api
|
||||||
|
|
||||||
|
# Copy startup script
|
||||||
|
COPY docker/standalone/start-all.sh /app/start-all.sh
|
||||||
|
RUN chmod +x /app/start-all.sh
|
||||||
|
|
||||||
|
# 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="/home/hindsight/.hindsight/bin:/app/api/.venv/bin:${PATH}"
|
||||||
|
|
||||||
|
# Install pg0 binary
|
||||||
|
RUN mkdir -p /home/hindsight/.hindsight/bin && \
|
||||||
|
ARCH=$(uname -m) && \
|
||||||
|
if [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then \
|
||||||
|
PG0_BINARY="pg0-linux-aarch64-gnu"; \
|
||||||
|
elif [ "$ARCH" = "x86_64" ]; then \
|
||||||
|
PG0_BINARY="pg0-linux-x86_64-gnu"; \
|
||||||
|
else \
|
||||||
|
echo "Unsupported architecture: $ARCH" && exit 1; \
|
||||||
|
fi && \
|
||||||
|
echo "Installing pg0 binary: $PG0_BINARY" && \
|
||||||
|
for i in 1 2 3 4 5; do \
|
||||||
|
curl -fsSL -o /home/hindsight/.hindsight/bin/pg0 \
|
||||||
|
"https://github.com/vectorize-io/pg0/releases/latest/download/$PG0_BINARY" && \
|
||||||
|
chmod +x /home/hindsight/.hindsight/bin/pg0 && \
|
||||||
|
break || (echo "Retry $i failed, waiting..." && sleep 10); \
|
||||||
|
done && \
|
||||||
|
/home/hindsight/.hindsight/bin/pg0 --version
|
||||||
|
|
||||||
|
# Pre-download PostgreSQL binaries
|
||||||
|
ENV PG0_HOME=/home/hindsight/.pg0-cache
|
||||||
|
RUN pg0 start --help && \
|
||||||
|
(pg0 start --name hindsight --port 5555 --username hindsight --password hindsight --database hindsight && \
|
||||||
|
sleep 2 && \
|
||||||
|
pg0 stop --name hindsight && \
|
||||||
|
echo "PostgreSQL pre-cached to $PG0_HOME") || echo "Pre-download skipped"
|
||||||
|
|
||||||
|
ENV PG0_HOME=/home/hindsight/.pg0
|
||||||
|
|
||||||
|
EXPOSE 8888
|
||||||
|
|
||||||
|
ENV HINDSIGHT_API_HOST=0.0.0.0
|
||||||
|
ENV HINDSIGHT_API_PORT=8888
|
||||||
|
ENV HINDSIGHT_API_LOG_LEVEL=info
|
||||||
|
ENV HINDSIGHT_ENABLE_API=true
|
||||||
|
ENV HINDSIGHT_ENABLE_CP=false
|
||||||
|
|
||||||
|
CMD ["/app/start-all.sh"]
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Stage: Final Image - Control Plane Only
|
||||||
|
# =============================================================================
|
||||||
|
FROM node:20-alpine AS cp-only
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy built SDK
|
||||||
|
COPY --from=sdk-builder /app/sdk /app/sdk
|
||||||
|
|
||||||
|
# Copy Control Plane standalone build
|
||||||
|
WORKDIR /app/control-plane
|
||||||
|
COPY --from=cp-builder /app/.next/standalone ./
|
||||||
|
COPY --from=cp-builder /app/.next/static ./.next/static
|
||||||
|
COPY --from=cp-builder /app/public ./public
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy startup script
|
||||||
|
COPY docker/standalone/start-all.sh /app/start-all.sh
|
||||||
|
RUN chmod +x /app/start-all.sh
|
||||||
|
|
||||||
|
# Install curl for health checks
|
||||||
|
RUN apk add --no-cache curl bash
|
||||||
|
|
||||||
|
EXPOSE 9999
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV HINDSIGHT_CP_DATAPLANE_API_URL=http://localhost:8888
|
||||||
|
ENV HINDSIGHT_ENABLE_API=false
|
||||||
|
ENV HINDSIGHT_ENABLE_CP=true
|
||||||
|
|
||||||
|
CMD ["/app/start-all.sh"]
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Stage: Final Image - Standalone (both API and Control Plane)
|
||||||
|
# =============================================================================
|
||||||
|
FROM python:3.11-slim AS standalone
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|
@ -70,6 +212,7 @@ RUN apt-get update && apt-get install -y \
|
||||||
libxml2 \
|
libxml2 \
|
||||||
libssl3 \
|
libssl3 \
|
||||||
libgssapi-krb5-2 \
|
libgssapi-krb5-2 \
|
||||||
|
libossp-uuid16 \
|
||||||
&& apt-get install -y libicu72 || apt-get install -y libicu74 || apt-get install -y libicu* \
|
&& apt-get install -y libicu72 || apt-get install -y libicu74 || apt-get install -y libicu* \
|
||||||
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
||||||
&& apt-get install -y nodejs \
|
&& apt-get install -y nodejs \
|
||||||
|
|
@ -80,22 +223,16 @@ RUN apt-get update && apt-get install -y \
|
||||||
RUN useradd -m -s /bin/bash hindsight
|
RUN useradd -m -s /bin/bash hindsight
|
||||||
|
|
||||||
# Copy API with virtual environment from builder
|
# Copy API with virtual environment from builder
|
||||||
COPY --from=api-base /app/api /app/api
|
COPY --from=api-builder /app/api /app/api
|
||||||
|
|
||||||
# Copy built SDK
|
# Copy built SDK
|
||||||
COPY --from=sdk-builder /app/sdk /app/sdk
|
COPY --from=sdk-builder /app/sdk /app/sdk
|
||||||
|
|
||||||
# Copy Control Plane
|
# Copy Control Plane standalone build
|
||||||
WORKDIR /app/control-plane
|
WORKDIR /app/control-plane
|
||||||
COPY --from=cp-builder /app/package*.json ./
|
COPY --from=cp-builder /app/.next/standalone ./
|
||||||
RUN npm ci --omit=dev
|
COPY --from=cp-builder /app/.next/static ./.next/static
|
||||||
|
|
||||||
# Link SDK for runtime
|
|
||||||
RUN cd /app/sdk && npm link && cd /app/control-plane && npm link @hindsight/client
|
|
||||||
|
|
||||||
COPY --from=cp-builder /app/.next ./.next
|
|
||||||
COPY --from=cp-builder /app/public ./public
|
COPY --from=cp-builder /app/public ./public
|
||||||
COPY --from=cp-builder /app/next.config.ts ./next.config.ts
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|
@ -109,24 +246,57 @@ RUN mkdir -p /app/data && chown -R hindsight:hindsight /app
|
||||||
# Switch to non-root user
|
# Switch to non-root user
|
||||||
USER hindsight
|
USER hindsight
|
||||||
|
|
||||||
# Install pg0
|
# Set PATH for hindsight user
|
||||||
RUN curl -fsSL https://raw.githubusercontent.com/vectorize-io/pg0/main/install.sh | bash
|
ENV PATH="/home/hindsight/.hindsight/bin:/app/api/.venv/bin:${PATH}"
|
||||||
|
|
||||||
# Start pg0 once to verify it works and pre-download PostgreSQL libraries
|
# Install pg0 binary
|
||||||
RUN pg0 --help && \
|
RUN mkdir -p /home/hindsight/.hindsight/bin && \
|
||||||
pg0 start --wait && \
|
ARCH=$(uname -m) && \
|
||||||
pg0 stop
|
if [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then \
|
||||||
|
PG0_BINARY="pg0-linux-aarch64-gnu"; \
|
||||||
|
elif [ "$ARCH" = "x86_64" ]; then \
|
||||||
|
PG0_BINARY="pg0-linux-x86_64-gnu"; \
|
||||||
|
else \
|
||||||
|
echo "Unsupported architecture: $ARCH" && exit 1; \
|
||||||
|
fi && \
|
||||||
|
echo "Installing pg0 binary: $PG0_BINARY" && \
|
||||||
|
for i in 1 2 3 4 5; do \
|
||||||
|
curl -fsSL -o /home/hindsight/.hindsight/bin/pg0 \
|
||||||
|
"https://github.com/vectorize-io/pg0/releases/latest/download/$PG0_BINARY" && \
|
||||||
|
chmod +x /home/hindsight/.hindsight/bin/pg0 && \
|
||||||
|
break || (echo "Retry $i failed, waiting..." && sleep 10); \
|
||||||
|
done && \
|
||||||
|
/home/hindsight/.hindsight/bin/pg0 --version
|
||||||
|
|
||||||
# Expose ports
|
# Pre-download PostgreSQL binaries
|
||||||
EXPOSE 8888 3000
|
ENV PG0_HOME=/home/hindsight/.pg0-cache
|
||||||
|
RUN pg0 start --help && \
|
||||||
|
(pg0 start --name hindsight --port 5555 --username hindsight --password hindsight --database hindsight && \
|
||||||
|
sleep 2 && \
|
||||||
|
pg0 stop --name hindsight && \
|
||||||
|
echo "PostgreSQL pre-cached to $PG0_HOME") || echo "Pre-download skipped"
|
||||||
|
|
||||||
|
ENV PG0_HOME=/home/hindsight/.pg0
|
||||||
|
|
||||||
|
EXPOSE 8888 9999
|
||||||
|
|
||||||
# Environment variables
|
|
||||||
ENV HINDSIGHT_API_HOST=0.0.0.0
|
ENV HINDSIGHT_API_HOST=0.0.0.0
|
||||||
ENV HINDSIGHT_API_PORT=8888
|
ENV HINDSIGHT_API_PORT=8888
|
||||||
ENV HINDSIGHT_API_LOG_LEVEL=info
|
ENV HINDSIGHT_API_LOG_LEVEL=info
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
ENV HINDSIGHT_CP_DATAPLANE_API_URL=http://localhost:8888
|
ENV HINDSIGHT_CP_DATAPLANE_API_URL=http://localhost:8888
|
||||||
ENV PATH="/home/hindsight/.local/bin:/app/api/.venv/bin:${PATH}"
|
ENV HINDSIGHT_ENABLE_API=true
|
||||||
|
ENV HINDSIGHT_ENABLE_CP=true
|
||||||
|
|
||||||
# Run startup script
|
|
||||||
CMD ["/app/start-all.sh"]
|
CMD ["/app/start-all.sh"]
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Default target selection based on build args
|
||||||
|
# =============================================================================
|
||||||
|
FROM standalone AS default-both
|
||||||
|
FROM api-only AS default-api
|
||||||
|
FROM cp-only AS default-cp
|
||||||
|
|
||||||
|
# This selects the final stage based on INCLUDE_API and INCLUDE_CP
|
||||||
|
# Use --target to override: docker build --target api-only .
|
||||||
|
FROM standalone
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,24 @@
|
||||||
services:
|
services:
|
||||||
hindsight:
|
hindsight:
|
||||||
|
image: hindsight
|
||||||
build:
|
build:
|
||||||
context: ../..
|
context: ../..
|
||||||
dockerfile: docker/standalone/Dockerfile
|
dockerfile: docker/standalone/Dockerfile
|
||||||
platform: linux/amd64
|
env_file:
|
||||||
|
- ../../.env
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "9999:9999"
|
||||||
- "8888:8888"
|
- "8888:8888"
|
||||||
environment:
|
environment:
|
||||||
# Pass through all HINDSIGHT_* environment variables from host
|
# These override env_file values only when set in host shell
|
||||||
HINDSIGHT_API_LLM_API_KEY: ${HINDSIGHT_API_LLM_API_KEY:-}
|
# Default values are applied only when not set in env_file or host
|
||||||
HINDSIGHT_API_LLM_MODEL: ${HINDSIGHT_API_LLM_MODEL:-}
|
|
||||||
HINDSIGHT_API_LLM_BASE_URL: ${HINDSIGHT_API_LLM_BASE_URL:-}
|
|
||||||
HINDSIGHT_API_LLM_PROVIDER: ${HINDSIGHT_API_LLM_PROVIDER:-}
|
|
||||||
HINDSIGHT_API_HOST: ${HINDSIGHT_API_HOST:-0.0.0.0}
|
HINDSIGHT_API_HOST: ${HINDSIGHT_API_HOST:-0.0.0.0}
|
||||||
HINDSIGHT_API_PORT: ${HINDSIGHT_API_PORT:-8888}
|
HINDSIGHT_API_PORT: ${HINDSIGHT_API_PORT:-8888}
|
||||||
HINDSIGHT_API_LOG_LEVEL: ${HINDSIGHT_API_LOG_LEVEL:-info}
|
HINDSIGHT_API_LOG_LEVEL: ${HINDSIGHT_API_LOG_LEVEL:-info}
|
||||||
# HINDSIGHT_API_DATABASE_URL can be set if you want to use an external database
|
# HINDSIGHT_API_DATABASE_URL can be set if you want to use an external database
|
||||||
# If not set, embedded pg0 will be used automatically
|
# If not set, embedded pg0 will be used automatically
|
||||||
# Add any other HINDSIGHT_* vars you need here
|
|
||||||
volumes:
|
volumes:
|
||||||
- hindsight_data:/app/data
|
- hindsight_data:/home/hindsight/.pg0
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
|
|
|
||||||
|
|
@ -4,36 +4,75 @@ set -e
|
||||||
echo "🚀 Starting Hindsight..."
|
echo "🚀 Starting Hindsight..."
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Start API (with embedded pg0)
|
# Service flags (default to true if not set)
|
||||||
echo "⚡ Starting Hindsight API (with embedded database)..."
|
ENABLE_API="${HINDSIGHT_ENABLE_API:-true}"
|
||||||
|
ENABLE_CP="${HINDSIGHT_ENABLE_CP:-true}"
|
||||||
|
|
||||||
|
# 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
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Track PIDs for wait
|
||||||
|
PIDS=()
|
||||||
|
|
||||||
|
# Start API if enabled
|
||||||
|
if [ "$ENABLE_API" = "true" ]; then
|
||||||
cd /app/api
|
cd /app/api
|
||||||
python -m hindsight_api.web.server &
|
python -m hindsight_api.web.server 2>&1 | sed -u 's/^/[api] /' &
|
||||||
API_PID=$!
|
API_PID=$!
|
||||||
|
PIDS+=($API_PID)
|
||||||
|
|
||||||
# Wait for API to be ready
|
# Wait for API to be ready
|
||||||
echo "⏳ Waiting for API..."
|
echo "⏳ Waiting for API..."
|
||||||
for i in {1..30}; do
|
for i in {1..60}; do
|
||||||
if curl -sf http://localhost:8888/health &>/dev/null || curl -sf http://localhost:8888/docs &>/dev/null; then
|
if curl -sf http://localhost:8888/health &>/dev/null; then
|
||||||
echo "✅ API is ready"
|
echo "✅ API is ready"
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
|
else
|
||||||
|
echo "⏭️ API disabled (HINDSIGHT_ENABLE_API=false)"
|
||||||
|
fi
|
||||||
|
|
||||||
# Start Control Plane
|
# Start Control Plane if enabled
|
||||||
|
if [ "$ENABLE_CP" = "true" ]; then
|
||||||
echo "🎛️ Starting Control Plane..."
|
echo "🎛️ Starting Control Plane..."
|
||||||
cd /app/control-plane
|
cd /app/control-plane
|
||||||
node .next/standalone/server.js &
|
PORT=9999 node server.js 2>&1 | grep -v -E "^[[:space:]]*(▲|✓|-|$)" | sed -u 's/^/[control-plane] /' &
|
||||||
CP_PID=$!
|
CP_PID=$!
|
||||||
|
PIDS+=($CP_PID)
|
||||||
|
else
|
||||||
|
echo "⏭️ Control Plane disabled (HINDSIGHT_ENABLE_CP=false)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Print status
|
||||||
echo ""
|
echo ""
|
||||||
echo "✅ Hindsight is running!"
|
echo "✅ Hindsight is running!"
|
||||||
echo ""
|
echo ""
|
||||||
echo "📍 Access:"
|
echo "📍 Access:"
|
||||||
echo " Control Plane: http://localhost:3000"
|
if [ "$ENABLE_CP" = "true" ]; then
|
||||||
|
echo " Control Plane: http://localhost:9999"
|
||||||
|
fi
|
||||||
|
if [ "$ENABLE_API" = "true" ]; then
|
||||||
echo " API: http://localhost:8888"
|
echo " API: http://localhost:8888"
|
||||||
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
# Check if any services are running
|
||||||
|
if [ ${#PIDS[@]} -eq 0 ]; then
|
||||||
|
echo "❌ No services enabled! Set HINDSIGHT_ENABLE_API=true or HINDSIGHT_ENABLE_CP=true"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Wait for any process to exit
|
# Wait for any process to exit
|
||||||
wait -n
|
wait -n
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -797,6 +797,24 @@ def create_app(memory: MemoryEngine, run_migrations: bool = True, initialize_mem
|
||||||
def _register_routes(app: FastAPI):
|
def _register_routes(app: FastAPI):
|
||||||
"""Register all API routes on the given app instance."""
|
"""Register all API routes on the given app instance."""
|
||||||
|
|
||||||
|
@app.get(
|
||||||
|
"/health",
|
||||||
|
summary="Health check endpoint",
|
||||||
|
description="Checks the health of the API and database connection",
|
||||||
|
tags=["Monitoring"]
|
||||||
|
)
|
||||||
|
async def health_endpoint():
|
||||||
|
"""
|
||||||
|
Health check endpoint that verifies database connectivity.
|
||||||
|
|
||||||
|
Returns 200 if healthy, 503 if unhealthy.
|
||||||
|
"""
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
|
||||||
|
health = await app.state.memory.health_check()
|
||||||
|
status_code = 200 if health.get("status") == "healthy" else 503
|
||||||
|
return JSONResponse(content=health, status_code=status_code)
|
||||||
|
|
||||||
@app.get(
|
@app.get(
|
||||||
"/metrics",
|
"/metrics",
|
||||||
summary="Prometheus metrics endpoint",
|
summary="Prometheus metrics endpoint",
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,19 @@
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
from fastmcp import FastMCP
|
from fastmcp import FastMCP
|
||||||
from hindsight_api import MemoryEngine
|
from hindsight_api import MemoryEngine
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
# Configure logging from HINDSIGHT_API_LOG_LEVEL environment variable
|
||||||
|
_log_level_str = os.environ.get("HINDSIGHT_API_LOG_LEVEL", "info").lower()
|
||||||
|
_log_level_map = {"critical": logging.CRITICAL, "error": logging.ERROR, "warning": logging.WARNING,
|
||||||
|
"info": logging.INFO, "debug": logging.DEBUG, "trace": logging.DEBUG}
|
||||||
|
logging.basicConfig(
|
||||||
|
level=_log_level_map.get(_log_level_str, logging.INFO),
|
||||||
|
format="%(asctime)s - %(levelname)s - %(name)s - %(message)s"
|
||||||
|
)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,23 @@ import logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class CrossEncoderReranker(ABC):
|
class CrossEncoderModel(ABC):
|
||||||
"""
|
"""
|
||||||
Abstract base class for cross-encoder reranking.
|
Abstract base class for cross-encoder reranking.
|
||||||
|
|
||||||
Cross-encoders take query-document pairs and return relevance scores.
|
Cross-encoders take query-document pairs and return relevance scores.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def load(self) -> None:
|
||||||
|
"""
|
||||||
|
Load the cross-encoder model.
|
||||||
|
|
||||||
|
This should be called during initialization to load the model
|
||||||
|
and avoid cold start latency on first predict() call.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def predict(self, pairs: List[Tuple[str, str]]) -> List[float]:
|
def predict(self, pairs: List[Tuple[str, str]]) -> List[float]:
|
||||||
"""
|
"""
|
||||||
|
|
@ -31,12 +41,11 @@ class CrossEncoderReranker(ABC):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class SentenceTransformersCrossEncoder(CrossEncoderReranker):
|
class SentenceTransformersCrossEncoder(CrossEncoderModel):
|
||||||
"""
|
"""
|
||||||
Cross-encoder implementation using SentenceTransformers.
|
Cross-encoder implementation using SentenceTransformers.
|
||||||
|
|
||||||
Uses lazy import so sentence-transformers is not required if another
|
Call load() during initialization to load the model and avoid cold starts.
|
||||||
reranking backend is used.
|
|
||||||
|
|
||||||
Default model is cross-encoder/ms-marco-MiniLM-L-6-v2:
|
Default model is cross-encoder/ms-marco-MiniLM-L-6-v2:
|
||||||
- Fast inference (~80ms for 100 pairs on CPU)
|
- Fast inference (~80ms for 100 pairs on CPU)
|
||||||
|
|
@ -46,13 +55,19 @@ class SentenceTransformersCrossEncoder(CrossEncoderReranker):
|
||||||
|
|
||||||
def __init__(self, model_name: str = "cross-encoder/ms-marco-MiniLM-L-6-v2"):
|
def __init__(self, model_name: str = "cross-encoder/ms-marco-MiniLM-L-6-v2"):
|
||||||
"""
|
"""
|
||||||
Initialize SentenceTransformers cross-encoder and load model.
|
Initialize SentenceTransformers cross-encoder.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
model_name: Name of the CrossEncoder model to use.
|
model_name: Name of the CrossEncoder model to use.
|
||||||
Default: cross-encoder/ms-marco-MiniLM-L-6-v2
|
Default: cross-encoder/ms-marco-MiniLM-L-6-v2
|
||||||
"""
|
"""
|
||||||
self.model_name = model_name
|
self.model_name = model_name
|
||||||
|
self._model = None
|
||||||
|
|
||||||
|
def load(self) -> None:
|
||||||
|
"""Load the cross-encoder model."""
|
||||||
|
if self._model is not None:
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from sentence_transformers import CrossEncoder
|
from sentence_transformers import CrossEncoder
|
||||||
|
|
@ -76,5 +91,7 @@ class SentenceTransformersCrossEncoder(CrossEncoderReranker):
|
||||||
Returns:
|
Returns:
|
||||||
List of relevance scores (raw logits from the model)
|
List of relevance scores (raw logits from the model)
|
||||||
"""
|
"""
|
||||||
scores = self._model.predict(pairs)
|
if self._model is None:
|
||||||
|
self.load()
|
||||||
|
scores = self._model.predict(pairs, show_progress_bar=False)
|
||||||
return scores.tolist() if hasattr(scores, 'tolist') else list(scores)
|
return scores.tolist() if hasattr(scores, 'tolist') else list(scores)
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,16 @@ class Embeddings(ABC):
|
||||||
the database schema.
|
the database schema.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def load(self) -> None:
|
||||||
|
"""
|
||||||
|
Load the embedding model.
|
||||||
|
|
||||||
|
This should be called during initialization to load the model
|
||||||
|
and avoid cold start latency on first encode() call.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def encode(self, texts: List[str]) -> List[List[float]]:
|
def encode(self, texts: List[str]) -> List[List[float]]:
|
||||||
"""
|
"""
|
||||||
|
|
@ -42,8 +52,7 @@ class SentenceTransformersEmbeddings(Embeddings):
|
||||||
"""
|
"""
|
||||||
Embeddings implementation using SentenceTransformers.
|
Embeddings implementation using SentenceTransformers.
|
||||||
|
|
||||||
Uses lazy import so sentence-transformers is not required if another
|
Call load() during initialization to load the model and avoid cold starts.
|
||||||
embedding backend is used.
|
|
||||||
|
|
||||||
Default model is BAAI/bge-small-en-v1.5 which produces 384-dimensional
|
Default model is BAAI/bge-small-en-v1.5 which produces 384-dimensional
|
||||||
embeddings matching the database schema.
|
embeddings matching the database schema.
|
||||||
|
|
@ -60,11 +69,12 @@ class SentenceTransformersEmbeddings(Embeddings):
|
||||||
"""
|
"""
|
||||||
self.model_name = model_name
|
self.model_name = model_name
|
||||||
self._model = None
|
self._model = None
|
||||||
self._load_model()
|
|
||||||
|
|
||||||
def _load_model(self):
|
def load(self) -> None:
|
||||||
"""Lazy load and validate the SentenceTransformer model."""
|
"""Load the embedding model."""
|
||||||
if self._model is None:
|
if self._model is not None:
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from sentence_transformers import SentenceTransformer
|
from sentence_transformers import SentenceTransformer
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
|
@ -97,5 +107,7 @@ class SentenceTransformersEmbeddings(Embeddings):
|
||||||
Returns:
|
Returns:
|
||||||
List of 384-dimensional embedding vectors
|
List of 384-dimensional embedding vectors
|
||||||
"""
|
"""
|
||||||
|
if self._model is None:
|
||||||
|
self.load()
|
||||||
embeddings = self._model.encode(texts, convert_to_numpy=True, show_progress_bar=False)
|
embeddings = self._model.encode(texts, convert_to_numpy=True, show_progress_bar=False)
|
||||||
return [emb.tolist() for emb in embeddings]
|
return [emb.tolist() for emb in embeddings]
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ from typing import Any, Dict, List, Optional, Tuple, Union, TypedDict
|
||||||
import asyncpg
|
import asyncpg
|
||||||
import asyncio
|
import asyncio
|
||||||
from .embeddings import Embeddings, SentenceTransformersEmbeddings
|
from .embeddings import Embeddings, SentenceTransformersEmbeddings
|
||||||
from .cross_encoder import CrossEncoderReranker as CrossEncoderModel
|
from .cross_encoder import CrossEncoderModel
|
||||||
import time
|
import time
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import uuid
|
import uuid
|
||||||
|
|
@ -362,15 +362,53 @@ class MemoryEngine:
|
||||||
logger.error(f"Failed to mark operation as failed {operation_id}: {e}")
|
logger.error(f"Failed to mark operation as failed {operation_id}: {e}")
|
||||||
|
|
||||||
async def initialize(self):
|
async def initialize(self):
|
||||||
"""Initialize the connection pool and background workers."""
|
"""Initialize the connection pool, models, and background workers.
|
||||||
|
|
||||||
|
Loads models (embeddings, cross-encoder) in parallel with pg0 startup
|
||||||
|
for faster overall initialization.
|
||||||
|
"""
|
||||||
if self._initialized:
|
if self._initialized:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Start pg0 embedded PostgreSQL if configured
|
import concurrent.futures
|
||||||
|
|
||||||
|
# Run model loading in thread pool (CPU-bound) in parallel with pg0 startup
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
|
||||||
|
async def start_pg0():
|
||||||
|
"""Start pg0 if configured."""
|
||||||
if self._use_pg0:
|
if self._use_pg0:
|
||||||
self._pg0 = EmbeddedPostgres()
|
self._pg0 = EmbeddedPostgres()
|
||||||
self.db_url = await self._pg0.ensure_running()
|
self.db_url = await self._pg0.ensure_running()
|
||||||
logger.info(f"Connecting to PostGre instance at {self.db_url}")
|
|
||||||
|
def load_embeddings():
|
||||||
|
"""Load embedding model (CPU-bound)."""
|
||||||
|
self.embeddings.load()
|
||||||
|
|
||||||
|
def load_cross_encoder():
|
||||||
|
"""Load cross-encoder model (CPU-bound)."""
|
||||||
|
self._cross_encoder_reranker.cross_encoder.load()
|
||||||
|
|
||||||
|
def load_query_analyzer():
|
||||||
|
"""Load query analyzer model (CPU-bound)."""
|
||||||
|
self.query_analyzer.load()
|
||||||
|
|
||||||
|
# Run pg0 and all model loads in parallel
|
||||||
|
# pg0 is async (IO-bound), models are sync (CPU-bound in thread pool)
|
||||||
|
# Use 3 workers to load all models concurrently
|
||||||
|
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
|
||||||
|
# Start all tasks
|
||||||
|
pg0_task = asyncio.create_task(start_pg0())
|
||||||
|
embeddings_future = loop.run_in_executor(executor, load_embeddings)
|
||||||
|
cross_encoder_future = loop.run_in_executor(executor, load_cross_encoder)
|
||||||
|
query_analyzer_future = loop.run_in_executor(executor, load_query_analyzer)
|
||||||
|
|
||||||
|
# Wait for all to complete
|
||||||
|
await asyncio.gather(
|
||||||
|
pg0_task, embeddings_future, cross_encoder_future, query_analyzer_future
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"Connecting to PostgreSQL at {self.db_url}")
|
||||||
|
|
||||||
# Create connection pool
|
# Create connection pool
|
||||||
# For read-heavy workloads with many parallel think/search operations,
|
# For read-heavy workloads with many parallel think/search operations,
|
||||||
|
|
@ -414,6 +452,24 @@ class MemoryEngine:
|
||||||
|
|
||||||
return await _retry_with_backoff(acquire)
|
return await _retry_with_backoff(acquire)
|
||||||
|
|
||||||
|
async def health_check(self) -> dict:
|
||||||
|
"""
|
||||||
|
Perform a health check by querying the database.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict with status and optional error message
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
pool = await self._get_pool()
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
result = await conn.fetchval("SELECT 1")
|
||||||
|
if result == 1:
|
||||||
|
return {"status": "healthy", "database": "connected"}
|
||||||
|
else:
|
||||||
|
return {"status": "unhealthy", "database": "unexpected response"}
|
||||||
|
except Exception as e:
|
||||||
|
return {"status": "unhealthy", "database": "error", "error": str(e)}
|
||||||
|
|
||||||
async def close(self):
|
async def close(self):
|
||||||
"""Close the connection pool and shutdown background workers."""
|
"""Close the connection pool and shutdown background workers."""
|
||||||
logger.info("close() started")
|
logger.info("close() started")
|
||||||
|
|
@ -503,8 +559,15 @@ class MemoryEngine:
|
||||||
if not texts:
|
if not texts:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
# Handle edge cases where event_date is at datetime boundaries
|
||||||
|
try:
|
||||||
time_lower = event_date - timedelta(hours=time_window_hours)
|
time_lower = event_date - timedelta(hours=time_window_hours)
|
||||||
|
except OverflowError:
|
||||||
|
time_lower = datetime.min
|
||||||
|
try:
|
||||||
time_upper = event_date + timedelta(hours=time_window_hours)
|
time_upper = event_date + timedelta(hours=time_window_hours)
|
||||||
|
except OverflowError:
|
||||||
|
time_upper = datetime.max
|
||||||
|
|
||||||
# Fetch ALL existing facts in time window ONCE (much faster than N queries)
|
# Fetch ALL existing facts in time window ONCE (much faster than N queries)
|
||||||
import time as time_mod
|
import time as time_mod
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,16 @@ class QueryAnalyzer(ABC):
|
||||||
information like temporal constraints, entities, etc.
|
information like temporal constraints, entities, etc.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def load(self) -> None:
|
||||||
|
"""
|
||||||
|
Load the query analyzer model.
|
||||||
|
|
||||||
|
This should be called during initialization to load the model
|
||||||
|
and avoid cold start latency on first analyze() call.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def analyze(
|
def analyze(
|
||||||
self, query: str, reference_date: Optional[datetime] = None
|
self, query: str, reference_date: Optional[datetime] = None
|
||||||
|
|
@ -94,9 +104,11 @@ class TransformerQueryAnalyzer(QueryAnalyzer):
|
||||||
self._model = None
|
self._model = None
|
||||||
self._tokenizer = None
|
self._tokenizer = None
|
||||||
|
|
||||||
def _load_model(self):
|
def load(self) -> None:
|
||||||
"""Lazy load the T5 model for temporal extraction."""
|
"""Load the T5 model for temporal extraction."""
|
||||||
if self._model is None:
|
if self._model is not None:
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
|
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
|
@ -105,10 +117,16 @@ class TransformerQueryAnalyzer(QueryAnalyzer):
|
||||||
"Install it with: pip install transformers"
|
"Install it with: pip install transformers"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger.info(f"Loading query analyzer model: {self.model_name}...")
|
||||||
self._tokenizer = AutoTokenizer.from_pretrained(self.model_name)
|
self._tokenizer = AutoTokenizer.from_pretrained(self.model_name)
|
||||||
self._model = AutoModelForSeq2SeqLM.from_pretrained(self.model_name)
|
self._model = AutoModelForSeq2SeqLM.from_pretrained(self.model_name)
|
||||||
self._model.to(self.device)
|
self._model.to(self.device)
|
||||||
self._model.eval()
|
self._model.eval()
|
||||||
|
logger.info("Query analyzer model loaded")
|
||||||
|
|
||||||
|
def _load_model(self):
|
||||||
|
"""Lazy load the T5 model for temporal extraction (calls load())."""
|
||||||
|
self.load()
|
||||||
|
|
||||||
def analyze(
|
def analyze(
|
||||||
self, query: str, reference_date: Optional[datetime] = None
|
self, query: str, reference_date: Optional[datetime] = None
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,107 @@ Link creation utilities for temporal, semantic, and entity links.
|
||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
from typing import List
|
from typing import List
|
||||||
from datetime import timedelta
|
from datetime import timedelta, datetime, timezone
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_datetime(dt):
|
||||||
|
"""Normalize datetime to be timezone-aware (UTC) for consistent comparison."""
|
||||||
|
if dt is None:
|
||||||
|
return None
|
||||||
|
if dt.tzinfo is None:
|
||||||
|
# Naive datetime - assume UTC
|
||||||
|
return dt.replace(tzinfo=timezone.utc)
|
||||||
|
return dt
|
||||||
|
|
||||||
|
|
||||||
|
def compute_temporal_links(
|
||||||
|
new_units: dict,
|
||||||
|
candidates: list,
|
||||||
|
time_window_hours: int = 24,
|
||||||
|
) -> list:
|
||||||
|
"""
|
||||||
|
Compute temporal links between new units and candidate neighbors.
|
||||||
|
|
||||||
|
This is a pure function that takes query results and returns link tuples,
|
||||||
|
making it easy to test without database access.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
new_units: Dict mapping unit_id (str) to event_date (datetime)
|
||||||
|
candidates: List of dicts with 'id' and 'event_date' keys (candidate neighbors)
|
||||||
|
time_window_hours: Time window in hours for temporal links
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of tuples: (from_unit_id, to_unit_id, 'temporal', weight, None)
|
||||||
|
"""
|
||||||
|
if not new_units:
|
||||||
|
return []
|
||||||
|
|
||||||
|
links = []
|
||||||
|
for unit_id, unit_event_date in new_units.items():
|
||||||
|
# Normalize unit_event_date for consistent comparison
|
||||||
|
unit_event_date_norm = _normalize_datetime(unit_event_date)
|
||||||
|
|
||||||
|
# Calculate time window bounds with overflow protection
|
||||||
|
try:
|
||||||
|
time_lower = unit_event_date_norm - timedelta(hours=time_window_hours)
|
||||||
|
except OverflowError:
|
||||||
|
time_lower = datetime.min.replace(tzinfo=timezone.utc)
|
||||||
|
try:
|
||||||
|
time_upper = unit_event_date_norm + timedelta(hours=time_window_hours)
|
||||||
|
except OverflowError:
|
||||||
|
time_upper = datetime.max.replace(tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
# Filter candidates within this unit's time window
|
||||||
|
matching_neighbors = [
|
||||||
|
(row['id'], row['event_date'])
|
||||||
|
for row in candidates
|
||||||
|
if time_lower <= _normalize_datetime(row['event_date']) <= time_upper
|
||||||
|
][:10] # Limit to top 10
|
||||||
|
|
||||||
|
for recent_id, recent_event_date in matching_neighbors:
|
||||||
|
# Calculate temporal proximity weight
|
||||||
|
time_diff_hours = abs((unit_event_date_norm - _normalize_datetime(recent_event_date)).total_seconds() / 3600)
|
||||||
|
weight = max(0.3, 1.0 - (time_diff_hours / time_window_hours))
|
||||||
|
links.append((unit_id, str(recent_id), 'temporal', weight, None))
|
||||||
|
|
||||||
|
return links
|
||||||
|
|
||||||
|
|
||||||
|
def compute_temporal_query_bounds(
|
||||||
|
new_units: dict,
|
||||||
|
time_window_hours: int = 24,
|
||||||
|
) -> tuple:
|
||||||
|
"""
|
||||||
|
Compute the min/max date bounds for querying temporal neighbors.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
new_units: Dict mapping unit_id (str) to event_date (datetime)
|
||||||
|
time_window_hours: Time window in hours
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (min_date, max_date) with overflow protection
|
||||||
|
"""
|
||||||
|
if not new_units:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
# Normalize all dates to be timezone-aware to avoid comparison issues
|
||||||
|
all_dates = [_normalize_datetime(d) for d in new_units.values()]
|
||||||
|
|
||||||
|
try:
|
||||||
|
min_date = min(all_dates) - timedelta(hours=time_window_hours)
|
||||||
|
except OverflowError:
|
||||||
|
min_date = datetime.min.replace(tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
try:
|
||||||
|
max_date = max(all_dates) + timedelta(hours=time_window_hours)
|
||||||
|
except OverflowError:
|
||||||
|
max_date = datetime.max.replace(tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
return min_date, max_date
|
||||||
|
|
||||||
|
|
||||||
def _log(log_buffer, message, level='info'):
|
def _log(log_buffer, message, level='info'):
|
||||||
"""Helper to log to buffer if available, otherwise use logger."""
|
"""Helper to log to buffer if available, otherwise use logger."""
|
||||||
if log_buffer is not None:
|
if log_buffer is not None:
|
||||||
|
|
@ -267,10 +363,8 @@ async def create_temporal_links_batch_per_fact(
|
||||||
_log(log_buffer, f" [7.1] Fetch event_dates for {len(unit_ids)} units: {time_mod.time() - fetch_dates_start:.3f}s")
|
_log(log_buffer, f" [7.1] Fetch event_dates for {len(unit_ids)} units: {time_mod.time() - fetch_dates_start:.3f}s")
|
||||||
|
|
||||||
# Fetch ALL potential temporal neighbors in ONE query (much faster!)
|
# Fetch ALL potential temporal neighbors in ONE query (much faster!)
|
||||||
# Get time range across all units
|
# Get time range across all units with overflow protection
|
||||||
all_dates = list(new_units.values())
|
min_date, max_date = compute_temporal_query_bounds(new_units, time_window_hours)
|
||||||
min_date = min(all_dates) - timedelta(hours=time_window_hours)
|
|
||||||
max_date = max(all_dates) + timedelta(hours=time_window_hours)
|
|
||||||
|
|
||||||
fetch_neighbors_start = time_mod.time()
|
fetch_neighbors_start = time_mod.time()
|
||||||
all_candidates = await conn.fetch(
|
all_candidates = await conn.fetch(
|
||||||
|
|
@ -291,24 +385,7 @@ async def create_temporal_links_batch_per_fact(
|
||||||
|
|
||||||
# Filter and create links in memory (much faster than N queries)
|
# Filter and create links in memory (much faster than N queries)
|
||||||
link_gen_start = time_mod.time()
|
link_gen_start = time_mod.time()
|
||||||
links = []
|
links = compute_temporal_links(new_units, all_candidates, time_window_hours)
|
||||||
for unit_id, unit_event_date in new_units.items():
|
|
||||||
# Filter candidates within this unit's time window
|
|
||||||
time_lower = unit_event_date - timedelta(hours=time_window_hours)
|
|
||||||
time_upper = unit_event_date + timedelta(hours=time_window_hours)
|
|
||||||
|
|
||||||
matching_neighbors = [
|
|
||||||
(row['id'], row['event_date'])
|
|
||||||
for row in all_candidates
|
|
||||||
if time_lower <= row['event_date'] <= time_upper
|
|
||||||
][:10] # Limit to top 10
|
|
||||||
|
|
||||||
for recent_id, recent_event_date in matching_neighbors:
|
|
||||||
# Calculate temporal proximity weight
|
|
||||||
time_diff_hours = abs((unit_event_date - recent_event_date).total_seconds() / 3600)
|
|
||||||
weight = max(0.3, 1.0 - (time_diff_hours / time_window_hours))
|
|
||||||
links.append((unit_id, str(recent_id), 'temporal', weight, None))
|
|
||||||
|
|
||||||
_log(log_buffer, f" [7.3] Generate {len(links)} temporal links: {time_mod.time() - link_gen_start:.3f}s")
|
_log(log_buffer, f" [7.3] Generate {len(links)} temporal links: {time_mod.time() - link_gen_start:.3f}s")
|
||||||
|
|
||||||
if links:
|
if links:
|
||||||
|
|
|
||||||
|
|
@ -23,9 +23,11 @@ class CrossEncoderReranker:
|
||||||
Args:
|
Args:
|
||||||
cross_encoder: CrossEncoderReranker instance. If None, uses default
|
cross_encoder: CrossEncoderReranker instance. If None, uses default
|
||||||
SentenceTransformersCrossEncoder with ms-marco-MiniLM-L-6-v2
|
SentenceTransformersCrossEncoder with ms-marco-MiniLM-L-6-v2
|
||||||
|
(loaded lazily for faster startup)
|
||||||
"""
|
"""
|
||||||
if cross_encoder is None:
|
if cross_encoder is None:
|
||||||
from hindsight_api.engine.cross_encoder import SentenceTransformersCrossEncoder
|
from hindsight_api.engine.cross_encoder import SentenceTransformersCrossEncoder
|
||||||
|
# Model is loaded lazily - call ensure_loaded() during initialize()
|
||||||
cross_encoder = SentenceTransformersCrossEncoder()
|
cross_encoder = SentenceTransformersCrossEncoder()
|
||||||
self.cross_encoder = cross_encoder
|
self.cross_encoder = cross_encoder
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,10 @@ import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import stat
|
import stat
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
@ -14,8 +14,7 @@ import httpx
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEFAULT_DATA_DIR = Path(os.environ.get("HINDSIGHT_API_PG0_DATA_DIR", Path.home() / ".hindsight" / "pg_data"))
|
# pg0 configuration
|
||||||
DEFAULT_INSTALL_DIR = Path.home() / ".hindsight" / "bin"
|
|
||||||
BINARY_NAME = "pg0"
|
BINARY_NAME = "pg0"
|
||||||
DEFAULT_PORT = 5555
|
DEFAULT_PORT = 5555
|
||||||
DEFAULT_USERNAME = "hindsight"
|
DEFAULT_USERNAME = "hindsight"
|
||||||
|
|
@ -65,9 +64,7 @@ def get_download_url(
|
||||||
version: str = "latest",
|
version: str = "latest",
|
||||||
repo: str = "vectorize-io/pg0",
|
repo: str = "vectorize-io/pg0",
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""Get the download URL for pg0 binary."""
|
||||||
"""
|
|
||||||
# Check for direct URL override
|
|
||||||
binary_name = get_platform_binary_name()
|
binary_name = get_platform_binary_name()
|
||||||
|
|
||||||
if version == "latest":
|
if version == "latest":
|
||||||
|
|
@ -76,17 +73,32 @@ def get_download_url(
|
||||||
return f"https://github.com/{repo}/releases/download/{version}/{binary_name}"
|
return f"https://github.com/{repo}/releases/download/{version}/{binary_name}"
|
||||||
|
|
||||||
|
|
||||||
|
def _find_pg0_binary() -> Optional[Path]:
|
||||||
|
"""Find pg0 binary in PATH or default install location."""
|
||||||
|
# First check PATH
|
||||||
|
pg0_in_path = shutil.which("pg0")
|
||||||
|
if pg0_in_path:
|
||||||
|
return Path(pg0_in_path)
|
||||||
|
|
||||||
|
# Fall back to default install location
|
||||||
|
default_path = Path.home() / ".hindsight" / "bin" / "pg0"
|
||||||
|
if default_path.exists() and os.access(default_path, os.X_OK):
|
||||||
|
return default_path
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class EmbeddedPostgres:
|
class EmbeddedPostgres:
|
||||||
"""
|
"""
|
||||||
Manages an embedded PostgreSQL server instance.
|
Manages an embedded PostgreSQL server instance using pg0.
|
||||||
|
|
||||||
This class handles:
|
This class handles:
|
||||||
- Downloading and installing the embedded-postgres CLI
|
- Finding or downloading the pg0 CLI
|
||||||
- Starting/stopping the PostgreSQL server
|
- Starting/stopping the PostgreSQL server
|
||||||
- Getting the connection URI
|
- Getting the connection URI
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
pg = EmbeddedPostgres(data_dir="~/.myapp/data")
|
pg = EmbeddedPostgres()
|
||||||
await pg.ensure_installed()
|
await pg.ensure_installed()
|
||||||
await pg.start()
|
await pg.start()
|
||||||
uri = await pg.get_uri()
|
uri = await pg.get_uri()
|
||||||
|
|
@ -96,8 +108,6 @@ class EmbeddedPostgres:
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
data_dir: Optional[Path] = None,
|
|
||||||
install_dir: Optional[Path] = None,
|
|
||||||
version: str = "latest",
|
version: str = "latest",
|
||||||
port: int = DEFAULT_PORT,
|
port: int = DEFAULT_PORT,
|
||||||
username: str = DEFAULT_USERNAME,
|
username: str = DEFAULT_USERNAME,
|
||||||
|
|
@ -109,17 +119,13 @@ class EmbeddedPostgres:
|
||||||
Initialize the embedded PostgreSQL manager.
|
Initialize the embedded PostgreSQL manager.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
data_dir: Directory to store PostgreSQL data. Defaults to ~/.hindsight/pg_data
|
version: Version of pg0 to download if not found. Defaults to "latest"
|
||||||
install_dir: Directory to install the CLI binary. Defaults to ~/.hindsight/bin
|
|
||||||
version: Version of embedded-postgres to use. Defaults to "latest"
|
|
||||||
port: Port to listen on. Defaults to 5555
|
port: Port to listen on. Defaults to 5555
|
||||||
username: Username for the database. Defaults to "hindsight"
|
username: Username for the database. Defaults to "hindsight"
|
||||||
password: Password for the database. Defaults to "hindsight"
|
password: Password for the database. Defaults to "hindsight"
|
||||||
database: Database name to create. Defaults to "hindsight"
|
database: Database name to create. Defaults to "hindsight"
|
||||||
name: Instance name for pg0. Defaults to "hindsight"
|
name: Instance name for pg0. Defaults to "hindsight"
|
||||||
"""
|
"""
|
||||||
self.data_dir = Path(data_dir or DEFAULT_DATA_DIR).expanduser()
|
|
||||||
self.install_dir = Path(install_dir or DEFAULT_INSTALL_DIR).expanduser()
|
|
||||||
self.version = version
|
self.version = version
|
||||||
self.port = port
|
self.port = port
|
||||||
self.username = username
|
self.username = username
|
||||||
|
|
@ -127,35 +133,42 @@ class EmbeddedPostgres:
|
||||||
self.database = database
|
self.database = database
|
||||||
self.name = name
|
self.name = name
|
||||||
|
|
||||||
# Binary path
|
# Will be set when binary is found/installed
|
||||||
binary_name = "pg0.exe" if platform.system() == "Windows" else "pg0"
|
self._binary_path: Optional[Path] = _find_pg0_binary()
|
||||||
self.binary_path = self.install_dir / binary_name
|
|
||||||
|
|
||||||
self._process: Optional[subprocess.Popen] = None
|
@property
|
||||||
|
def binary_path(self) -> Path:
|
||||||
|
"""Get the path to the pg0 binary."""
|
||||||
|
if self._binary_path is None:
|
||||||
|
# Default install location
|
||||||
|
return Path.home() / ".hindsight" / "bin" / "pg0"
|
||||||
|
return self._binary_path
|
||||||
|
|
||||||
def is_installed(self) -> bool:
|
def is_installed(self) -> bool:
|
||||||
"""Check if the embedded-postgres CLI is installed."""
|
"""Check if pg0 is available (in PATH or installed)."""
|
||||||
return self.binary_path.exists() and os.access(self.binary_path, os.X_OK)
|
self._binary_path = _find_pg0_binary()
|
||||||
|
return self._binary_path is not None
|
||||||
|
|
||||||
async def ensure_installed(self) -> None:
|
async def ensure_installed(self) -> None:
|
||||||
"""
|
"""
|
||||||
Ensure the embedded-postgres CLI is installed.
|
Ensure pg0 is available.
|
||||||
|
|
||||||
Downloads and installs the binary if not already present.
|
First checks PATH, then default location, then downloads if needed.
|
||||||
"""
|
"""
|
||||||
if self.is_installed():
|
if self.is_installed():
|
||||||
logger.info(f"pg0 already installed at {self.binary_path}")
|
logger.debug(f"pg0 found at {self._binary_path}")
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.info("Installing pg0 CLI...")
|
logger.info("pg0 not found, downloading...")
|
||||||
|
|
||||||
# Log platform information
|
# Log platform information
|
||||||
binary_name = get_platform_binary_name()
|
binary_name = get_platform_binary_name()
|
||||||
logger.info(f"Detected platform: system={platform.system()}, machine={platform.machine()}")
|
logger.info(f"Detected platform: system={platform.system()}, machine={platform.machine()}")
|
||||||
logger.info(f"Will download binary: {binary_name}")
|
|
||||||
|
|
||||||
# Create install directory
|
# Install to default location
|
||||||
self.install_dir.mkdir(parents=True, exist_ok=True)
|
install_dir = Path.home() / ".hindsight" / "bin"
|
||||||
|
install_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
install_path = install_dir / "pg0"
|
||||||
|
|
||||||
# Download the binary
|
# Download the binary
|
||||||
download_url = get_download_url(self.version)
|
download_url = get_download_url(self.version)
|
||||||
|
|
@ -167,60 +180,74 @@ class EmbeddedPostgres:
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
# Write binary to disk
|
# Write binary to disk
|
||||||
with open(self.binary_path, "wb") as f:
|
with open(install_path, "wb") as f:
|
||||||
f.write(response.content)
|
f.write(response.content)
|
||||||
|
|
||||||
# Make executable on Unix
|
# Make executable on Unix
|
||||||
if platform.system() != "Windows":
|
if platform.system() != "Windows":
|
||||||
st = os.stat(self.binary_path)
|
st = os.stat(install_path)
|
||||||
os.chmod(self.binary_path, st.st_mode | stat.S_IEXEC)
|
os.chmod(install_path, st.st_mode | stat.S_IEXEC)
|
||||||
|
|
||||||
logger.info(f"Installed pg0 to {self.binary_path}")
|
self._binary_path = install_path
|
||||||
|
logger.info(f"Installed pg0 to {install_path}")
|
||||||
|
|
||||||
except httpx.HTTPError as e:
|
except httpx.HTTPError as e:
|
||||||
raise RuntimeError(f"Failed to download pg0: {e}") from e
|
raise RuntimeError(f"Failed to download pg0: {e}") from e
|
||||||
|
|
||||||
def _run_command(self, *args: str, capture_output: bool = True) -> subprocess.CompletedProcess:
|
def _run_command(self, *args: str, capture_output: bool = True) -> subprocess.CompletedProcess:
|
||||||
"""Run an embedded-postgres command synchronously."""
|
"""Run a pg0 command synchronously."""
|
||||||
|
cmd = [str(self.binary_path), *args]
|
||||||
|
return subprocess.run(cmd, capture_output=capture_output, text=True)
|
||||||
|
|
||||||
|
async def _run_command_async(self, *args: str, timeout: int = 120) -> tuple[int, str, str]:
|
||||||
|
"""Run a pg0 command asynchronously."""
|
||||||
cmd = [str(self.binary_path), *args]
|
cmd = [str(self.binary_path), *args]
|
||||||
|
|
||||||
return subprocess.run(
|
def run_sync():
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
cmd,
|
cmd,
|
||||||
capture_output=capture_output,
|
stdin=subprocess.DEVNULL,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
text=True,
|
text=True,
|
||||||
|
timeout=timeout,
|
||||||
)
|
)
|
||||||
|
return result.returncode, result.stdout, result.stderr
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return 1, "", "Command timed out"
|
||||||
|
|
||||||
async def _run_command_async(self, *args: str) -> tuple[int, str, str]:
|
loop = asyncio.get_event_loop()
|
||||||
"""Run an embedded-postgres command asynchronously."""
|
return await loop.run_in_executor(None, run_sync)
|
||||||
cmd = [str(self.binary_path), *args]
|
|
||||||
|
|
||||||
process = await asyncio.create_subprocess_exec(
|
def _extract_uri_from_output(self, output: str) -> Optional[str]:
|
||||||
*cmd,
|
"""Extract the PostgreSQL URI from pg0 start output."""
|
||||||
stdout=asyncio.subprocess.PIPE,
|
match = re.search(r"Connection URI:\s*(postgresql://[^\s]+)", output)
|
||||||
stderr=asyncio.subprocess.PIPE,
|
if match:
|
||||||
)
|
return match.group(1)
|
||||||
|
return None
|
||||||
|
|
||||||
stdout, stderr = await process.communicate()
|
async def start(self, max_retries: int = 3, retry_delay: float = 2.0) -> str:
|
||||||
return process.returncode, stdout.decode(), stderr.decode()
|
|
||||||
|
|
||||||
async def start(self) -> str:
|
|
||||||
"""
|
"""
|
||||||
Start the PostgreSQL server.
|
Start the PostgreSQL server with retry logic.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
max_retries: Maximum number of start attempts (default: 3)
|
||||||
|
retry_delay: Initial delay between retries in seconds (default: 2.0)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The connection URI for the started server.
|
The connection URI for the started server.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
RuntimeError: If the server fails to start.
|
RuntimeError: If the server fails to start after all retries.
|
||||||
"""
|
"""
|
||||||
if not self.is_installed():
|
if not self.is_installed():
|
||||||
raise RuntimeError("pg0 is not installed. Call ensure_installed() first.")
|
raise RuntimeError("pg0 is not installed. Call ensure_installed() first.")
|
||||||
|
|
||||||
# Create data directory
|
logger.info(f"Starting embedded PostgreSQL (name: {self.name}, port: {self.port})...")
|
||||||
self.data_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
logger.info(f"Starting embedded PostgreSQL (name: {self.name}, data: {self.data_dir}, install: {self.install_dir}, port: {self.port})...")
|
|
||||||
|
|
||||||
|
last_error = None
|
||||||
|
for attempt in range(1, max_retries + 1):
|
||||||
returncode, stdout, stderr = await self._run_command_async(
|
returncode, stdout, stderr = await self._run_command_async(
|
||||||
"start",
|
"start",
|
||||||
"--name", self.name,
|
"--name", self.name,
|
||||||
|
|
@ -228,24 +255,40 @@ class EmbeddedPostgres:
|
||||||
"--username", self.username,
|
"--username", self.username,
|
||||||
"--password", self.password,
|
"--password", self.password,
|
||||||
"--database", self.database,
|
"--database", self.database,
|
||||||
"--data-dir", self.data_dir.as_posix()
|
timeout=300,
|
||||||
)
|
)
|
||||||
|
|
||||||
if returncode != 0:
|
# Try to extract URI from output
|
||||||
raise RuntimeError(f"Failed to start PostgreSQL: {stderr}")
|
uri = self._extract_uri_from_output(stdout)
|
||||||
|
if uri:
|
||||||
|
logger.info(f"PostgreSQL started on port {self.port}")
|
||||||
|
return uri
|
||||||
|
|
||||||
logger.info("Embedded PostgreSQL started")
|
# Check if pg0 info can find the running instance
|
||||||
|
try:
|
||||||
|
uri = await self.get_uri()
|
||||||
|
logger.info(f"PostgreSQL started on port {self.port}")
|
||||||
|
return uri
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
|
|
||||||
# Get and return the URI
|
# Start failed, log and retry
|
||||||
return await self.get_uri()
|
last_error = stderr or f"pg0 start returned exit code {returncode}"
|
||||||
|
if attempt < max_retries:
|
||||||
|
delay = retry_delay * (2 ** (attempt - 1))
|
||||||
|
logger.warning(f"pg0 start attempt {attempt}/{max_retries} failed: {last_error.strip()}")
|
||||||
|
logger.info(f"Retrying in {delay:.1f}s...")
|
||||||
|
await asyncio.sleep(delay)
|
||||||
|
else:
|
||||||
|
logger.warning(f"pg0 start attempt {attempt}/{max_retries} failed: {last_error.strip()}")
|
||||||
|
|
||||||
|
# All retries exhausted - use constructed URI as fallback
|
||||||
|
uri = f"postgresql://{self.username}:{self.password}@localhost:{self.port}/{self.database}"
|
||||||
|
logger.warning(f"All pg0 start attempts failed, using constructed URI: {uri}")
|
||||||
|
return uri
|
||||||
|
|
||||||
async def stop(self) -> None:
|
async def stop(self) -> None:
|
||||||
"""
|
"""Stop the PostgreSQL server."""
|
||||||
Stop the PostgreSQL server.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
RuntimeError: If the server fails to stop.
|
|
||||||
"""
|
|
||||||
if not self.is_installed():
|
if not self.is_installed():
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -254,7 +297,6 @@ class EmbeddedPostgres:
|
||||||
returncode, stdout, stderr = await self._run_command_async("stop", "--name", self.name)
|
returncode, stdout, stderr = await self._run_command_async("stop", "--name", self.name)
|
||||||
|
|
||||||
if returncode != 0:
|
if returncode != 0:
|
||||||
# Don't raise if server wasn't running
|
|
||||||
if "not running" in stderr.lower():
|
if "not running" in stderr.lower():
|
||||||
return
|
return
|
||||||
raise RuntimeError(f"Failed to stop PostgreSQL: {stderr}")
|
raise RuntimeError(f"Failed to stop PostgreSQL: {stderr}")
|
||||||
|
|
@ -262,20 +304,13 @@ class EmbeddedPostgres:
|
||||||
logger.info("Embedded PostgreSQL stopped")
|
logger.info("Embedded PostgreSQL stopped")
|
||||||
|
|
||||||
async def _get_info(self) -> dict:
|
async def _get_info(self) -> dict:
|
||||||
"""
|
"""Get info from pg0 using the `info -o json` command."""
|
||||||
Get info from pg0 using the `info -o json` command.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dictionary with 'running' (bool) and 'uri' (str) keys.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
RuntimeError: If unable to get info.
|
|
||||||
"""
|
|
||||||
if not self.is_installed():
|
if not self.is_installed():
|
||||||
raise RuntimeError("pg0 is not installed.")
|
raise RuntimeError("pg0 is not installed.")
|
||||||
|
|
||||||
returncode, stdout, stderr = await self._run_command_async(
|
returncode, stdout, stderr = await self._run_command_async(
|
||||||
"info", "--name", self.name, "-o", "json")
|
"info", "--name", self.name, "-o", "json"
|
||||||
|
)
|
||||||
|
|
||||||
if returncode != 0:
|
if returncode != 0:
|
||||||
raise RuntimeError(f"Failed to get PostgreSQL info: {stderr}")
|
raise RuntimeError(f"Failed to get PostgreSQL info: {stderr}")
|
||||||
|
|
@ -286,15 +321,7 @@ class EmbeddedPostgres:
|
||||||
raise RuntimeError(f"Failed to parse pg0 info output: {e}")
|
raise RuntimeError(f"Failed to parse pg0 info output: {e}")
|
||||||
|
|
||||||
async def get_uri(self) -> str:
|
async def get_uri(self) -> str:
|
||||||
"""
|
"""Get the connection URI for the PostgreSQL server."""
|
||||||
Get the connection URI for the PostgreSQL server.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
PostgreSQL connection URI (e.g., postgresql://user:pass@localhost:5432/db)
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
RuntimeError: If unable to get the URI or server is not running.
|
|
||||||
"""
|
|
||||||
info = await self._get_info()
|
info = await self._get_info()
|
||||||
uri = info.get("uri")
|
uri = info.get("uri")
|
||||||
if not uri:
|
if not uri:
|
||||||
|
|
@ -302,12 +329,7 @@ class EmbeddedPostgres:
|
||||||
return uri
|
return uri
|
||||||
|
|
||||||
async def status(self) -> dict:
|
async def status(self) -> dict:
|
||||||
"""
|
"""Get the status of the PostgreSQL server."""
|
||||||
Get the status of the PostgreSQL server.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dictionary with status information including 'running' boolean and 'uri'.
|
|
||||||
"""
|
|
||||||
if not self.is_installed():
|
if not self.is_installed():
|
||||||
return {"installed": False, "running": False}
|
return {"installed": False, "running": False}
|
||||||
|
|
||||||
|
|
@ -317,16 +339,9 @@ class EmbeddedPostgres:
|
||||||
"installed": True,
|
"installed": True,
|
||||||
"running": info.get("running", False),
|
"running": info.get("running", False),
|
||||||
"uri": info.get("uri"),
|
"uri": info.get("uri"),
|
||||||
"data_dir": str(self.data_dir),
|
|
||||||
"binary_path": str(self.binary_path),
|
|
||||||
}
|
}
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
return {
|
return {"installed": True, "running": False}
|
||||||
"installed": True,
|
|
||||||
"running": False,
|
|
||||||
"data_dir": str(self.data_dir),
|
|
||||||
"binary_path": str(self.binary_path),
|
|
||||||
}
|
|
||||||
|
|
||||||
async def is_running(self) -> bool:
|
async def is_running(self) -> bool:
|
||||||
"""Check if the PostgreSQL server is currently running."""
|
"""Check if the PostgreSQL server is currently running."""
|
||||||
|
|
@ -355,59 +370,42 @@ class EmbeddedPostgres:
|
||||||
return await self.start()
|
return await self.start()
|
||||||
|
|
||||||
def uninstall(self) -> None:
|
def uninstall(self) -> None:
|
||||||
"""Remove the embedded-postgres binary."""
|
"""Remove the pg0 binary (only if we installed it)."""
|
||||||
if self.binary_path.exists():
|
default_path = Path.home() / ".hindsight" / "bin" / "pg0"
|
||||||
self.binary_path.unlink()
|
if default_path.exists():
|
||||||
logger.info(f"Removed {self.binary_path}")
|
default_path.unlink()
|
||||||
|
logger.info(f"Removed {default_path}")
|
||||||
|
|
||||||
def clear_data(self) -> None:
|
def clear_data(self) -> None:
|
||||||
"""Remove all PostgreSQL data (destructive!)."""
|
"""Remove all PostgreSQL data (destructive!)."""
|
||||||
if self.data_dir.exists():
|
result = self._run_command("drop", "--name", self.name, "--force")
|
||||||
shutil.rmtree(self.data_dir)
|
if result.returncode == 0:
|
||||||
logger.info(f"Removed data directory {self.data_dir}")
|
logger.info(f"Dropped pg0 instance {self.name}")
|
||||||
|
else:
|
||||||
|
logger.warning(f"Failed to drop pg0 instance {self.name}: {result.stderr}")
|
||||||
|
|
||||||
|
|
||||||
# Convenience functions for simple usage
|
# Convenience functions
|
||||||
|
|
||||||
_default_instance: Optional[EmbeddedPostgres] = None
|
_default_instance: Optional[EmbeddedPostgres] = None
|
||||||
|
|
||||||
|
|
||||||
def get_embedded_postgres(
|
def get_embedded_postgres() -> EmbeddedPostgres:
|
||||||
data_dir: Optional[Path] = None,
|
"""Get or create the default EmbeddedPostgres instance."""
|
||||||
install_dir: Optional[Path] = None,
|
|
||||||
) -> EmbeddedPostgres:
|
|
||||||
"""
|
|
||||||
Get or create the default EmbeddedPostgres instance.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
data_dir: Override default data directory
|
|
||||||
install_dir: Override default install directory
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
EmbeddedPostgres instance
|
|
||||||
"""
|
|
||||||
global _default_instance
|
global _default_instance
|
||||||
|
|
||||||
if _default_instance is None or data_dir or install_dir:
|
if _default_instance is None:
|
||||||
_default_instance = EmbeddedPostgres(
|
_default_instance = EmbeddedPostgres()
|
||||||
data_dir=data_dir,
|
|
||||||
install_dir=install_dir,
|
|
||||||
)
|
|
||||||
|
|
||||||
return _default_instance
|
return _default_instance
|
||||||
|
|
||||||
|
|
||||||
async def start_embedded_postgres(
|
async def start_embedded_postgres() -> str:
|
||||||
data_dir: Optional[Path] = None,
|
|
||||||
) -> str:
|
|
||||||
"""
|
"""
|
||||||
Quick start function for embedded PostgreSQL.
|
Quick start function for embedded PostgreSQL.
|
||||||
|
|
||||||
Downloads, installs, and starts PostgreSQL in one call.
|
Downloads, installs, and starts PostgreSQL in one call.
|
||||||
|
|
||||||
Args:
|
|
||||||
data_dir: Directory to store PostgreSQL data
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Connection URI string
|
Connection URI string
|
||||||
|
|
||||||
|
|
@ -415,7 +413,7 @@ async def start_embedded_postgres(
|
||||||
db_url = await start_embedded_postgres()
|
db_url = await start_embedded_postgres()
|
||||||
conn = await asyncpg.connect(db_url)
|
conn = await asyncpg.connect(db_url)
|
||||||
"""
|
"""
|
||||||
pg = get_embedded_postgres(data_dir=data_dir)
|
pg = get_embedded_postgres()
|
||||||
return await pg.ensure_running()
|
return await pg.ensure_running()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,10 @@ app = create_app(
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
# Get log level from environment variable (default: info)
|
||||||
|
env_log_level = os.environ.get("HINDSIGHT_API_LOG_LEVEL", "info").lower()
|
||||||
|
if env_log_level not in ["critical", "error", "warning", "info", "debug", "trace"]:
|
||||||
|
env_log_level = "info"
|
||||||
|
|
||||||
# Parse CLI arguments
|
# Parse CLI arguments
|
||||||
parser = argparse.ArgumentParser(description="Memory Graph API Server")
|
parser = argparse.ArgumentParser(description="Memory Graph API Server")
|
||||||
|
|
@ -87,8 +90,8 @@ if __name__ == "__main__":
|
||||||
parser.add_argument("--port", type=int, default=8888, help="Port to bind to (default: 8888)")
|
parser.add_argument("--port", type=int, default=8888, help="Port to bind to (default: 8888)")
|
||||||
parser.add_argument("--reload", action="store_true", help="Enable auto-reload on code changes")
|
parser.add_argument("--reload", action="store_true", help="Enable auto-reload on code changes")
|
||||||
parser.add_argument("--workers", type=int, default=1, help="Number of worker processes (default: 1)")
|
parser.add_argument("--workers", type=int, default=1, help="Number of worker processes (default: 1)")
|
||||||
parser.add_argument("--log-level", default="info", choices=["critical", "error", "warning", "info", "debug", "trace"],
|
parser.add_argument("--log-level", default=env_log_level, choices=["critical", "error", "warning", "info", "debug", "trace"],
|
||||||
help="Log level (default: info)")
|
help=f"Log level (default: {env_log_level}, from HINDSIGHT_API_LOG_LEVEL)")
|
||||||
parser.add_argument("--access-log", action="store_true", help="Enable access log")
|
parser.add_argument("--access-log", action="store_true", help="Enable access log")
|
||||||
parser.add_argument("--no-access-log", dest="access_log", action="store_false", help="Disable access log")
|
parser.add_argument("--no-access-log", dest="access_log", action="store_false", help="Disable access log")
|
||||||
parser.add_argument("--proxy-headers", action="store_true", help="Enable X-Forwarded-Proto, X-Forwarded-For headers")
|
parser.add_argument("--proxy-headers", action="store_true", help="Enable X-Forwarded-Proto, X-Forwarded-For headers")
|
||||||
|
|
@ -99,6 +102,21 @@ if __name__ == "__main__":
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Configure Python logging based on log level
|
||||||
|
log_level_map = {
|
||||||
|
"critical": logging.CRITICAL,
|
||||||
|
"error": logging.ERROR,
|
||||||
|
"warning": logging.WARNING,
|
||||||
|
"info": logging.INFO,
|
||||||
|
"debug": logging.DEBUG,
|
||||||
|
"trace": logging.DEBUG, # Python doesn't have TRACE, use DEBUG
|
||||||
|
}
|
||||||
|
logging.basicConfig(
|
||||||
|
level=log_level_map.get(args.log_level, logging.INFO),
|
||||||
|
format="%(asctime)s - %(levelname)s - %(name)s - %(message)s"
|
||||||
|
)
|
||||||
|
logging.info(f"Starting Hindsight API on {args.host}:{args.port}")
|
||||||
|
|
||||||
app_ref = "hindsight_api.web.server:app"
|
app_ref = "hindsight_api.web.server:app"
|
||||||
|
|
||||||
# Prepare uvicorn config
|
# Prepare uvicorn config
|
||||||
|
|
|
||||||
256
hindsight-api/tests/test_link_utils.py
Normal file
256
hindsight-api/tests/test_link_utils.py
Normal file
|
|
@ -0,0 +1,256 @@
|
||||||
|
"""Tests for link_utils datetime handling and temporal link computation."""
|
||||||
|
import pytest
|
||||||
|
from datetime import datetime, timezone, timedelta
|
||||||
|
|
||||||
|
from hindsight_api.engine.retain.link_utils import (
|
||||||
|
_normalize_datetime,
|
||||||
|
compute_temporal_links,
|
||||||
|
compute_temporal_query_bounds,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestNormalizeDatetime:
|
||||||
|
"""Tests for the _normalize_datetime helper function."""
|
||||||
|
|
||||||
|
def test_none_returns_none(self):
|
||||||
|
"""Test that None input returns None."""
|
||||||
|
assert _normalize_datetime(None) is None
|
||||||
|
|
||||||
|
def test_naive_datetime_becomes_utc(self):
|
||||||
|
"""Test that naive datetimes are converted to UTC."""
|
||||||
|
naive_dt = datetime(2024, 6, 15, 10, 30, 0)
|
||||||
|
result = _normalize_datetime(naive_dt)
|
||||||
|
|
||||||
|
assert result.tzinfo is not None
|
||||||
|
assert result.tzinfo == timezone.utc
|
||||||
|
assert result.year == 2024
|
||||||
|
assert result.month == 6
|
||||||
|
assert result.day == 15
|
||||||
|
assert result.hour == 10
|
||||||
|
assert result.minute == 30
|
||||||
|
|
||||||
|
def test_aware_datetime_unchanged(self):
|
||||||
|
"""Test that timezone-aware datetimes are returned unchanged."""
|
||||||
|
aware_dt = datetime(2024, 6, 15, 10, 30, 0, tzinfo=timezone.utc)
|
||||||
|
result = _normalize_datetime(aware_dt)
|
||||||
|
|
||||||
|
assert result == aware_dt
|
||||||
|
assert result.tzinfo == timezone.utc
|
||||||
|
|
||||||
|
def test_mixed_datetimes_can_be_compared(self):
|
||||||
|
"""Test that normalized naive and aware datetimes can be compared."""
|
||||||
|
naive_dt = datetime(2024, 6, 15, 10, 30, 0)
|
||||||
|
aware_dt = datetime(2024, 6, 15, 10, 30, 0, tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
normalized_naive = _normalize_datetime(naive_dt)
|
||||||
|
normalized_aware = _normalize_datetime(aware_dt)
|
||||||
|
|
||||||
|
# Should be able to compare without TypeError
|
||||||
|
assert normalized_naive == normalized_aware
|
||||||
|
|
||||||
|
|
||||||
|
class TestComputeTemporalQueryBounds:
|
||||||
|
"""Tests for compute_temporal_query_bounds function."""
|
||||||
|
|
||||||
|
def test_empty_units_returns_none(self):
|
||||||
|
"""Test that empty input returns (None, None)."""
|
||||||
|
min_date, max_date = compute_temporal_query_bounds({})
|
||||||
|
assert min_date is None
|
||||||
|
assert max_date is None
|
||||||
|
|
||||||
|
def test_single_unit_normal_date(self):
|
||||||
|
"""Test bounds for a single unit with normal date."""
|
||||||
|
units = {"unit-1": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)}
|
||||||
|
min_date, max_date = compute_temporal_query_bounds(units, time_window_hours=24)
|
||||||
|
|
||||||
|
assert min_date == datetime(2024, 6, 14, 12, 0, 0, tzinfo=timezone.utc)
|
||||||
|
assert max_date == datetime(2024, 6, 16, 12, 0, 0, tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
def test_multiple_units(self):
|
||||||
|
"""Test bounds span across multiple units."""
|
||||||
|
units = {
|
||||||
|
"unit-1": datetime(2024, 6, 10, 12, 0, 0, tzinfo=timezone.utc),
|
||||||
|
"unit-2": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc),
|
||||||
|
"unit-3": datetime(2024, 6, 20, 12, 0, 0, tzinfo=timezone.utc),
|
||||||
|
}
|
||||||
|
min_date, max_date = compute_temporal_query_bounds(units, time_window_hours=24)
|
||||||
|
|
||||||
|
# min should be Jun 10 - 24h = Jun 9
|
||||||
|
assert min_date == datetime(2024, 6, 9, 12, 0, 0, tzinfo=timezone.utc)
|
||||||
|
# max should be Jun 20 + 24h = Jun 21
|
||||||
|
assert max_date == datetime(2024, 6, 21, 12, 0, 0, tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
def test_mixed_naive_and_aware_datetimes(self):
|
||||||
|
"""Test that mixed naive/aware datetimes work correctly."""
|
||||||
|
units = {
|
||||||
|
"unit-1": datetime(2024, 6, 10, 12, 0, 0), # naive
|
||||||
|
"unit-2": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc), # aware
|
||||||
|
}
|
||||||
|
# Should not raise TypeError
|
||||||
|
min_date, max_date = compute_temporal_query_bounds(units, time_window_hours=24)
|
||||||
|
|
||||||
|
assert min_date is not None
|
||||||
|
assert max_date is not None
|
||||||
|
assert min_date.tzinfo is not None
|
||||||
|
assert max_date.tzinfo is not None
|
||||||
|
|
||||||
|
def test_overflow_near_datetime_min(self):
|
||||||
|
"""Test overflow protection near datetime.min."""
|
||||||
|
units = {"unit-1": datetime(1, 1, 2, 0, 0, tzinfo=timezone.utc)}
|
||||||
|
min_date, max_date = compute_temporal_query_bounds(units, time_window_hours=48)
|
||||||
|
|
||||||
|
# Should handle overflow gracefully
|
||||||
|
assert min_date == datetime.min.replace(tzinfo=timezone.utc)
|
||||||
|
assert max_date is not None
|
||||||
|
|
||||||
|
def test_overflow_near_datetime_max(self):
|
||||||
|
"""Test overflow protection near datetime.max."""
|
||||||
|
units = {"unit-1": datetime(9999, 12, 30, 0, 0, tzinfo=timezone.utc)}
|
||||||
|
min_date, max_date = compute_temporal_query_bounds(units, time_window_hours=48)
|
||||||
|
|
||||||
|
# Should handle overflow gracefully
|
||||||
|
assert min_date is not None
|
||||||
|
assert max_date == datetime.max.replace(tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
|
class TestComputeTemporalLinks:
|
||||||
|
"""Tests for compute_temporal_links function."""
|
||||||
|
|
||||||
|
def test_empty_units_returns_empty(self):
|
||||||
|
"""Test that empty input returns empty list."""
|
||||||
|
links = compute_temporal_links({}, [])
|
||||||
|
assert links == []
|
||||||
|
|
||||||
|
def test_no_candidates_returns_empty(self):
|
||||||
|
"""Test that no candidates means no links."""
|
||||||
|
units = {"unit-1": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)}
|
||||||
|
links = compute_temporal_links(units, [])
|
||||||
|
assert links == []
|
||||||
|
|
||||||
|
def test_candidate_within_window_creates_link(self):
|
||||||
|
"""Test that candidates within time window create links."""
|
||||||
|
units = {"unit-1": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)}
|
||||||
|
candidates = [
|
||||||
|
{"id": "candidate-1", "event_date": datetime(2024, 6, 15, 10, 0, 0, tzinfo=timezone.utc)},
|
||||||
|
]
|
||||||
|
|
||||||
|
links = compute_temporal_links(units, candidates, time_window_hours=24)
|
||||||
|
|
||||||
|
assert len(links) == 1
|
||||||
|
assert links[0][0] == "unit-1"
|
||||||
|
assert links[0][1] == "candidate-1"
|
||||||
|
assert links[0][2] == "temporal"
|
||||||
|
assert links[0][4] is None
|
||||||
|
# Weight should be high since they're close (2 hours apart)
|
||||||
|
assert links[0][3] > 0.9
|
||||||
|
|
||||||
|
def test_candidate_outside_window_no_link(self):
|
||||||
|
"""Test that candidates outside time window don't create links."""
|
||||||
|
units = {"unit-1": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)}
|
||||||
|
candidates = [
|
||||||
|
{"id": "candidate-1", "event_date": datetime(2024, 6, 10, 12, 0, 0, tzinfo=timezone.utc)},
|
||||||
|
]
|
||||||
|
|
||||||
|
links = compute_temporal_links(units, candidates, time_window_hours=24)
|
||||||
|
|
||||||
|
assert len(links) == 0
|
||||||
|
|
||||||
|
def test_weight_decreases_with_distance(self):
|
||||||
|
"""Test that weight decreases as time difference increases."""
|
||||||
|
units = {"unit-1": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)}
|
||||||
|
candidates = [
|
||||||
|
{"id": "close", "event_date": datetime(2024, 6, 15, 11, 0, 0, tzinfo=timezone.utc)}, # 1 hour
|
||||||
|
{"id": "far", "event_date": datetime(2024, 6, 14, 18, 0, 0, tzinfo=timezone.utc)}, # 18 hours
|
||||||
|
]
|
||||||
|
|
||||||
|
links = compute_temporal_links(units, candidates, time_window_hours=24)
|
||||||
|
|
||||||
|
assert len(links) == 2
|
||||||
|
close_link = next(l for l in links if l[1] == "close")
|
||||||
|
far_link = next(l for l in links if l[1] == "far")
|
||||||
|
|
||||||
|
assert close_link[3] > far_link[3]
|
||||||
|
|
||||||
|
def test_max_10_links_per_unit(self):
|
||||||
|
"""Test that at most 10 links are created per unit."""
|
||||||
|
units = {"unit-1": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)}
|
||||||
|
# Create 15 candidates all within window
|
||||||
|
candidates = [
|
||||||
|
{"id": f"candidate-{i}", "event_date": datetime(2024, 6, 15, 11, 0, 0, tzinfo=timezone.utc)}
|
||||||
|
for i in range(15)
|
||||||
|
]
|
||||||
|
|
||||||
|
links = compute_temporal_links(units, candidates, time_window_hours=24)
|
||||||
|
|
||||||
|
assert len(links) == 10
|
||||||
|
|
||||||
|
def test_multiple_units_multiple_candidates(self):
|
||||||
|
"""Test with multiple units and candidates."""
|
||||||
|
units = {
|
||||||
|
"unit-1": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc),
|
||||||
|
"unit-2": datetime(2024, 6, 20, 12, 0, 0, tzinfo=timezone.utc),
|
||||||
|
}
|
||||||
|
candidates = [
|
||||||
|
{"id": "c1", "event_date": datetime(2024, 6, 15, 10, 0, 0, tzinfo=timezone.utc)}, # near unit-1
|
||||||
|
{"id": "c2", "event_date": datetime(2024, 6, 20, 10, 0, 0, tzinfo=timezone.utc)}, # near unit-2
|
||||||
|
{"id": "c3", "event_date": datetime(2024, 6, 17, 12, 0, 0, tzinfo=timezone.utc)}, # between, near neither
|
||||||
|
]
|
||||||
|
|
||||||
|
links = compute_temporal_links(units, candidates, time_window_hours=24)
|
||||||
|
|
||||||
|
# unit-1 should link to c1 only
|
||||||
|
# unit-2 should link to c2 only
|
||||||
|
unit1_links = [l for l in links if l[0] == "unit-1"]
|
||||||
|
unit2_links = [l for l in links if l[0] == "unit-2"]
|
||||||
|
|
||||||
|
assert len(unit1_links) == 1
|
||||||
|
assert unit1_links[0][1] == "c1"
|
||||||
|
|
||||||
|
assert len(unit2_links) == 1
|
||||||
|
assert unit2_links[0][1] == "c2"
|
||||||
|
|
||||||
|
def test_mixed_naive_and_aware_datetimes(self):
|
||||||
|
"""Test that mixed naive/aware datetimes work correctly."""
|
||||||
|
units = {"unit-1": datetime(2024, 6, 15, 12, 0, 0)} # naive
|
||||||
|
candidates = [
|
||||||
|
{"id": "c1", "event_date": datetime(2024, 6, 15, 10, 0, 0, tzinfo=timezone.utc)}, # aware
|
||||||
|
]
|
||||||
|
|
||||||
|
# Should not raise TypeError
|
||||||
|
links = compute_temporal_links(units, candidates, time_window_hours=24)
|
||||||
|
assert len(links) == 1
|
||||||
|
|
||||||
|
def test_overflow_near_datetime_min(self):
|
||||||
|
"""Test overflow protection when unit date is near datetime.min."""
|
||||||
|
units = {"unit-1": datetime(1, 1, 2, 0, 0, tzinfo=timezone.utc)}
|
||||||
|
candidates = [
|
||||||
|
{"id": "c1", "event_date": datetime(1, 1, 1, 12, 0, 0, tzinfo=timezone.utc)},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Should not raise OverflowError
|
||||||
|
links = compute_temporal_links(units, candidates, time_window_hours=48)
|
||||||
|
assert len(links) == 1
|
||||||
|
|
||||||
|
def test_overflow_near_datetime_max(self):
|
||||||
|
"""Test overflow protection when unit date is near datetime.max."""
|
||||||
|
units = {"unit-1": datetime(9999, 12, 30, 0, 0, tzinfo=timezone.utc)}
|
||||||
|
candidates = [
|
||||||
|
{"id": "c1", "event_date": datetime(9999, 12, 31, 12, 0, 0, tzinfo=timezone.utc)},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Should not raise OverflowError
|
||||||
|
links = compute_temporal_links(units, candidates, time_window_hours=48)
|
||||||
|
assert len(links) == 1
|
||||||
|
|
||||||
|
def test_weight_minimum_is_0_3(self):
|
||||||
|
"""Test that weight doesn't go below 0.3."""
|
||||||
|
units = {"unit-1": datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)}
|
||||||
|
candidates = [
|
||||||
|
# 23 hours apart - should be just within 24h window but low weight
|
||||||
|
{"id": "c1", "event_date": datetime(2024, 6, 14, 13, 0, 0, tzinfo=timezone.utc)},
|
||||||
|
]
|
||||||
|
|
||||||
|
links = compute_temporal_links(units, candidates, time_window_hours=24)
|
||||||
|
|
||||||
|
assert len(links) == 1
|
||||||
|
assert links[0][3] >= 0.3
|
||||||
111
hindsight-control-plane/package-lock.json
generated
111
hindsight-control-plane/package-lock.json
generated
|
|
@ -12,7 +12,9 @@
|
||||||
"@hindsight/client": "file:../hindsight-clients/typescript",
|
"@hindsight/client": "file:../hindsight-clients/typescript",
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
"@radix-ui/react-popover": "^1.1.15",
|
"@radix-ui/react-popover": "^1.1.15",
|
||||||
|
"@radix-ui/react-radio-group": "^1.3.8",
|
||||||
"@radix-ui/react-select": "^2.2.6",
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@tailwindcss/postcss": "^4.1.17",
|
"@tailwindcss/postcss": "^4.1.17",
|
||||||
|
|
@ -1586,6 +1588,52 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-label": {
|
||||||
|
"version": "2.1.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz",
|
||||||
|
"integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": "2.1.4"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": {
|
||||||
|
"version": "2.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz",
|
||||||
|
"integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-slot": "1.2.4"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-popover": {
|
"node_modules/@radix-ui/react-popover": {
|
||||||
"version": "1.1.15",
|
"version": "1.1.15",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz",
|
||||||
|
|
@ -1762,6 +1810,69 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-radio-group": {
|
||||||
|
"version": "1.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz",
|
||||||
|
"integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.3",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-direction": "1.1.1",
|
||||||
|
"@radix-ui/react-presence": "1.1.5",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-roving-focus": "1.1.11",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||||
|
"@radix-ui/react-use-previous": "1.1.1",
|
||||||
|
"@radix-ui/react-use-size": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-roving-focus": {
|
||||||
|
"version": "1.1.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz",
|
||||||
|
"integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.3",
|
||||||
|
"@radix-ui/react-collection": "1.1.7",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-direction": "1.1.1",
|
||||||
|
"@radix-ui/react-id": "1.1.1",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.2.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-select": {
|
"node_modules/@radix-ui/react-select": {
|
||||||
"version": "2.2.6",
|
"version": "2.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz",
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,9 @@
|
||||||
"@hindsight/client": "file:../hindsight-clients/typescript",
|
"@hindsight/client": "file:../hindsight-clients/typescript",
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
"@radix-ui/react-popover": "^1.1.15",
|
"@radix-ui/react-popover": "^1.1.15",
|
||||||
|
"@radix-ui/react-radio-group": "^1.3.8",
|
||||||
"@radix-ui/react-select": "^2.2.6",
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@tailwindcss/postcss": "^4.1.17",
|
"@tailwindcss/postcss": "^4.1.17",
|
||||||
|
|
|
||||||
|
|
@ -13,3 +13,31 @@ export async function GET() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const { bank_id } = body;
|
||||||
|
|
||||||
|
if (!bank_id) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'bank_id is required' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await sdk.createOrUpdateBank({
|
||||||
|
client: lowLevelClient,
|
||||||
|
path: { bank_id },
|
||||||
|
body: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json(response.data, { status: 201 });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating bank:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to create bank' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import * as React from 'react';
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import { useBank } from '@/lib/bank-context';
|
import { useBank } from '@/lib/bank-context';
|
||||||
|
import { client } from '@/lib/api';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
|
|
@ -18,19 +19,105 @@ import {
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from '@/components/ui/popover';
|
} from '@/components/ui/popover';
|
||||||
import { Check, ChevronsUpDown } from 'lucide-react';
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogFooter,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Check, ChevronsUpDown, Plus, FileText } from 'lucide-react';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
function BankSelectorInner() {
|
function BankSelectorInner() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const { currentBank, setCurrentBank, banks } = useBank();
|
const { currentBank, setCurrentBank, banks, loadBanks } = useBank();
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
|
const [createDialogOpen, setCreateDialogOpen] = React.useState(false);
|
||||||
|
const [newBankId, setNewBankId] = React.useState('');
|
||||||
|
const [isCreating, setIsCreating] = React.useState(false);
|
||||||
|
const [createError, setCreateError] = React.useState<string | null>(null);
|
||||||
|
|
||||||
|
// Document creation state
|
||||||
|
const [docDialogOpen, setDocDialogOpen] = React.useState(false);
|
||||||
|
const [docContent, setDocContent] = React.useState('');
|
||||||
|
const [docContext, setDocContext] = React.useState('');
|
||||||
|
const [docEventDate, setDocEventDate] = React.useState('');
|
||||||
|
const [docDocumentId, setDocDocumentId] = React.useState('');
|
||||||
|
const [docAsync, setDocAsync] = React.useState(false);
|
||||||
|
const [isCreatingDoc, setIsCreatingDoc] = React.useState(false);
|
||||||
|
const [docError, setDocError] = React.useState<string | null>(null);
|
||||||
|
|
||||||
const sortedBanks = React.useMemo(() => {
|
const sortedBanks = React.useMemo(() => {
|
||||||
return [...banks].sort((a, b) => a.localeCompare(b));
|
return [...banks].sort((a, b) => a.localeCompare(b));
|
||||||
}, [banks]);
|
}, [banks]);
|
||||||
|
|
||||||
|
const handleCreateBank = async () => {
|
||||||
|
if (!newBankId.trim()) return;
|
||||||
|
|
||||||
|
setIsCreating(true);
|
||||||
|
setCreateError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.createBank(newBankId.trim());
|
||||||
|
await loadBanks();
|
||||||
|
setCreateDialogOpen(false);
|
||||||
|
setNewBankId('');
|
||||||
|
// Navigate to the new bank
|
||||||
|
setCurrentBank(newBankId.trim());
|
||||||
|
router.push(`/banks/${newBankId.trim()}?view=data`);
|
||||||
|
} catch (error) {
|
||||||
|
setCreateError(error instanceof Error ? error.message : 'Failed to create bank');
|
||||||
|
} finally {
|
||||||
|
setIsCreating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateDocument = async () => {
|
||||||
|
if (!currentBank || !docContent.trim()) return;
|
||||||
|
|
||||||
|
setIsCreatingDoc(true);
|
||||||
|
setDocError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const item: any = { content: docContent };
|
||||||
|
if (docContext) item.context = docContext;
|
||||||
|
if (docEventDate) item.event_date = docEventDate;
|
||||||
|
|
||||||
|
const params: any = {
|
||||||
|
bank_id: currentBank,
|
||||||
|
items: [item],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (docDocumentId) params.document_id = docDocumentId;
|
||||||
|
|
||||||
|
if (docAsync) {
|
||||||
|
await client.retain({ ...params, async: true });
|
||||||
|
} else {
|
||||||
|
await client.retain(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset form and close dialog
|
||||||
|
setDocDialogOpen(false);
|
||||||
|
setDocContent('');
|
||||||
|
setDocContext('');
|
||||||
|
setDocEventDate('');
|
||||||
|
setDocDocumentId('');
|
||||||
|
setDocAsync(false);
|
||||||
|
|
||||||
|
// Navigate to documents view to see the new document
|
||||||
|
router.push(`/banks/${currentBank}?view=documents`);
|
||||||
|
} catch (error) {
|
||||||
|
setDocError(error instanceof Error ? error.message : 'Failed to create document');
|
||||||
|
} finally {
|
||||||
|
setIsCreatingDoc(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-card text-card-foreground px-5 py-3 border-b-4 border-primary">
|
<div className="bg-card text-card-foreground px-5 py-3 border-b-4 border-primary">
|
||||||
<div className="flex items-center gap-2.5 text-sm">
|
<div className="flex items-center gap-2.5 text-sm">
|
||||||
|
|
@ -81,6 +168,163 @@ function BankSelectorInner() {
|
||||||
</Command>
|
</Command>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-9 border-2 border-primary hover:bg-accent gap-1.5"
|
||||||
|
onClick={() => setCreateDialogOpen(true)}
|
||||||
|
title="Create new memory bank"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
<span>New Bank</span>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{currentBank && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-9 border-2 border-secondary hover:bg-secondary/20 gap-1.5"
|
||||||
|
onClick={() => setDocDialogOpen(true)}
|
||||||
|
title="Add document to current bank"
|
||||||
|
>
|
||||||
|
<FileText className="h-4 w-4" />
|
||||||
|
<span>New Document</span>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>
|
||||||
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Create New Memory Bank</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="py-4">
|
||||||
|
<Input
|
||||||
|
placeholder="Enter bank ID..."
|
||||||
|
value={newBankId}
|
||||||
|
onChange={(e) => setNewBankId(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' && !isCreating) {
|
||||||
|
handleCreateBank();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
{createError && (
|
||||||
|
<p className="text-sm text-destructive mt-2">{createError}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setCreateDialogOpen(false);
|
||||||
|
setNewBankId('');
|
||||||
|
setCreateError(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleCreateBank}
|
||||||
|
disabled={isCreating || !newBankId.trim()}
|
||||||
|
>
|
||||||
|
{isCreating ? 'Creating...' : 'Create'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog open={docDialogOpen} onOpenChange={setDocDialogOpen}>
|
||||||
|
<DialogContent className="sm:max-w-[600px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Add New Document</DialogTitle>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Add a new document to memory bank: <span className="font-semibold">{currentBank}</span>
|
||||||
|
</p>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="py-4 space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="font-bold block mb-1 text-sm">Content *</label>
|
||||||
|
<Textarea
|
||||||
|
value={docContent}
|
||||||
|
onChange={(e) => setDocContent(e.target.value)}
|
||||||
|
placeholder="Enter the document content..."
|
||||||
|
className="min-h-[150px] resize-y"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="font-bold block mb-1 text-sm">Context</label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={docContext}
|
||||||
|
onChange={(e) => setDocContext(e.target.value)}
|
||||||
|
placeholder="Optional context about this document..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="font-bold block mb-1 text-sm">Event Date</label>
|
||||||
|
<Input
|
||||||
|
type="datetime-local"
|
||||||
|
value={docEventDate}
|
||||||
|
onChange={(e) => setDocEventDate(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="font-bold block mb-1 text-sm">Document ID</label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={docDocumentId}
|
||||||
|
onChange={(e) => setDocDocumentId(e.target.value)}
|
||||||
|
placeholder="Optional document identifier..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Checkbox
|
||||||
|
id="async-doc"
|
||||||
|
checked={docAsync}
|
||||||
|
onCheckedChange={(checked) => setDocAsync(checked as boolean)}
|
||||||
|
/>
|
||||||
|
<label htmlFor="async-doc" className="text-sm cursor-pointer">
|
||||||
|
Process in background (async)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{docError && (
|
||||||
|
<p className="text-sm text-destructive">{docError}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setDocDialogOpen(false);
|
||||||
|
setDocContent('');
|
||||||
|
setDocContext('');
|
||||||
|
setDocEventDate('');
|
||||||
|
setDocDocumentId('');
|
||||||
|
setDocAsync(false);
|
||||||
|
setDocError(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleCreateDocument}
|
||||||
|
disabled={isCreatingDoc || !docContent.trim()}
|
||||||
|
>
|
||||||
|
{isCreatingDoc ? 'Adding...' : 'Add Document'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ import cytoscape from 'cytoscape';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
import { Copy, Check, X, Calendar, ZoomIn, ZoomOut, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, FileText, Layers } from 'lucide-react';
|
||||||
import { Copy, Check, X, Calendar, ZoomIn, ZoomOut, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';
|
import { MemoryDetailPanel } from './memory-detail-panel';
|
||||||
|
|
||||||
type FactType = 'world' | 'bank' | 'opinion';
|
type FactType = 'world' | 'bank' | 'opinion';
|
||||||
type ViewMode = 'graph' | 'table' | 'timeline';
|
type ViewMode = 'graph' | 'table' | 'timeline';
|
||||||
|
|
@ -31,6 +31,8 @@ export function DataView({ factType }: DataViewProps) {
|
||||||
const [selectedChunk, setSelectedChunk] = useState<any>(null);
|
const [selectedChunk, setSelectedChunk] = useState<any>(null);
|
||||||
const [loadingChunk, setLoadingChunk] = useState(false);
|
const [loadingChunk, setLoadingChunk] = useState(false);
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [selectedGraphNode, setSelectedGraphNode] = useState<any>(null);
|
||||||
|
const [selectedTableMemory, setSelectedTableMemory] = useState<any>(null);
|
||||||
const itemsPerPage = 100;
|
const itemsPerPage = 100;
|
||||||
const cyRef = useRef<any>(null);
|
const cyRef = useRef<any>(null);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
@ -195,6 +197,23 @@ export function DataView({ factType }: DataViewProps) {
|
||||||
] as any,
|
] as any,
|
||||||
layout: layouts[layout] || layouts.circle,
|
layout: layouts[layout] || layouts.circle,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add click handler for nodes
|
||||||
|
cyRef.current.on('tap', 'node', (evt: any) => {
|
||||||
|
const nodeId = evt.target.id();
|
||||||
|
// Find the corresponding table row data
|
||||||
|
const nodeData = data.table_rows?.find((row: any) => row.id === nodeId);
|
||||||
|
if (nodeData) {
|
||||||
|
setSelectedGraphNode(nodeData);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click on background to deselect
|
||||||
|
cyRef.current.on('tap', (evt: any) => {
|
||||||
|
if (evt.target === cyRef.current) {
|
||||||
|
setSelectedGraphNode(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -265,7 +284,8 @@ export function DataView({ factType }: DataViewProps) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{viewMode === 'graph' && (
|
{viewMode === 'graph' && (
|
||||||
<div className="relative">
|
<div className="flex gap-4">
|
||||||
|
<div className={`relative transition-all ${selectedGraphNode ? 'w-2/3' : 'w-full'}`}>
|
||||||
<div className="p-4 bg-card border-b-2 border-primary flex gap-4 items-center flex-wrap">
|
<div className="p-4 bg-card border-b-2 border-primary flex gap-4 items-center flex-wrap">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<label className="font-semibold text-card-foreground">Limit nodes:</label>
|
<label className="font-semibold text-card-foreground">Limit nodes:</label>
|
||||||
|
|
@ -292,6 +312,9 @@ export function DataView({ factType }: DataViewProps) {
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground ml-auto">
|
||||||
|
Click on a node to view details
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div ref={containerRef} className="w-full h-[800px] bg-background" />
|
<div ref={containerRef} className="w-full h-[800px] bg-background" />
|
||||||
<div className="absolute top-20 left-5 bg-card p-4 border-2 border-primary rounded-lg shadow-lg max-w-[250px]">
|
<div className="absolute top-20 left-5 bg-card p-4 border-2 border-primary rounded-lg shadow-lg max-w-[250px]">
|
||||||
|
|
@ -324,11 +347,32 @@ export function DataView({ factType }: DataViewProps) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Memory Detail Panel for Graph View */}
|
||||||
|
{selectedGraphNode && (
|
||||||
|
<div className="w-1/3">
|
||||||
|
<MemoryDetailPanel
|
||||||
|
memory={selectedGraphNode}
|
||||||
|
onClose={() => setSelectedGraphNode(null)}
|
||||||
|
onViewDocument={(docId) => {
|
||||||
|
viewDocument(docId);
|
||||||
|
setSelectedGraphNode(null);
|
||||||
|
setViewMode('table');
|
||||||
|
}}
|
||||||
|
onViewChunk={(chunkId) => {
|
||||||
|
viewChunk(chunkId);
|
||||||
|
setSelectedGraphNode(null);
|
||||||
|
setViewMode('table');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{viewMode === 'table' && (
|
{viewMode === 'table' && (
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<div className={`transition-all ${selectedDocument || selectedChunk ? 'w-1/2' : 'w-full'}`}>
|
<div className={`transition-all ${selectedDocument || selectedChunk || selectedTableMemory ? 'w-2/3' : 'w-full'}`}>
|
||||||
<div className="px-5 mb-4">
|
<div className="px-5 mb-4">
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
|
|
@ -338,20 +382,7 @@ export function DataView({ factType }: DataViewProps) {
|
||||||
className="max-w-2xl"
|
className="max-w-2xl"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-x-auto px-5 pb-5">
|
<div className="px-5 pb-5">
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>ID</TableHead>
|
|
||||||
<TableHead>Text</TableHead>
|
|
||||||
<TableHead>Context</TableHead>
|
|
||||||
<TableHead>Occurred</TableHead>
|
|
||||||
<TableHead>Mentioned</TableHead>
|
|
||||||
<TableHead>Entities</TableHead>
|
|
||||||
<TableHead>Document</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{data.table_rows && data.table_rows.length > 0 ? (
|
{data.table_rows && data.table_rows.length > 0 ? (
|
||||||
(() => {
|
(() => {
|
||||||
const filteredRows = data.table_rows.filter((row: any) => {
|
const filteredRows = data.table_rows.filter((row: any) => {
|
||||||
|
|
@ -368,143 +399,194 @@ export function DataView({ factType }: DataViewProps) {
|
||||||
const endIndex = startIndex + itemsPerPage;
|
const endIndex = startIndex + itemsPerPage;
|
||||||
const paginatedRows = filteredRows.slice(startIndex, endIndex);
|
const paginatedRows = filteredRows.slice(startIndex, endIndex);
|
||||||
|
|
||||||
return paginatedRows.map((row: any, idx: number) => {
|
return (
|
||||||
// Format temporal range
|
<>
|
||||||
let occurredDisplay = 'N/A';
|
<div className="grid gap-3">
|
||||||
if (row.occurred_start && row.occurred_end) {
|
{paginatedRows.map((row: any, idx: number) => {
|
||||||
const start = new Date(row.occurred_start).toLocaleString();
|
const occurredDisplay = row.occurred_start
|
||||||
const end = new Date(row.occurred_end).toLocaleString();
|
? new Date(row.occurred_start).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
|
||||||
occurredDisplay = start === end ? start : `${start} - ${end}`;
|
: null;
|
||||||
} else if (row.date) {
|
|
||||||
// Fallback to old date field
|
|
||||||
occurredDisplay = row.date;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mentionedDisplay = row.mentioned_at
|
|
||||||
? new Date(row.mentioned_at).toLocaleString()
|
|
||||||
: 'N/A';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow
|
<div
|
||||||
key={idx}
|
key={row.id || idx}
|
||||||
className={selectedDocument?.id === row.document_id ? 'bg-accent' : ''}
|
onClick={() => setSelectedTableMemory(row)}
|
||||||
|
className={`group p-4 bg-card border rounded-lg cursor-pointer transition-all hover:border-primary hover:shadow-md ${
|
||||||
|
selectedTableMemory?.id === row.id ? 'border-primary ring-2 ring-primary/20' : 'border-border'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<TableCell>
|
<div className="flex items-start gap-4">
|
||||||
<div className="flex items-center gap-2">
|
{/* Main content */}
|
||||||
<span title={row.id} className="text-muted-foreground">
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm text-foreground line-clamp-2 mb-2">
|
||||||
|
{row.text}
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted-foreground">
|
||||||
|
{occurredDisplay && (
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Calendar className="h-3 w-3" />
|
||||||
|
{occurredDisplay}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{row.context && (
|
||||||
|
<span className="truncate max-w-[200px]" title={row.context}>
|
||||||
|
{row.context}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="font-mono opacity-50" title={row.id}>
|
||||||
{row.id.substring(0, 8)}...
|
{row.id.substring(0, 8)}...
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
|
{row.entities && (
|
||||||
|
<div className="flex gap-1 mt-2 flex-wrap">
|
||||||
|
{row.entities.split(', ').slice(0, 5).map((entity: string, i: number) => (
|
||||||
|
<span key={i} className="text-[10px] px-1.5 py-0.5 rounded bg-secondary text-secondary-foreground">
|
||||||
|
{entity}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
{row.entities.split(', ').length > 5 && (
|
||||||
|
<span className="text-[10px] text-muted-foreground">
|
||||||
|
+{row.entities.split(', ').length - 5}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action buttons */}
|
||||||
|
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
{row.document_id && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-6 w-6 p-0"
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.stopPropagation();
|
||||||
|
viewDocument(row.document_id);
|
||||||
|
}}
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
title="View Document"
|
||||||
|
>
|
||||||
|
<FileText className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{row.chunk_id && (
|
||||||
|
<Button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
viewChunk(row.chunk_id);
|
||||||
|
}}
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
title="View Chunk"
|
||||||
|
>
|
||||||
|
<Layers className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
copyToClipboard(row.id);
|
copyToClipboard(row.id);
|
||||||
}}
|
}}
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
title="Copy ID"
|
||||||
>
|
>
|
||||||
{copiedId === row.id ? (
|
{copiedId === row.id ? (
|
||||||
<Check className="h-3 w-3 text-green-600" />
|
<Check className="h-4 w-4 text-green-600" />
|
||||||
) : (
|
) : (
|
||||||
<Copy className="h-3 w-3" />
|
<Copy className="h-4 w-4" />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
|
||||||
<TableCell>{row.text}</TableCell>
|
|
||||||
<TableCell>{row.context || 'N/A'}</TableCell>
|
|
||||||
<TableCell>{occurredDisplay}</TableCell>
|
|
||||||
<TableCell>{mentionedDisplay}</TableCell>
|
|
||||||
<TableCell>{row.entities || 'None'}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
{row.document_id ? (
|
|
||||||
<Button
|
|
||||||
onClick={() => viewDocument(row.document_id)}
|
|
||||||
size="sm"
|
|
||||||
variant={selectedDocument?.id === row.document_id ? 'default' : 'outline'}
|
|
||||||
>
|
|
||||||
Doc
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<span className="text-muted-foreground text-sm">-</span>
|
|
||||||
)}
|
|
||||||
{row.chunk_id ? (
|
|
||||||
<Button
|
|
||||||
onClick={() => viewChunk(row.chunk_id)}
|
|
||||||
size="sm"
|
|
||||||
variant={selectedChunk?.chunk_id === row.chunk_id ? 'default' : 'outline'}
|
|
||||||
>
|
|
||||||
Chunk
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</div>
|
||||||
</TableRow>
|
|
||||||
);
|
);
|
||||||
});
|
})}
|
||||||
})()
|
</div>
|
||||||
) : (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={7} className="text-center">
|
|
||||||
{data.table_rows ? 'No facts match your search' : 'No facts found for this agent and fact type'}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
|
|
||||||
{/* Pagination Controls */}
|
{/* Pagination Controls */}
|
||||||
{data.table_rows && data.table_rows.length > 0 && (() => {
|
{totalPages > 1 && (
|
||||||
const filteredRows = data.table_rows.filter((row: any) => {
|
<div className="flex items-center justify-between mt-4 pt-4 border-t">
|
||||||
if (!searchQuery) return true;
|
|
||||||
const query = searchQuery.toLowerCase();
|
|
||||||
return (
|
|
||||||
row.text?.toLowerCase().includes(query) ||
|
|
||||||
row.context?.toLowerCase().includes(query)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
const totalPages = Math.ceil(filteredRows.length / itemsPerPage);
|
|
||||||
|
|
||||||
if (totalPages <= 1) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-between px-5 py-4">
|
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
Showing {((currentPage - 1) * itemsPerPage) + 1} to {Math.min(currentPage * itemsPerPage, filteredRows.length)} of {filteredRows.length} results
|
Showing {startIndex + 1} to {Math.min(endIndex, filteredRows.length)} of {filteredRows.length}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex items-center gap-1">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setCurrentPage(1)}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<ChevronsLeft className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
|
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
|
||||||
disabled={currentPage === 1}
|
disabled={currentPage === 1}
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
>
|
>
|
||||||
Previous
|
<ChevronLeft className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<div className="flex items-center gap-2 px-3">
|
<span className="text-sm px-3">
|
||||||
<span className="text-sm">
|
{currentPage} / {totalPages}
|
||||||
Page {currentPage} of {totalPages}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
|
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
|
||||||
disabled={currentPage === totalPages}
|
disabled={currentPage === totalPages}
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
>
|
>
|
||||||
Next
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setCurrentPage(totalPages)}
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<ChevronsRight className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
})()}
|
})()
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-12 text-muted-foreground">
|
||||||
|
{data.table_rows ? 'No memories match your search' : 'No memories found'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Memory Detail Panel for Table View */}
|
||||||
|
{selectedTableMemory && !selectedDocument && !selectedChunk && (
|
||||||
|
<div className="w-1/3 pr-5 pb-5">
|
||||||
|
<MemoryDetailPanel
|
||||||
|
memory={selectedTableMemory}
|
||||||
|
onClose={() => setSelectedTableMemory(null)}
|
||||||
|
onViewDocument={(docId) => {
|
||||||
|
viewDocument(docId);
|
||||||
|
setSelectedTableMemory(null);
|
||||||
|
}}
|
||||||
|
onViewChunk={(chunkId) => {
|
||||||
|
viewChunk(chunkId);
|
||||||
|
setSelectedTableMemory(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Document Detail Panel */}
|
{/* Document Detail Panel */}
|
||||||
{selectedDocument && (
|
{selectedDocument && (
|
||||||
<div className="w-1/2 pr-5 pb-5">
|
<div className="w-1/3 pr-5 pb-5">
|
||||||
<div className="bg-card border-2 border-primary rounded-lg p-4 sticky top-4 max-h-[calc(100vh-120px)] overflow-y-auto">
|
<div className="bg-card border-2 border-primary rounded-lg p-4 sticky top-4 max-h-[calc(100vh-120px)] overflow-y-auto">
|
||||||
<div className="flex justify-between items-start mb-4">
|
<div className="flex justify-between items-start mb-4">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -571,7 +653,7 @@ export function DataView({ factType }: DataViewProps) {
|
||||||
|
|
||||||
{/* Chunk Detail Panel */}
|
{/* Chunk Detail Panel */}
|
||||||
{selectedChunk && (
|
{selectedChunk && (
|
||||||
<div className="w-1/2 pr-5 pb-5">
|
<div className="w-1/3 pr-5 pb-5">
|
||||||
<div className="bg-card border-2 border-primary rounded-lg p-4 sticky top-4 max-h-[calc(100vh-120px)] overflow-y-auto">
|
<div className="bg-card border-2 border-primary rounded-lg p-4 sticky top-4 max-h-[calc(100vh-120px)] overflow-y-auto">
|
||||||
<div className="flex justify-between items-start mb-4">
|
<div className="flex justify-between items-start mb-4">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -1000,87 +1082,12 @@ function TimelineView({ data, onViewDocument }: { data: any; onViewDocument: (id
|
||||||
{/* Detail Panel */}
|
{/* Detail Panel */}
|
||||||
{selectedItem && (
|
{selectedItem && (
|
||||||
<div className="w-1/3">
|
<div className="w-1/3">
|
||||||
<div className="bg-card border border-border rounded-lg p-3 sticky top-4 max-h-[600px] overflow-y-auto">
|
<MemoryDetailPanel
|
||||||
<div className="flex justify-between items-start mb-3">
|
memory={selectedItem}
|
||||||
<h3 className="text-sm font-semibold text-card-foreground">Details</h3>
|
onClose={() => setSelectedItem(null)}
|
||||||
<Button
|
onViewDocument={onViewDocument}
|
||||||
variant="ghost"
|
compact
|
||||||
size="sm"
|
/>
|
||||||
onClick={() => setSelectedItem(null)}
|
|
||||||
className="h-6 w-6 p-0"
|
|
||||||
>
|
|
||||||
<X className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="p-2 bg-muted rounded">
|
|
||||||
<div className="text-[10px] font-medium text-muted-foreground uppercase mb-0.5">Text</div>
|
|
||||||
<div className="text-xs whitespace-pre-wrap">{selectedItem.text}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{selectedItem.context && (
|
|
||||||
<div className="p-2 bg-muted rounded">
|
|
||||||
<div className="text-[10px] font-medium text-muted-foreground uppercase mb-0.5">Context</div>
|
|
||||||
<div className="text-xs">{selectedItem.context}</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
<div className="p-2 bg-muted rounded">
|
|
||||||
<div className="text-[10px] font-medium text-muted-foreground uppercase mb-0.5">Start</div>
|
|
||||||
<div className="text-xs">
|
|
||||||
{selectedItem.occurred_start
|
|
||||||
? new Date(selectedItem.occurred_start).toLocaleString('en-US', {
|
|
||||||
month: 'short', day: 'numeric', year: 'numeric',
|
|
||||||
hour: '2-digit', minute: '2-digit', hour12: false
|
|
||||||
})
|
|
||||||
: 'N/A'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="p-2 bg-muted rounded">
|
|
||||||
<div className="text-[10px] font-medium text-muted-foreground uppercase mb-0.5">End</div>
|
|
||||||
<div className="text-xs">
|
|
||||||
{selectedItem.occurred_end
|
|
||||||
? new Date(selectedItem.occurred_end).toLocaleString('en-US', {
|
|
||||||
month: 'short', day: 'numeric', year: 'numeric',
|
|
||||||
hour: '2-digit', minute: '2-digit', hour12: false
|
|
||||||
})
|
|
||||||
: 'N/A'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{selectedItem.entities && (
|
|
||||||
<div className="p-2 bg-muted rounded">
|
|
||||||
<div className="text-[10px] font-medium text-muted-foreground uppercase mb-0.5">Entities</div>
|
|
||||||
<div className="flex gap-1 flex-wrap">
|
|
||||||
{selectedItem.entities.split(', ').map((entity: string, i: number) => (
|
|
||||||
<span key={i} className="text-[10px] px-1.5 py-0.5 rounded bg-secondary text-secondary-foreground">
|
|
||||||
{entity}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="p-2 bg-muted rounded">
|
|
||||||
<div className="text-[10px] font-medium text-muted-foreground uppercase mb-0.5">ID</div>
|
|
||||||
<div className="text-[10px] font-mono text-muted-foreground truncate">{selectedItem.id}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{selectedItem.document_id && (
|
|
||||||
<Button
|
|
||||||
onClick={() => onViewDocument(selectedItem.document_id)}
|
|
||||||
className="w-full h-7 text-xs"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
View Document
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,8 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { client } from '@/lib/api';
|
import { client } from '@/lib/api';
|
||||||
import { useBank } from '@/lib/bank-context';
|
import { useBank } from '@/lib/bank-context';
|
||||||
import { ChevronDown, ChevronUp } from 'lucide-react';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||||
import { X } from 'lucide-react';
|
import { X } from 'lucide-react';
|
||||||
|
|
||||||
|
|
@ -18,16 +15,6 @@ export function DocumentsView() {
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
|
|
||||||
// Add memory form state
|
|
||||||
const [showAddMemory, setShowAddMemory] = useState(false);
|
|
||||||
const [content, setContent] = useState('');
|
|
||||||
const [context, setContext] = useState('');
|
|
||||||
const [eventDate, setEventDate] = useState('');
|
|
||||||
const [documentId, setDocumentId] = useState('');
|
|
||||||
const [async, setAsync] = useState(false);
|
|
||||||
const [submitLoading, setSubmitLoading] = useState(false);
|
|
||||||
const [submitResult, setSubmitResult] = useState<string | null>(null);
|
|
||||||
|
|
||||||
// Document view panel state
|
// Document view panel state
|
||||||
const [selectedDocument, setSelectedDocument] = useState<any>(null);
|
const [selectedDocument, setSelectedDocument] = useState<any>(null);
|
||||||
const [loadingDocument, setLoadingDocument] = useState(false);
|
const [loadingDocument, setLoadingDocument] = useState(false);
|
||||||
|
|
@ -70,50 +57,6 @@ export function DocumentsView() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const submitMemory = async () => {
|
|
||||||
if (!currentBank || !content) {
|
|
||||||
alert('Please enter content');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSubmitLoading(true);
|
|
||||||
setSubmitResult(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const item: any = { content };
|
|
||||||
if (context) item.context = context;
|
|
||||||
if (eventDate) item.event_date = eventDate;
|
|
||||||
|
|
||||||
const params: any = {
|
|
||||||
bank_id: currentBank,
|
|
||||||
items: [item],
|
|
||||||
};
|
|
||||||
|
|
||||||
if (documentId) params.document_id = documentId;
|
|
||||||
|
|
||||||
let data: any;
|
|
||||||
if (async) {
|
|
||||||
data = await client.retain({ ...params, async: true });
|
|
||||||
} else {
|
|
||||||
data = await client.retain(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
setSubmitResult(data.message as string);
|
|
||||||
setContent('');
|
|
||||||
setContext('');
|
|
||||||
setEventDate('');
|
|
||||||
setDocumentId('');
|
|
||||||
|
|
||||||
// Refresh documents list
|
|
||||||
loadDocuments();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error submitting memory:', error);
|
|
||||||
setSubmitResult('Error: ' + (error as Error).message);
|
|
||||||
} finally {
|
|
||||||
setSubmitLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Auto-load documents when component mounts
|
// Auto-load documents when component mounts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentBank) {
|
if (currentBank) {
|
||||||
|
|
@ -123,94 +66,6 @@ export function DocumentsView() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{/* Retain Memory Section */}
|
|
||||||
<div className="mb-6 bg-card rounded-lg border-2 border-primary overflow-hidden">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => setShowAddMemory(!showAddMemory)}
|
|
||||||
className="w-full flex items-center justify-between p-4 h-auto"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-lg font-semibold text-card-foreground">Retain Memory</span>
|
|
||||||
<span className="text-sm text-muted-foreground">Add new memories to this memory bank</span>
|
|
||||||
</div>
|
|
||||||
{showAddMemory ? <ChevronUp className="w-5 h-5" /> : <ChevronDown className="w-5 h-5" />}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{showAddMemory && (
|
|
||||||
<div className="p-4 border-t border-border bg-background">
|
|
||||||
<div className="max-w-3xl">
|
|
||||||
<div className="mb-4">
|
|
||||||
<label className="font-bold block mb-1 text-card-foreground">Content *</label>
|
|
||||||
<Textarea
|
|
||||||
value={content}
|
|
||||||
onChange={(e) => setContent(e.target.value)}
|
|
||||||
placeholder="Enter the memory content..."
|
|
||||||
className="min-h-[100px] resize-y"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-4">
|
|
||||||
<label className="font-bold block mb-1 text-card-foreground">Context</label>
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
value={context}
|
|
||||||
onChange={(e) => setContext(e.target.value)}
|
|
||||||
placeholder="Optional context about this memory..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
|
||||||
<div>
|
|
||||||
<label className="font-bold block mb-1 text-card-foreground">Event Date</label>
|
|
||||||
<Input
|
|
||||||
type="datetime-local"
|
|
||||||
value={eventDate}
|
|
||||||
onChange={(e) => setEventDate(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="font-bold block mb-1 text-card-foreground">Document ID</label>
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
value={documentId}
|
|
||||||
onChange={(e) => setDocumentId(e.target.value)}
|
|
||||||
placeholder="Optional document identifier..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-4">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Checkbox
|
|
||||||
id="async-docs"
|
|
||||||
checked={async}
|
|
||||||
onCheckedChange={(checked) => setAsync(checked as boolean)}
|
|
||||||
/>
|
|
||||||
<label htmlFor="async-docs" className="text-sm text-card-foreground cursor-pointer">
|
|
||||||
Async (process in background)
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={submitMemory}
|
|
||||||
disabled={submitLoading || !content}
|
|
||||||
>
|
|
||||||
{submitLoading ? 'Retaining...' : 'Retain Memory'}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{submitResult && (
|
|
||||||
<div className={`mt-4 p-3 rounded-lg border-2 text-sm ${submitResult.startsWith('Error') ? 'bg-destructive/10 border-destructive text-destructive' : 'bg-primary/10 border-primary text-primary'}`}>
|
|
||||||
{submitResult}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Documents List Section */}
|
{/* Documents List Section */}
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex items-center justify-center py-20">
|
<div className="flex items-center justify-center py-20">
|
||||||
|
|
|
||||||
161
hindsight-control-plane/src/components/memory-detail-panel.tsx
Normal file
161
hindsight-control-plane/src/components/memory-detail-panel.tsx
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Copy, Check, X } from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
interface MemoryDetailPanelProps {
|
||||||
|
memory: any;
|
||||||
|
onClose: () => void;
|
||||||
|
onViewDocument?: (documentId: string) => void;
|
||||||
|
onViewChunk?: (chunkId: string) => void;
|
||||||
|
compact?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MemoryDetailPanel({
|
||||||
|
memory,
|
||||||
|
onClose,
|
||||||
|
onViewDocument,
|
||||||
|
onViewChunk,
|
||||||
|
compact = false,
|
||||||
|
}: MemoryDetailPanelProps) {
|
||||||
|
const [copiedId, setCopiedId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const copyToClipboard = async (text: string) => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
setCopiedId(text);
|
||||||
|
setTimeout(() => setCopiedId(null), 2000);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to copy:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!memory) return null;
|
||||||
|
|
||||||
|
const padding = compact ? 'p-3' : 'p-4';
|
||||||
|
const titleSize = compact ? 'text-sm' : 'text-lg';
|
||||||
|
const labelSize = compact ? 'text-[10px]' : 'text-xs';
|
||||||
|
const textSize = compact ? 'text-xs' : 'text-sm';
|
||||||
|
const gap = compact ? 'space-y-2' : 'space-y-4';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`bg-card border-2 border-primary rounded-lg ${padding} sticky top-4 max-h-[calc(100vh-120px)] overflow-y-auto`}>
|
||||||
|
<div className="flex justify-between items-start mb-4">
|
||||||
|
<div>
|
||||||
|
<h3 className={`${titleSize} font-bold text-card-foreground`}>Memory Details</h3>
|
||||||
|
{!compact && (
|
||||||
|
<p className="text-sm text-muted-foreground">Full memory content and metadata</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={onClose}
|
||||||
|
className={compact ? 'h-6 w-6 p-0' : 'h-8 w-8 p-0'}
|
||||||
|
>
|
||||||
|
<X className={compact ? 'h-3 w-3' : 'h-4 w-4'} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={gap}>
|
||||||
|
{/* Full Text */}
|
||||||
|
<div className={`${compact ? 'p-2' : 'p-3'} bg-muted rounded-lg`}>
|
||||||
|
<div className={`${labelSize} font-bold text-muted-foreground uppercase mb-1`}>Full Text</div>
|
||||||
|
<div className={`${textSize} whitespace-pre-wrap`}>{memory.text}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Context */}
|
||||||
|
{memory.context && (
|
||||||
|
<div className={`${compact ? 'p-2' : 'p-3'} bg-muted rounded-lg`}>
|
||||||
|
<div className={`${labelSize} font-bold text-muted-foreground uppercase mb-1`}>Context</div>
|
||||||
|
<div className={textSize}>{memory.context}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Dates */}
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div className={`${compact ? 'p-2' : 'p-3'} bg-muted rounded-lg`}>
|
||||||
|
<div className={`${labelSize} font-bold text-muted-foreground uppercase mb-1`}>Occurred</div>
|
||||||
|
<div className={textSize}>
|
||||||
|
{memory.occurred_start
|
||||||
|
? new Date(memory.occurred_start).toLocaleString()
|
||||||
|
: 'N/A'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={`${compact ? 'p-2' : 'p-3'} bg-muted rounded-lg`}>
|
||||||
|
<div className={`${labelSize} font-bold text-muted-foreground uppercase mb-1`}>Mentioned</div>
|
||||||
|
<div className={textSize}>
|
||||||
|
{memory.mentioned_at
|
||||||
|
? new Date(memory.mentioned_at).toLocaleString()
|
||||||
|
: 'N/A'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Entities */}
|
||||||
|
{memory.entities && (
|
||||||
|
<div className={`${compact ? 'p-2' : 'p-3'} bg-muted rounded-lg`}>
|
||||||
|
<div className={`${labelSize} font-bold text-muted-foreground uppercase mb-2`}>Entities</div>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{memory.entities.split(', ').map((entity: string, i: number) => (
|
||||||
|
<span
|
||||||
|
key={i}
|
||||||
|
className={`${compact ? 'text-[10px] px-1.5 py-0.5' : 'text-xs px-2 py-1'} rounded bg-secondary text-secondary-foreground`}
|
||||||
|
>
|
||||||
|
{entity}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ID */}
|
||||||
|
<div className={`${compact ? 'p-2' : 'p-3'} bg-muted rounded-lg`}>
|
||||||
|
<div className={`${labelSize} font-bold text-muted-foreground uppercase mb-1`}>Memory ID</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className={`${compact ? 'text-[10px]' : 'text-sm'} font-mono break-all`}>{memory.id}</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 w-6 p-0 flex-shrink-0"
|
||||||
|
onClick={() => copyToClipboard(memory.id)}
|
||||||
|
>
|
||||||
|
{copiedId === memory.id ? (
|
||||||
|
<Check className="h-3 w-3 text-green-600" />
|
||||||
|
) : (
|
||||||
|
<Copy className="h-3 w-3" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Document/Chunk buttons */}
|
||||||
|
{(memory.document_id || memory.chunk_id) && (
|
||||||
|
<div className={`flex gap-2 ${compact ? 'pt-1' : ''}`}>
|
||||||
|
{memory.document_id && onViewDocument && (
|
||||||
|
<Button
|
||||||
|
onClick={() => onViewDocument(memory.document_id)}
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className={`flex-1 ${compact ? 'h-7 text-xs' : ''}`}
|
||||||
|
>
|
||||||
|
View Document
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{memory.chunk_id && onViewChunk && (
|
||||||
|
<Button
|
||||||
|
onClick={() => onViewChunk(memory.chunk_id)}
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className={`flex-1 ${compact ? 'h-7 text-xs' : ''}`}
|
||||||
|
>
|
||||||
|
View Chunk
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,8 @@ import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
|
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||||
import { Info } from 'lucide-react';
|
import { Info } from 'lucide-react';
|
||||||
|
|
@ -149,6 +151,7 @@ export function SearchDebugView() {
|
||||||
trace: data.trace || null,
|
trace: data.trace || null,
|
||||||
loading: false,
|
loading: false,
|
||||||
currentRetrievalFactType: defaultFactType,
|
currentRetrievalFactType: defaultFactType,
|
||||||
|
currentPhase: 'final',
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error running search:', error);
|
console.error('Error running search:', error);
|
||||||
|
|
@ -651,52 +654,43 @@ export function SearchDebugView() {
|
||||||
|
|
||||||
{/* Phase Controls */}
|
{/* Phase Controls */}
|
||||||
{pane.trace && (
|
{pane.trace && (
|
||||||
<div className="p-2.5 bg-card border-b-2 border-primary flex gap-3">
|
<div className="p-2.5 bg-card border-b-2 border-primary">
|
||||||
<label className="flex items-center gap-1.5 cursor-pointer">
|
<RadioGroup
|
||||||
<input
|
value={pane.currentPhase}
|
||||||
type="radio"
|
onValueChange={(value) => updatePane(pane.id, { currentPhase: value as Phase })}
|
||||||
name={`phase-${pane.id}`}
|
className="flex gap-3"
|
||||||
checked={pane.currentPhase === 'retrieval'}
|
>
|
||||||
onChange={() => updatePane(pane.id, { currentPhase: 'retrieval' })}
|
<div className="flex items-center gap-1.5">
|
||||||
/>
|
<RadioGroupItem value="retrieval" id={`phase-retrieval-${pane.id}`} />
|
||||||
<span className="text-xs font-bold">1. Retrieval</span>
|
<Label htmlFor={`phase-retrieval-${pane.id}`} className="text-xs font-bold cursor-pointer">
|
||||||
</label>
|
1. Retrieval
|
||||||
<label className="flex items-center gap-1.5 cursor-pointer">
|
</Label>
|
||||||
<input
|
</div>
|
||||||
type="radio"
|
<div className="flex items-center gap-1.5">
|
||||||
name={`phase-${pane.id}`}
|
<RadioGroupItem value="rrf" id={`phase-rrf-${pane.id}`} />
|
||||||
checked={pane.currentPhase === 'rrf'}
|
<Label htmlFor={`phase-rrf-${pane.id}`} className="text-xs font-bold cursor-pointer">
|
||||||
onChange={() => updatePane(pane.id, { currentPhase: 'rrf' })}
|
2. RRF Merge
|
||||||
/>
|
</Label>
|
||||||
<span className="text-xs font-bold">2. RRF Merge</span>
|
</div>
|
||||||
</label>
|
<div className="flex items-center gap-1.5">
|
||||||
<label className="flex items-center gap-1.5 cursor-pointer">
|
<RadioGroupItem value="rerank" id={`phase-rerank-${pane.id}`} />
|
||||||
<input
|
<Label htmlFor={`phase-rerank-${pane.id}`} className="text-xs font-bold cursor-pointer">
|
||||||
type="radio"
|
3. Reranking
|
||||||
name={`phase-${pane.id}`}
|
</Label>
|
||||||
checked={pane.currentPhase === 'rerank'}
|
</div>
|
||||||
onChange={() => updatePane(pane.id, { currentPhase: 'rerank' })}
|
<div className="flex items-center gap-1.5">
|
||||||
/>
|
<RadioGroupItem value="json" id={`phase-json-${pane.id}`} />
|
||||||
<span className="text-xs font-bold">3. Reranking</span>
|
<Label htmlFor={`phase-json-${pane.id}`} className="text-xs font-bold cursor-pointer">
|
||||||
</label>
|
4. Raw JSON
|
||||||
<label className="flex items-center gap-1.5 cursor-pointer">
|
</Label>
|
||||||
<input
|
</div>
|
||||||
type="radio"
|
<div className="flex items-center gap-1.5">
|
||||||
name={`phase-${pane.id}`}
|
<RadioGroupItem value="final" id={`phase-final-${pane.id}`} />
|
||||||
checked={pane.currentPhase === 'json'}
|
<Label htmlFor={`phase-final-${pane.id}`} className="text-xs font-bold cursor-pointer">
|
||||||
onChange={() => updatePane(pane.id, { currentPhase: 'json' })}
|
5. Final Results
|
||||||
/>
|
</Label>
|
||||||
<span className="text-xs font-bold">4. Raw JSON</span>
|
</div>
|
||||||
</label>
|
</RadioGroup>
|
||||||
<label className="flex items-center gap-1.5 cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name={`phase-${pane.id}`}
|
|
||||||
checked={pane.currentPhase === 'final'}
|
|
||||||
onChange={() => updatePane(pane.id, { currentPhase: 'final' })}
|
|
||||||
/>
|
|
||||||
<span className="text-xs font-bold">5. Final Results</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,142 +2,121 @@
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||||
import { XIcon } from "lucide-react"
|
import { X } from "lucide-react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
function Dialog({
|
const Dialog = DialogPrimitive.Root
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
|
||||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogTrigger({
|
const DialogTrigger = DialogPrimitive.Trigger
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
|
||||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogPortal({
|
const DialogPortal = DialogPrimitive.Portal
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
|
||||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogClose({
|
const DialogClose = DialogPrimitive.Close
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
|
||||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogOverlay({
|
const DialogOverlay = React.forwardRef<
|
||||||
className,
|
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||||
...props
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
>(({ className, ...props }, ref) => (
|
||||||
return (
|
|
||||||
<DialogPrimitive.Overlay
|
<DialogPrimitive.Overlay
|
||||||
data-slot="dialog-overlay"
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
))
|
||||||
}
|
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||||
|
|
||||||
function DialogContent({
|
const DialogContent = React.forwardRef<
|
||||||
className,
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||||
children,
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||||
showCloseButton = true,
|
>(({ className, children, ...props }, ref) => (
|
||||||
...props
|
<DialogPortal>
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
|
||||||
showCloseButton?: boolean
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<DialogPortal data-slot="dialog-portal">
|
|
||||||
<DialogOverlay />
|
<DialogOverlay />
|
||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
data-slot="dialog-content"
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
{showCloseButton && (
|
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||||
<DialogPrimitive.Close
|
<X className="h-4 w-4" />
|
||||||
data-slot="dialog-close"
|
|
||||||
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
|
||||||
>
|
|
||||||
<XIcon />
|
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
)}
|
|
||||||
</DialogPrimitive.Content>
|
</DialogPrimitive.Content>
|
||||||
</DialogPortal>
|
</DialogPortal>
|
||||||
)
|
))
|
||||||
}
|
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||||
|
|
||||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
const DialogHeader = ({
|
||||||
return (
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
<div
|
<div
|
||||||
data-slot="dialog-header"
|
|
||||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="dialog-footer"
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
DialogHeader.displayName = "DialogHeader"
|
||||||
|
|
||||||
function DialogTitle({
|
const DialogFooter = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
return (
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
DialogFooter.displayName = "DialogFooter"
|
||||||
|
|
||||||
|
const DialogTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
<DialogPrimitive.Title
|
<DialogPrimitive.Title
|
||||||
data-slot="dialog-title"
|
ref={ref}
|
||||||
className={cn("text-lg leading-none font-semibold", className)}
|
className={cn(
|
||||||
|
"text-lg font-semibold leading-none tracking-tight",
|
||||||
|
className
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
))
|
||||||
}
|
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||||
|
|
||||||
function DialogDescription({
|
const DialogDescription = React.forwardRef<
|
||||||
className,
|
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||||
...props
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
>(({ className, ...props }, ref) => (
|
||||||
return (
|
|
||||||
<DialogPrimitive.Description
|
<DialogPrimitive.Description
|
||||||
data-slot="dialog-description"
|
ref={ref}
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
))
|
||||||
}
|
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogOverlay,
|
|
||||||
DialogPortal,
|
DialogPortal,
|
||||||
DialogTitle,
|
DialogOverlay,
|
||||||
|
DialogClose,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogFooter,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
26
hindsight-control-plane/src/components/ui/label.tsx
Normal file
26
hindsight-control-plane/src/components/ui/label.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const labelVariants = cva(
|
||||||
|
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||||
|
)
|
||||||
|
|
||||||
|
const Label = React.forwardRef<
|
||||||
|
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||||
|
VariantProps<typeof labelVariants>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<LabelPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(labelVariants(), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
Label.displayName = LabelPrimitive.Root.displayName
|
||||||
|
|
||||||
|
export { Label }
|
||||||
44
hindsight-control-plane/src/components/ui/radio-group.tsx
Normal file
44
hindsight-control-plane/src/components/ui/radio-group.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
||||||
|
import { Circle } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const RadioGroup = React.forwardRef<
|
||||||
|
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<RadioGroupPrimitive.Root
|
||||||
|
className={cn("grid gap-2", className)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
|
||||||
|
|
||||||
|
const RadioGroupItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<RadioGroupPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
||||||
|
<Circle className="h-2.5 w-2.5 fill-current text-current" />
|
||||||
|
</RadioGroupPrimitive.Indicator>
|
||||||
|
</RadioGroupPrimitive.Item>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
|
||||||
|
|
||||||
|
export { RadioGroup, RadioGroupItem }
|
||||||
|
|
@ -28,6 +28,16 @@ export class ControlPlaneClient {
|
||||||
return this.fetchApi<{ banks: any[] }>('/api/banks', { cache: 'no-store' as RequestCache });
|
return this.fetchApi<{ banks: any[] }>('/api/banks', { cache: 'no-store' as RequestCache });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new bank
|
||||||
|
*/
|
||||||
|
async createBank(bankId: string) {
|
||||||
|
return this.fetchApi<{ bank_id: string }>('/api/banks', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ bank_id: bankId }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recall memories
|
* Recall memories
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -913,12 +913,12 @@ class BenchmarkRunner:
|
||||||
# Only clear on first item for shared agent_id
|
# Only clear on first item for shared agent_id
|
||||||
clear_this_agent = (i == 1)
|
clear_this_agent = (i == 1)
|
||||||
|
|
||||||
# Check if we should skip this item (filln mode)
|
# Check if we should skip this item (fill mode - skip if already in results file)
|
||||||
|
item_id = self.dataset.get_item_id(item)
|
||||||
if filln:
|
if filln:
|
||||||
has_data = await self._agent_has_data(item_agent_id)
|
if item_id in existing_item_ids:
|
||||||
if has_data:
|
console.print(f"\n[bold blue]Item {i}/{len(items)}[/bold blue] (ID: {item_id})")
|
||||||
console.print(f"\n[bold blue]Item {i}/{len(items)}[/bold blue] (ID: {self.dataset.get_item_id(item)})")
|
console.print(f" [yellow]⊘[/yellow] Skipping - already has results in output file")
|
||||||
console.print(f" [yellow]⊘[/yellow] Skipping - agent '{item_agent_id}' already has indexed data")
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
result = await self.process_single_item(
|
result = await self.process_single_item(
|
||||||
|
|
@ -981,12 +981,11 @@ class BenchmarkRunner:
|
||||||
item_id = self.dataset.get_item_id(item)
|
item_id = self.dataset.get_item_id(item)
|
||||||
item_agent_id = f"{agent_id}_{item_id}"
|
item_agent_id = f"{agent_id}_{item_id}"
|
||||||
|
|
||||||
# Check if we should skip this item (filln mode)
|
# Check if we should skip this item (fill mode - skip if already in results file)
|
||||||
if filln:
|
if filln:
|
||||||
has_data = await self._agent_has_data(item_agent_id)
|
if item_id in existing_item_ids:
|
||||||
if has_data:
|
|
||||||
console.print(f"\n[bold blue]Item {i}/{len(items)}[/bold blue] (ID: {item_id})")
|
console.print(f"\n[bold blue]Item {i}/{len(items)}[/bold blue] (ID: {item_id})")
|
||||||
console.print(f" [yellow]⊘[/yellow] Skipping - agent '{item_agent_id}' already has indexed data")
|
console.print(f" [yellow]⊘[/yellow] Skipping - already has results in output file")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Process the item
|
# Process the item
|
||||||
|
|
|
||||||
|
|
@ -551,7 +551,7 @@ if __name__ == "__main__":
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--fill",
|
"--fill",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Only process questions where the agent has no indexed data yet (for resuming interrupted runs)"
|
help="Only process questions not already in results file (for resuming interrupted runs)"
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--question-id",
|
"--question-id",
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,10 @@ Associate retained content with a document:
|
||||||
<TabItem value="python" label="Python">
|
<TabItem value="python" label="Python">
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
from hindsight_client import Hindsight
|
||||||
|
|
||||||
|
client = Hindsight(base_url="http://localhost:8888")
|
||||||
|
|
||||||
# Retain with document ID
|
# Retain with document ID
|
||||||
client.retain(
|
client.retain(
|
||||||
bank_id="my-bank",
|
bank_id="my-bank",
|
||||||
|
|
@ -40,7 +44,7 @@ client.retain(
|
||||||
# Batch retain for a document
|
# Batch retain for a document
|
||||||
client.retain_batch(
|
client.retain_batch(
|
||||||
bank_id="my-bank",
|
bank_id="my-bank",
|
||||||
contents=[
|
items=[
|
||||||
{"content": "Item 1: Product launch delayed to Q2"},
|
{"content": "Item 1: Product launch delayed to Q2"},
|
||||||
{"content": "Item 2: New hiring targets announced"},
|
{"content": "Item 2: New hiring targets announced"},
|
||||||
{"content": "Item 3: Budget approved for ML team"}
|
{"content": "Item 3: Budget approved for ML team"}
|
||||||
|
|
@ -60,24 +64,22 @@ with open("notes.txt") as f:
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem value="node" label="Node.js">
|
<TabItem value="node" label="Node.js">
|
||||||
|
|
||||||
```javascript
|
```typescript
|
||||||
|
import { HindsightClient } from '@hindsight/client';
|
||||||
|
|
||||||
|
const client = new HindsightClient({ baseUrl: 'http://localhost:8888' });
|
||||||
|
|
||||||
// Retain with document ID
|
// Retain with document ID
|
||||||
await client.retain({
|
await client.retain('my-bank', 'Alice presented the Q4 roadmap...', {
|
||||||
bankId: 'my-bank',
|
document_id: 'meeting-2024-03-15'
|
||||||
content: 'Alice presented the Q4 roadmap...',
|
|
||||||
documentId: 'meeting-2024-03-15'
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Batch retain
|
// Batch retain
|
||||||
await client.retainBatch({
|
await client.retainBatch('my-bank', [
|
||||||
bankId: 'my-bank',
|
|
||||||
contents: [
|
|
||||||
{ content: 'Item 1: Product launch delayed to Q2' },
|
{ content: 'Item 1: Product launch delayed to Q2' },
|
||||||
{ content: 'Item 2: New hiring targets announced' },
|
{ content: 'Item 2: New hiring targets announced' },
|
||||||
{ content: 'Item 3: Budget approved for ML team' }
|
{ content: 'Item 3: Budget approved for ML team' }
|
||||||
],
|
], { documentId: 'meeting-2024-03-15' });
|
||||||
documentId: 'meeting-2024-03-15'
|
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
@ -120,19 +122,15 @@ client.retain(
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem value="node" label="Node.js">
|
<TabItem value="node" label="Node.js">
|
||||||
|
|
||||||
```javascript
|
```typescript
|
||||||
// Original
|
// Original
|
||||||
await client.retain({
|
await client.retain('my-bank', 'Project deadline: March 31', {
|
||||||
bankId: 'my-bank',
|
document_id: 'project-plan'
|
||||||
content: 'Project deadline: March 31',
|
|
||||||
documentId: 'project-plan'
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update
|
// Update
|
||||||
await client.retain({
|
await client.retain('my-bank', 'Project deadline: April 15 (extended)', {
|
||||||
bankId: 'my-bank',
|
document_id: 'project-plan'
|
||||||
content: 'Project deadline: April 15 (extended)',
|
|
||||||
documentId: 'project-plan'
|
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -158,16 +156,23 @@ View all documents in a memory bank:
|
||||||
<TabItem value="python" label="Python">
|
<TabItem value="python" label="Python">
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# List all documents
|
# Using the low-level API
|
||||||
documents = client.list_documents(bank_id="my-bank")
|
from hindsight_client_api import ApiClient, Configuration
|
||||||
|
from hindsight_client_api.api import DefaultApi
|
||||||
|
|
||||||
for doc in documents:
|
config = Configuration(host="http://localhost:8888")
|
||||||
print(f"{doc['id']}: {doc['memory_count']} memories")
|
api_client = ApiClient(config)
|
||||||
print(f" Created: {doc['created_at']}")
|
api = DefaultApi(api_client)
|
||||||
print(f" Updated: {doc['updated_at']}")
|
|
||||||
|
# List all documents
|
||||||
|
response = api.list_documents(bank_id="my-bank")
|
||||||
|
|
||||||
|
for doc in response.items:
|
||||||
|
print(f"{doc.id}: {doc.memory_unit_count} memories")
|
||||||
|
print(f" Created: {doc.created_at}")
|
||||||
|
|
||||||
# With pagination
|
# With pagination
|
||||||
documents = client.list_documents(
|
response = api.list_documents(
|
||||||
bank_id="my-bank",
|
bank_id="my-bank",
|
||||||
limit=50,
|
limit=50,
|
||||||
offset=0
|
offset=0
|
||||||
|
|
@ -177,17 +182,21 @@ documents = client.list_documents(
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem value="node" label="Node.js">
|
<TabItem value="node" label="Node.js">
|
||||||
|
|
||||||
```javascript
|
```typescript
|
||||||
|
import { sdk, createClient, createConfig } from '@hindsight/client';
|
||||||
|
|
||||||
|
const apiClient = createClient(createConfig({ baseUrl: 'http://localhost:8888' }));
|
||||||
|
|
||||||
// List all documents
|
// List all documents
|
||||||
const documents = await client.listDocuments({
|
const response = await sdk.listDocuments({
|
||||||
bankId: 'my-bank'
|
client: apiClient,
|
||||||
|
path: { bank_id: 'my-bank' }
|
||||||
});
|
});
|
||||||
|
|
||||||
documents.forEach(doc => {
|
for (const doc of response.data.items) {
|
||||||
console.log(`${doc.id}: ${doc.memoryCount} memories`);
|
console.log(`${doc.id}: ${doc.memory_unit_count} memories`);
|
||||||
console.log(` Created: ${doc.createdAt}`);
|
console.log(` Created: ${doc.created_at}`);
|
||||||
console.log(` Updated: ${doc.updatedAt}`);
|
}
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
@ -206,52 +215,36 @@ hindsight documents list my-bank --limit 50
|
||||||
|
|
||||||
## Get Document Details
|
## Get Document Details
|
||||||
|
|
||||||
Retrieve a specific document with its memories:
|
Retrieve a specific document with its content:
|
||||||
|
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<TabItem value="python" label="Python">
|
<TabItem value="python" label="Python">
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Get document
|
# Get document
|
||||||
doc = client.get_document(
|
doc = api.get_document(
|
||||||
bank_id="my-bank",
|
bank_id="my-bank",
|
||||||
document_id="meeting-2024-03-15"
|
document_id="meeting-2024-03-15"
|
||||||
)
|
)
|
||||||
|
|
||||||
print(f"Document: {doc['id']}")
|
print(f"Document: {doc.id}")
|
||||||
print(f"Original text: {doc['original_text'][:200]}...")
|
print(f"Original text: {doc.original_text[:200]}...")
|
||||||
print(f"Memories: {doc['memory_count']}")
|
print(f"Memories: {doc.memory_unit_count}")
|
||||||
|
|
||||||
# Get with memories
|
|
||||||
doc = client.get_document(
|
|
||||||
bank_id="my-bank",
|
|
||||||
document_id="meeting-2024-03-15",
|
|
||||||
include_memories=True
|
|
||||||
)
|
|
||||||
|
|
||||||
for memory in doc['memories']:
|
|
||||||
print(f" - {memory['text']}")
|
|
||||||
```
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem value="node" label="Node.js">
|
<TabItem value="node" label="Node.js">
|
||||||
|
|
||||||
```javascript
|
```typescript
|
||||||
// Get document
|
// Get document
|
||||||
const doc = await client.getDocument({
|
const doc = await sdk.getDocument({
|
||||||
bankId: 'my-bank',
|
client: apiClient,
|
||||||
documentId: 'meeting-2024-03-15'
|
path: { bank_id: 'my-bank', document_id: 'meeting-2024-03-15' }
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`Document: ${doc.id}`);
|
console.log(`Document: ${doc.data.id}`);
|
||||||
console.log(`Memories: ${doc.memoryCount}`);
|
console.log(`Original text: ${doc.data.original_text.substring(0, 200)}...`);
|
||||||
|
console.log(`Memories: ${doc.data.memory_unit_count}`);
|
||||||
// Get with memories
|
|
||||||
const withMemories = await client.getDocument({
|
|
||||||
bankId: 'my-bank',
|
|
||||||
documentId: 'meeting-2024-03-15',
|
|
||||||
includeMemories: true
|
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
@ -260,9 +253,6 @@ const withMemories = await client.getDocument({
|
||||||
```bash
|
```bash
|
||||||
# Get document
|
# Get document
|
||||||
hindsight documents get my-bank meeting-2024-03-15
|
hindsight documents get my-bank meeting-2024-03-15
|
||||||
|
|
||||||
# With memories
|
|
||||||
hindsight documents get my-bank meeting-2024-03-15 --include-memories
|
|
||||||
```
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
@ -277,31 +267,31 @@ Remove a document and all its memories:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Delete document (removes all associated memories)
|
# Delete document (removes all associated memories)
|
||||||
client.delete_document(
|
api.delete_document(
|
||||||
bank_id="my-bank",
|
bank_id="my-bank",
|
||||||
document_id="old-meeting"
|
document_id="old-meeting"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Bulk delete
|
# Bulk delete
|
||||||
for doc_id in ["old-1", "old-2", "old-3"]:
|
for doc_id in ["old-1", "old-2", "old-3"]:
|
||||||
client.delete_document(bank_id="my-bank", document_id=doc_id)
|
api.delete_document(bank_id="my-bank", document_id=doc_id)
|
||||||
```
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem value="node" label="Node.js">
|
<TabItem value="node" label="Node.js">
|
||||||
|
|
||||||
```javascript
|
```typescript
|
||||||
// Delete document
|
// Delete document
|
||||||
await client.deleteDocument({
|
await sdk.deleteDocument({
|
||||||
bankId: 'my-bank',
|
client: apiClient,
|
||||||
documentId: 'old-meeting'
|
path: { bank_id: 'my-bank', document_id: 'old-meeting' }
|
||||||
});
|
});
|
||||||
|
|
||||||
// Bulk delete
|
// Bulk delete
|
||||||
for (const docId of ['old-1', 'old-2', 'old-3']) {
|
for (const docId of ['old-1', 'old-2', 'old-3']) {
|
||||||
await client.deleteDocument({
|
await sdk.deleteDocument({
|
||||||
bankId: 'my-bank',
|
client: apiClient,
|
||||||
documentId: docId
|
path: { bank_id: 'my-bank', document_id: docId }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -327,11 +317,12 @@ hindsight documents delete my-bank old-meeting --confirm
|
||||||
"id": "meeting-2024-03-15",
|
"id": "meeting-2024-03-15",
|
||||||
"bank_id": "my-bank",
|
"bank_id": "my-bank",
|
||||||
"original_text": "Alice presented the Q4 roadmap...",
|
"original_text": "Alice presented the Q4 roadmap...",
|
||||||
"content_hash": "sha256:abc123...",
|
"memory_unit_count": 12,
|
||||||
"memory_count": 12,
|
|
||||||
"created_at": "2024-03-15T14:00:00Z",
|
"created_at": "2024-03-15T14:00:00Z",
|
||||||
"updated_at": "2024-03-15T14:00:00Z",
|
"retain_params": {
|
||||||
"metadata": {}
|
"context": "team meeting",
|
||||||
|
"event_date": "2024-03-15"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -339,7 +330,12 @@ hindsight documents delete my-bank old-meeting --confirm
|
||||||
|
|
||||||
### Meeting Notes
|
### Meeting Notes
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<TabItem value="python" label="Python">
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
# Store meeting notes with date-based IDs
|
# Store meeting notes with date-based IDs
|
||||||
client.retain(
|
client.retain(
|
||||||
bank_id="team-memory",
|
bank_id="team-memory",
|
||||||
|
|
@ -348,10 +344,21 @@ client.retain(
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
### Documentation
|
### Documentation
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<TabItem value="python" label="Python">
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
# Store docs with version tracking
|
# Store docs with version tracking
|
||||||
|
docs_dir = Path("docs")
|
||||||
|
version = "1.0"
|
||||||
|
|
||||||
for file in docs_dir.glob("*.md"):
|
for file in docs_dir.glob("*.md"):
|
||||||
client.retain(
|
client.retain(
|
||||||
bank_id="docs-memory",
|
bank_id="docs-memory",
|
||||||
|
|
@ -360,8 +367,14 @@ for file in docs_dir.glob("*.md"):
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
### Conversation History
|
### Conversation History
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<TabItem value="python" label="Python">
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Store chat history with session IDs
|
# Store chat history with session IDs
|
||||||
client.retain(
|
client.retain(
|
||||||
|
|
@ -371,6 +384,9 @@ client.retain(
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
## Next Steps
|
## Next Steps
|
||||||
|
|
||||||
- [**Entities**](./entities) — Track people, places, and concepts
|
- [**Entities**](./entities) — Track people, places, and concepts
|
||||||
|
|
|
||||||
|
|
@ -17,13 +17,34 @@ Make sure you've [installed Hindsight](./installation) and understand [how retai
|
||||||
|
|
||||||
When you retain information, Hindsight automatically identifies and tracks entities:
|
When you retain information, Hindsight automatically identifies and tracks entities:
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<TabItem value="python" label="Python">
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
from hindsight_client import Hindsight
|
||||||
|
|
||||||
|
client = Hindsight(base_url="http://localhost:8888")
|
||||||
|
|
||||||
client.retain(
|
client.retain(
|
||||||
bank_id="my-bank",
|
bank_id="my-bank",
|
||||||
content="Alice works at Google in Mountain View. She specializes in TensorFlow."
|
content="Alice works at Google in Mountain View. She specializes in TensorFlow."
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem value="node" label="Node.js">
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { HindsightClient } from '@hindsight/client';
|
||||||
|
|
||||||
|
const client = new HindsightClient({ baseUrl: 'http://localhost:8888' });
|
||||||
|
|
||||||
|
await client.retain('my-bank', 'Alice works at Google in Mountain View. She specializes in TensorFlow.');
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
**Entities extracted:**
|
**Entities extracted:**
|
||||||
- **Alice** (person)
|
- **Alice** (person)
|
||||||
- **Google** (organization)
|
- **Google** (organization)
|
||||||
|
|
@ -46,14 +67,22 @@ Get all entities tracked in a memory bank:
|
||||||
<TabItem value="python" label="Python">
|
<TabItem value="python" label="Python">
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
# Using the low-level API
|
||||||
|
from hindsight_client_api import ApiClient, Configuration
|
||||||
|
from hindsight_client_api.api import DefaultApi
|
||||||
|
|
||||||
|
config = Configuration(host="http://localhost:8888")
|
||||||
|
api_client = ApiClient(config)
|
||||||
|
api = DefaultApi(api_client)
|
||||||
|
|
||||||
# List all entities
|
# List all entities
|
||||||
entities = client.list_entities(bank_id="my-bank")
|
response = api.list_entities(bank_id="my-bank")
|
||||||
|
|
||||||
for entity in entities:
|
for entity in response.items:
|
||||||
print(f"{entity['name']}: {entity['mention_count']} mentions")
|
print(f"{entity.canonical_name}: {entity.mention_count} mentions")
|
||||||
|
|
||||||
# List with filters
|
# List with pagination
|
||||||
entities = client.list_entities(
|
response = api.list_entities(
|
||||||
bank_id="my-bank",
|
bank_id="my-bank",
|
||||||
limit=50,
|
limit=50,
|
||||||
offset=0
|
offset=0
|
||||||
|
|
@ -63,21 +92,26 @@ entities = client.list_entities(
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem value="node" label="Node.js">
|
<TabItem value="node" label="Node.js">
|
||||||
|
|
||||||
```javascript
|
```typescript
|
||||||
|
import { sdk, createClient, createConfig } from '@hindsight/client';
|
||||||
|
|
||||||
|
const apiClient = createClient(createConfig({ baseUrl: 'http://localhost:8888' }));
|
||||||
|
|
||||||
// List all entities
|
// List all entities
|
||||||
const entities = await client.listEntities({
|
const response = await sdk.listEntities({
|
||||||
bankId: 'my-bank'
|
client: apiClient,
|
||||||
|
path: { bank_id: 'my-bank' }
|
||||||
});
|
});
|
||||||
|
|
||||||
entities.forEach(e => {
|
for (const entity of response.data.items) {
|
||||||
console.log(`${e.name}: ${e.mentionCount} mentions`);
|
console.log(`${entity.canonical_name}: ${entity.mention_count} mentions`);
|
||||||
});
|
}
|
||||||
|
|
||||||
// List with filters
|
// List with pagination
|
||||||
const filtered = await client.listEntities({
|
const paginated = await sdk.listEntities({
|
||||||
bankId: 'my-bank',
|
client: apiClient,
|
||||||
limit: 50,
|
path: { bank_id: 'my-bank' },
|
||||||
offset: 0
|
query: { limit: 50, offset: 0 }
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -103,58 +137,39 @@ Retrieve detailed information about a specific entity:
|
||||||
<TabItem value="python" label="Python">
|
<TabItem value="python" label="Python">
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Get entity state (observations + related facts)
|
# Get entity details with observations
|
||||||
entity = client.get_entity(
|
entity = api.get_entity(
|
||||||
bank_id="my-bank",
|
bank_id="my-bank",
|
||||||
entity_id="entity-uuid"
|
entity_id="entity-uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
print(f"Entity: {entity['name']}")
|
print(f"Entity: {entity.canonical_name}")
|
||||||
print(f"First seen: {entity['first_seen']}")
|
print(f"First seen: {entity.first_seen}")
|
||||||
print(f"Mentions: {entity['mention_count']}")
|
print(f"Mentions: {entity.mention_count}")
|
||||||
|
|
||||||
# Observations (synthesized summaries)
|
# Observations (synthesized summaries)
|
||||||
for obs in entity['observations']:
|
for obs in entity.observations:
|
||||||
print(f" - {obs['text']}")
|
print(f" - {obs.text}")
|
||||||
|
|
||||||
# Include related facts
|
|
||||||
entity = client.get_entity(
|
|
||||||
bank_id="my-bank",
|
|
||||||
entity_id="entity-uuid",
|
|
||||||
include_facts=True,
|
|
||||||
max_facts=20
|
|
||||||
)
|
|
||||||
|
|
||||||
for fact in entity['facts']:
|
|
||||||
print(f" [{fact['occurred_at']}] {fact['text']}")
|
|
||||||
```
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem value="node" label="Node.js">
|
<TabItem value="node" label="Node.js">
|
||||||
|
|
||||||
```javascript
|
```typescript
|
||||||
// Get entity state
|
// Get entity details
|
||||||
const entity = await client.getEntity({
|
const entity = await sdk.getEntity({
|
||||||
bankId: 'my-bank',
|
client: apiClient,
|
||||||
entityId: 'entity-uuid'
|
path: { bank_id: 'my-bank', entity_id: 'entity-uuid' }
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`Entity: ${entity.name}`);
|
console.log(`Entity: ${entity.data.canonical_name}`);
|
||||||
console.log(`First seen: ${entity.firstSeen}`);
|
console.log(`First seen: ${entity.data.first_seen}`);
|
||||||
console.log(`Mentions: ${entity.mentionCount}`);
|
console.log(`Mentions: ${entity.data.mention_count}`);
|
||||||
|
|
||||||
// Observations
|
// Observations
|
||||||
entity.observations.forEach(obs => {
|
for (const obs of entity.data.observations) {
|
||||||
console.log(` - ${obs.text}`);
|
console.log(` - ${obs.text}`);
|
||||||
});
|
}
|
||||||
|
|
||||||
// Include related facts
|
|
||||||
const withFacts = await client.getEntity({
|
|
||||||
bankId: 'my-bank',
|
|
||||||
entityId: 'entity-uuid',
|
|
||||||
includeFacts: true,
|
|
||||||
maxFacts: 20
|
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
@ -163,9 +178,6 @@ const withFacts = await client.getEntity({
|
||||||
```bash
|
```bash
|
||||||
# Get entity details
|
# Get entity details
|
||||||
hindsight entities get my-bank entity-uuid
|
hindsight entities get my-bank entity-uuid
|
||||||
|
|
||||||
# With related facts
|
|
||||||
hindsight entities get my-bank entity-uuid --include-facts
|
|
||||||
```
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
@ -185,50 +197,30 @@ Observations are high-level summaries automatically synthesized from multiple fa
|
||||||
|
|
||||||
Observations are generated in the background after retaining information.
|
Observations are generated in the background after retaining information.
|
||||||
|
|
||||||
## Search Entities
|
## Regenerate Observations
|
||||||
|
|
||||||
Find entities by name or related terms:
|
Force regeneration of entity observations:
|
||||||
|
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<TabItem value="python" label="Python">
|
<TabItem value="python" label="Python">
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Search by name
|
# Regenerate observations for an entity
|
||||||
entities = client.search_entities(
|
api.regenerate_entity_observations(
|
||||||
bank_id="my-bank",
|
bank_id="my-bank",
|
||||||
query="Alice"
|
entity_id="entity-uuid"
|
||||||
)
|
|
||||||
|
|
||||||
# Fuzzy matching handles variations
|
|
||||||
entities = client.search_entities(
|
|
||||||
bank_id="my-bank",
|
|
||||||
query="Alic" # Matches "Alice", "Alicia", etc.
|
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem value="node" label="Node.js">
|
<TabItem value="node" label="Node.js">
|
||||||
|
|
||||||
```javascript
|
```typescript
|
||||||
// Search by name
|
// Regenerate observations
|
||||||
const entities = await client.searchEntities({
|
await sdk.regenerateEntityObservations({
|
||||||
bankId: 'my-bank',
|
client: apiClient,
|
||||||
query: 'Alice'
|
path: { bank_id: 'my-bank', entity_id: 'entity-uuid' }
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fuzzy matching
|
|
||||||
const fuzzy = await client.searchEntities({
|
|
||||||
bankId: 'my-bank',
|
|
||||||
query: 'Alic'
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
</TabItem>
|
|
||||||
<TabItem value="cli" label="CLI">
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Search entities
|
|
||||||
hindsight entities search my-bank "Alice"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
@ -239,7 +231,6 @@ hindsight entities search my-bank "Alice"
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"id": "entity-uuid",
|
"id": "entity-uuid",
|
||||||
"name": "Alice Chen",
|
|
||||||
"canonical_name": "Alice Chen",
|
"canonical_name": "Alice Chen",
|
||||||
"first_seen": "2024-01-15T10:30:00Z",
|
"first_seen": "2024-01-15T10:30:00Z",
|
||||||
"last_seen": "2024-03-20T14:22:00Z",
|
"last_seen": "2024-03-20T14:22:00Z",
|
||||||
|
|
@ -247,7 +238,7 @@ hindsight entities search my-bank "Alice"
|
||||||
"observations": [
|
"observations": [
|
||||||
{
|
{
|
||||||
"text": "Alice is a software engineer at Google specializing in ML",
|
"text": "Alice is a software engineer at Google specializing in ML",
|
||||||
"created_at": "2024-03-20T15:00:00Z"
|
"mentioned_at": "2024-03-20T15:00:00Z"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,14 @@
|
||||||
sidebar_position: 6
|
sidebar_position: 6
|
||||||
---
|
---
|
||||||
|
|
||||||
# Memory bank Identity
|
# Memory Bank Identity
|
||||||
|
|
||||||
Configure memory bank personality, background, and behavior.
|
Configure memory bank personality, background, and behavior.
|
||||||
|
|
||||||
import Tabs from '@theme/Tabs';
|
import Tabs from '@theme/Tabs';
|
||||||
import TabItem from '@theme/TabItem';
|
import TabItem from '@theme/TabItem';
|
||||||
|
|
||||||
## Creating an Memory bank
|
## Creating a Memory Bank
|
||||||
|
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<TabItem value="python" label="Python">
|
<TabItem value="python" label="Python">
|
||||||
|
|
@ -19,8 +19,8 @@ from hindsight_client import Hindsight
|
||||||
|
|
||||||
client = Hindsight(base_url="http://localhost:8888")
|
client = Hindsight(base_url="http://localhost:8888")
|
||||||
|
|
||||||
client.create_agent(
|
client.create_bank(
|
||||||
agent_id="my-agent",
|
bank_id="my-bank",
|
||||||
name="Research Assistant",
|
name="Research Assistant",
|
||||||
background="I am a research assistant specializing in machine learning",
|
background="I am a research assistant specializing in machine learning",
|
||||||
personality={
|
personality={
|
||||||
|
|
@ -38,11 +38,11 @@ client.create_agent(
|
||||||
<TabItem value="node" label="Node.js">
|
<TabItem value="node" label="Node.js">
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { OpenAPI, ManagementService } from '@hindsight/client';
|
import { HindsightClient } from '@hindsight/client';
|
||||||
|
|
||||||
OpenAPI.BASE = 'http://localhost:8888';
|
const client = new HindsightClient({ baseUrl: 'http://localhost:8888' });
|
||||||
|
|
||||||
await ManagementService.createAgentApiAgentsAgentIdPut('my-agent', {
|
await client.createBank('my-bank', {
|
||||||
name: 'Research Assistant',
|
name: 'Research Assistant',
|
||||||
background: 'I am a research assistant specializing in machine learning',
|
background: 'I am a research assistant specializing in machine learning',
|
||||||
personality: {
|
personality: {
|
||||||
|
|
@ -61,10 +61,10 @@ await ManagementService.createAgentApiAgentsAgentIdPut('my-agent', {
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Set background
|
# Set background
|
||||||
hindsight agent background my-agent "I am a research assistant specializing in ML"
|
hindsight agent background my-bank "I am a research assistant specializing in ML"
|
||||||
|
|
||||||
# Set personality
|
# Set personality
|
||||||
hindsight memory bank personality my-agent \
|
hindsight agent personality my-bank \
|
||||||
--openness 0.8 \
|
--openness 0.8 \
|
||||||
--conscientiousness 0.7 \
|
--conscientiousness 0.7 \
|
||||||
--extraversion 0.5 \
|
--extraversion 0.5 \
|
||||||
|
|
@ -90,72 +90,83 @@ Each trait is scored 0.0 to 1.0:
|
||||||
|
|
||||||
### How Traits Affect Behavior
|
### How Traits Affect Behavior
|
||||||
|
|
||||||
**Openness** influences how the memory bank weighs new vs. established ideas:
|
**Openness** influences how the bank weighs new vs. established ideas:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# High openness agent
|
# High openness bank
|
||||||
"Let's try this new framework—it looks promising!"
|
"Let's try this new framework—it looks promising!"
|
||||||
|
|
||||||
# Low openness agent
|
# Low openness bank
|
||||||
"Let's stick with the proven solution we know works."
|
"Let's stick with the proven solution we know works."
|
||||||
```
|
```
|
||||||
|
|
||||||
**Conscientiousness** affects structure and thoroughness:
|
**Conscientiousness** affects structure and thoroughness:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# High conscientiousness agent
|
# High conscientiousness bank
|
||||||
"Here's a detailed, step-by-step analysis..."
|
"Here's a detailed, step-by-step analysis..."
|
||||||
|
|
||||||
# Low conscientiousness agent
|
# Low conscientiousness bank
|
||||||
"Quick take: this should work, let's try it."
|
"Quick take: this should work, let's try it."
|
||||||
```
|
```
|
||||||
|
|
||||||
**Extraversion** shapes collaboration preferences:
|
**Extraversion** shapes collaboration preferences:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# High extraversion agent
|
# High extraversion bank
|
||||||
"We should get the team together to discuss this."
|
"We should get the team together to discuss this."
|
||||||
|
|
||||||
# Low extraversion agent
|
# Low extraversion bank
|
||||||
"I'll analyze this independently and share my findings."
|
"I'll analyze this independently and share my findings."
|
||||||
```
|
```
|
||||||
|
|
||||||
**Agreeableness** affects how disagreements are handled:
|
**Agreeableness** affects how disagreements are handled:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# High agreeableness agent
|
# High agreeableness bank
|
||||||
"That's a valid point. Perhaps we can find a middle ground..."
|
"That's a valid point. Perhaps we can find a middle ground..."
|
||||||
|
|
||||||
# Low agreeableness agent
|
# Low agreeableness bank
|
||||||
"Actually, the data doesn't support that conclusion."
|
"Actually, the data doesn't support that conclusion."
|
||||||
```
|
```
|
||||||
|
|
||||||
**Neuroticism** influences risk assessment:
|
**Neuroticism** influences risk assessment:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# High neuroticism agent
|
# High neuroticism bank
|
||||||
"We should consider what could go wrong here..."
|
"We should consider what could go wrong here..."
|
||||||
|
|
||||||
# Low neuroticism agent
|
# Low neuroticism bank
|
||||||
"The risks seem manageable, let's proceed."
|
"The risks seem manageable, let's proceed."
|
||||||
```
|
```
|
||||||
|
|
||||||
## Background
|
## Background
|
||||||
|
|
||||||
The background is a first-person narrative providing agent context:
|
The background is a first-person narrative providing bank context:
|
||||||
|
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<TabItem value="python" label="Python">
|
<TabItem value="python" label="Python">
|
||||||
|
|
||||||
```python
|
```python
|
||||||
client.create_agent(
|
client.create_bank(
|
||||||
agent_id="financial-advisor",
|
bank_id="financial-advisor",
|
||||||
background="""I am a conservative financial advisor with 20 years of experience.
|
background="""I am a conservative financial advisor with 20 years of experience.
|
||||||
I prioritize capital preservation over aggressive growth.
|
I prioritize capital preservation over aggressive growth.
|
||||||
I have seen multiple market crashes and believe in diversification."""
|
I have seen multiple market crashes and believe in diversification."""
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem value="node" label="Node.js">
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
await client.createBank('financial-advisor', {
|
||||||
|
background: `I am a conservative financial advisor with 20 years of experience.
|
||||||
|
I prioritize capital preservation over aggressive growth.
|
||||||
|
I have seen multiple market crashes and believe in diversification.`
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
|
|
@ -164,91 +175,43 @@ Background influences:
|
||||||
- Perspective in responses
|
- Perspective in responses
|
||||||
- Opinion formation context
|
- Opinion formation context
|
||||||
|
|
||||||
### Merging Background
|
## Getting Bank Profile
|
||||||
|
|
||||||
New background information is merged intelligently:
|
|
||||||
|
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<TabItem value="python" label="Python">
|
<TabItem value="python" label="Python">
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Original background
|
# Using the low-level API
|
||||||
client.create_agent(
|
from hindsight_client_api import ApiClient, Configuration
|
||||||
agent_id="assistant",
|
from hindsight_client_api.api import DefaultApi
|
||||||
background="I am a helpful AI assistant"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add more context (merged, not replaced)
|
config = Configuration(host="http://localhost:8888")
|
||||||
client.update_background(
|
api_client = ApiClient(config)
|
||||||
agent_id="assistant",
|
api = DefaultApi(api_client)
|
||||||
background="I specialize in Python programming"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Result: "I am a helpful AI assistant. I specialize in Python programming."
|
profile = api.get_bank_profile("my-bank")
|
||||||
|
|
||||||
|
print(f"Name: {profile.name}")
|
||||||
|
print(f"Background: {profile.background}")
|
||||||
|
print(f"Personality: {profile.personality}")
|
||||||
```
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
</Tabs>
|
<TabItem value="node" label="Node.js">
|
||||||
|
|
||||||
Merging rules:
|
```typescript
|
||||||
- **Conflicts**: New overwrites old
|
const profile = await client.getBankProfile('my-bank');
|
||||||
- **Additions**: Non-conflicting info is added
|
|
||||||
- **Normalization**: "You are..." → "I am..."
|
|
||||||
|
|
||||||
## Getting Memory bank Profile
|
console.log(`Name: ${profile.name}`);
|
||||||
|
console.log(`Background: ${profile.background}`);
|
||||||
<Tabs>
|
console.log(`Personality:`, profile.personality);
|
||||||
<TabItem value="python" label="Python">
|
|
||||||
|
|
||||||
```python
|
|
||||||
profile = client.get_profile(agent_id="my-agent")
|
|
||||||
|
|
||||||
print(f"Background: {profile['background']}")
|
|
||||||
print(f"Personality: {profile['personality']}")
|
|
||||||
```
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem value="cli" label="CLI">
|
<TabItem value="cli" label="CLI">
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
hindsight memory bank profile my-agent
|
hindsight agent profile my-bank
|
||||||
```
|
|
||||||
|
|
||||||
</TabItem>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
## Updating Personality
|
|
||||||
|
|
||||||
<Tabs>
|
|
||||||
<TabItem value="python" label="Python">
|
|
||||||
|
|
||||||
```python
|
|
||||||
client.update_personality(
|
|
||||||
agent_id="my-agent",
|
|
||||||
openness=0.9,
|
|
||||||
conscientiousness=0.8
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
</TabItem>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
## Listing Memory banks
|
|
||||||
|
|
||||||
<Tabs>
|
|
||||||
<TabItem value="python" label="Python">
|
|
||||||
|
|
||||||
```python
|
|
||||||
memory banks = client.list_agents()
|
|
||||||
for agent in memory banks:
|
|
||||||
print(agent["agent_id"])
|
|
||||||
```
|
|
||||||
|
|
||||||
</TabItem>
|
|
||||||
<TabItem value="cli" label="CLI">
|
|
||||||
|
|
||||||
```bash
|
|
||||||
hindsight agent list
|
|
||||||
```
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
@ -256,7 +219,7 @@ hindsight agent list
|
||||||
|
|
||||||
## Default Values
|
## Default Values
|
||||||
|
|
||||||
If not specified, memory banks use neutral defaults:
|
If not specified, banks use neutral defaults:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
{
|
{
|
||||||
|
|
@ -287,9 +250,9 @@ Common personality configurations:
|
||||||
<TabItem value="python" label="Python">
|
<TabItem value="python" label="Python">
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Customer support agent
|
# Customer support bank
|
||||||
client.create_agent(
|
client.create_bank(
|
||||||
agent_id="support",
|
bank_id="support",
|
||||||
background="I am a friendly customer support agent",
|
background="I am a friendly customer support agent",
|
||||||
personality={
|
personality={
|
||||||
"openness": 0.5,
|
"openness": 0.5,
|
||||||
|
|
@ -301,9 +264,9 @@ client.create_agent(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Code reviewer agent
|
# Code reviewer bank
|
||||||
client.create_agent(
|
client.create_bank(
|
||||||
agent_id="reviewer",
|
bank_id="reviewer",
|
||||||
background="I am a thorough code reviewer focused on quality",
|
background="I am a thorough code reviewer focused on quality",
|
||||||
personality={
|
personality={
|
||||||
"openness": 0.4, # Prefers proven patterns
|
"openness": 0.4, # Prefers proven patterns
|
||||||
|
|
@ -316,21 +279,70 @@ client.create_agent(
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem value="node" label="Node.js">
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Customer support bank
|
||||||
|
await client.createBank('support', {
|
||||||
|
background: 'I am a friendly customer support agent',
|
||||||
|
personality: {
|
||||||
|
openness: 0.5,
|
||||||
|
conscientiousness: 0.7,
|
||||||
|
extraversion: 0.6,
|
||||||
|
agreeableness: 0.9,
|
||||||
|
neuroticism: 0.3,
|
||||||
|
bias_strength: 0.4
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Code reviewer bank
|
||||||
|
await client.createBank('reviewer', {
|
||||||
|
background: 'I am a thorough code reviewer focused on quality',
|
||||||
|
personality: {
|
||||||
|
openness: 0.4,
|
||||||
|
conscientiousness: 0.9,
|
||||||
|
extraversion: 0.3,
|
||||||
|
agreeableness: 0.4,
|
||||||
|
neuroticism: 0.5,
|
||||||
|
bias_strength: 0.6
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
## Memory bank Isolation
|
## Bank Isolation
|
||||||
|
|
||||||
Each agent has:
|
Each bank has:
|
||||||
- **Separate memories** — memory banks don't share memories
|
- **Separate memories** — banks don't share memories
|
||||||
- **Own personality** — traits are per-agent
|
- **Own personality** — traits are per-bank
|
||||||
- **Independent opinions** — formed from their own experiences
|
- **Independent opinions** — formed from their own experiences
|
||||||
|
|
||||||
```python
|
<Tabs>
|
||||||
# Store to agent A
|
<TabItem value="python" label="Python">
|
||||||
client.store(agent_id="agent-a", content="Python is great")
|
|
||||||
|
|
||||||
# Memory bank B doesn't see it
|
```python
|
||||||
results = client.search(agent_id="agent-b", query="Python")
|
# Store to bank A
|
||||||
|
client.retain(bank_id="bank-a", content="Python is great")
|
||||||
|
|
||||||
|
# Bank B doesn't see it
|
||||||
|
results = client.recall(bank_id="bank-b", query="Python")
|
||||||
# Returns empty
|
# Returns empty
|
||||||
```
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem value="node" label="Node.js">
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Store to bank A
|
||||||
|
await client.retain('bank-a', 'Python is great');
|
||||||
|
|
||||||
|
// Bank B doesn't see it
|
||||||
|
const results = await client.recall('bank-b', 'Python');
|
||||||
|
// Returns empty
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
|
||||||
|
|
@ -31,84 +31,40 @@ For large content batches, use async mode to avoid timeouts:
|
||||||
<TabItem value="python" label="Python">
|
<TabItem value="python" label="Python">
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
from hindsight_client import Hindsight
|
||||||
|
|
||||||
|
client = Hindsight(base_url="http://localhost:8888")
|
||||||
|
|
||||||
# Start async batch retain
|
# Start async batch retain
|
||||||
operation = client.retain_batch_async(
|
result = client.retain_batch(
|
||||||
bank_id="my-bank",
|
bank_id="my-bank",
|
||||||
contents=[
|
items=[
|
||||||
{"content": doc1_text},
|
{"content": doc1_text},
|
||||||
{"content": doc2_text},
|
{"content": doc2_text},
|
||||||
# ... hundreds or thousands of documents
|
# ... hundreds or thousands of documents
|
||||||
]
|
],
|
||||||
|
async_=True # Enable async mode
|
||||||
)
|
)
|
||||||
|
|
||||||
print(f"Operation ID: {operation['operation_id']}")
|
print(f"Operation ID: {result.get('operation_id')}")
|
||||||
print(f"Status: {operation['status']}") # 'pending' or 'running'
|
|
||||||
|
|
||||||
# Check status
|
|
||||||
status = client.get_operation(
|
|
||||||
bank_id="my-bank",
|
|
||||||
operation_id=operation['operation_id']
|
|
||||||
)
|
|
||||||
|
|
||||||
print(f"Status: {status['status']}") # 'pending', 'running', 'completed', 'failed'
|
|
||||||
print(f"Progress: {status['progress']}/{status['total']}")
|
|
||||||
|
|
||||||
# Wait for completion
|
|
||||||
import time
|
|
||||||
|
|
||||||
while True:
|
|
||||||
status = client.get_operation(bank_id="my-bank", operation_id=operation['operation_id'])
|
|
||||||
if status['status'] in ['completed', 'failed']:
|
|
||||||
break
|
|
||||||
print(f"Progress: {status['progress']}/{status['total']}")
|
|
||||||
time.sleep(5)
|
|
||||||
|
|
||||||
if status['status'] == 'completed':
|
|
||||||
print(f"Created {len(status['result']['memory_ids'])} memories")
|
|
||||||
```
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem value="node" label="Node.js">
|
<TabItem value="node" label="Node.js">
|
||||||
|
|
||||||
```javascript
|
```typescript
|
||||||
|
import { HindsightClient } from '@hindsight/client';
|
||||||
|
|
||||||
|
const client = new HindsightClient({ baseUrl: 'http://localhost:8888' });
|
||||||
|
|
||||||
// Start async batch retain
|
// Start async batch retain
|
||||||
const operation = await client.retainBatchAsync({
|
const result = await client.retainBatch('my-bank', [
|
||||||
bankId: 'my-bank',
|
|
||||||
contents: [
|
|
||||||
{ content: doc1Text },
|
{ content: doc1Text },
|
||||||
{ content: doc2Text },
|
{ content: doc2Text },
|
||||||
// ... hundreds or thousands of documents
|
// ... hundreds or thousands of documents
|
||||||
]
|
], { async: true });
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`Operation ID: ${operation.operationId}`);
|
console.log(`Operation ID: ${result.operation_id}`);
|
||||||
console.log(`Status: ${operation.status}`);
|
|
||||||
|
|
||||||
// Check status
|
|
||||||
const status = await client.getOperation({
|
|
||||||
bankId: 'my-bank',
|
|
||||||
operationId: operation.operationId
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`Status: ${status.status}`);
|
|
||||||
console.log(`Progress: ${status.progress}/${status.total}`);
|
|
||||||
|
|
||||||
// Wait for completion
|
|
||||||
async function waitForOperation(bankId, operationId) {
|
|
||||||
while (true) {
|
|
||||||
const status = await client.getOperation({ bankId, operationId });
|
|
||||||
if (['completed', 'failed'].includes(status.status)) {
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
console.log(`Progress: ${status.progress}/${status.total}`);
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const finalStatus = await waitForOperation('my-bank', operation.operationId);
|
|
||||||
if (finalStatus.status === 'completed') {
|
|
||||||
console.log(`Created ${finalStatus.result.memoryIds.length} memories`);
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
@ -119,12 +75,6 @@ if (finalStatus.status === 'completed') {
|
||||||
hindsight retain my-bank --files docs/*.md --async
|
hindsight retain my-bank --files docs/*.md --async
|
||||||
|
|
||||||
# Returns operation ID: op-abc123...
|
# Returns operation ID: op-abc123...
|
||||||
|
|
||||||
# Check status
|
|
||||||
hindsight operations get my-bank op-abc123
|
|
||||||
|
|
||||||
# Watch progress
|
|
||||||
hindsight operations watch my-bank op-abc123
|
|
||||||
```
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
@ -138,54 +88,45 @@ View all operations for a memory bank:
|
||||||
<TabItem value="python" label="Python">
|
<TabItem value="python" label="Python">
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
# Using the low-level API
|
||||||
|
from hindsight_client_api import ApiClient, Configuration
|
||||||
|
from hindsight_client_api.api import DefaultApi
|
||||||
|
|
||||||
|
config = Configuration(host="http://localhost:8888")
|
||||||
|
api_client = ApiClient(config)
|
||||||
|
api = DefaultApi(api_client)
|
||||||
|
|
||||||
# List all operations
|
# List all operations
|
||||||
operations = client.list_operations(bank_id="my-bank")
|
response = api.list_operations(bank_id="my-bank")
|
||||||
|
|
||||||
for op in operations:
|
for op in response.items:
|
||||||
print(f"{op['operation_id']}: {op['type']} - {op['status']}")
|
print(f"{op.id}: {op.task_type} - {op.status}")
|
||||||
if op['status'] == 'running':
|
print(f" Items: {op.items_count}")
|
||||||
print(f" Progress: {op['progress']}/{op['total']}")
|
if op.error_message:
|
||||||
|
print(f" Error: {op.error_message}")
|
||||||
# Filter by status
|
|
||||||
pending = client.list_operations(
|
|
||||||
bank_id="my-bank",
|
|
||||||
status="pending"
|
|
||||||
)
|
|
||||||
|
|
||||||
running = client.list_operations(
|
|
||||||
bank_id="my-bank",
|
|
||||||
status="running"
|
|
||||||
)
|
|
||||||
|
|
||||||
# With pagination
|
|
||||||
operations = client.list_operations(
|
|
||||||
bank_id="my-bank",
|
|
||||||
limit=50,
|
|
||||||
offset=0
|
|
||||||
)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem value="node" label="Node.js">
|
<TabItem value="node" label="Node.js">
|
||||||
|
|
||||||
```javascript
|
```typescript
|
||||||
|
import { sdk, createClient, createConfig } from '@hindsight/client';
|
||||||
|
|
||||||
|
const apiClient = createClient(createConfig({ baseUrl: 'http://localhost:8888' }));
|
||||||
|
|
||||||
// List all operations
|
// List all operations
|
||||||
const operations = await client.listOperations({
|
const response = await sdk.listOperations({
|
||||||
bankId: 'my-bank'
|
client: apiClient,
|
||||||
|
path: { bank_id: 'my-bank' }
|
||||||
});
|
});
|
||||||
|
|
||||||
operations.forEach(op => {
|
for (const op of response.data.items) {
|
||||||
console.log(`${op.operationId}: ${op.type} - ${op.status}`);
|
console.log(`${op.id}: ${op.task_type} - ${op.status}`);
|
||||||
if (op.status === 'running') {
|
console.log(` Items: ${op.items_count}`);
|
||||||
console.log(` Progress: ${op.progress}/${op.total}`);
|
if (op.error_message) {
|
||||||
|
console.log(` Error: ${op.error_message}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Filter by status
|
|
||||||
const pending = await client.listOperations({
|
|
||||||
bankId: 'my-bank',
|
|
||||||
status: 'pending'
|
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
@ -214,39 +155,42 @@ Stop a running or pending operation:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Cancel operation
|
# Cancel operation
|
||||||
client.cancel_operation(
|
api.cancel_operation(
|
||||||
bank_id="my-bank",
|
bank_id="my-bank",
|
||||||
operation_id="op-abc123"
|
operation_id="op-abc123"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Cancel all pending operations
|
# Cancel all pending operations
|
||||||
operations = client.list_operations(bank_id="my-bank", status="pending")
|
response = api.list_operations(bank_id="my-bank")
|
||||||
for op in operations:
|
for op in response.items:
|
||||||
client.cancel_operation(bank_id="my-bank", operation_id=op['operation_id'])
|
if op.status == "pending":
|
||||||
|
api.cancel_operation(bank_id="my-bank", operation_id=op.id)
|
||||||
```
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem value="node" label="Node.js">
|
<TabItem value="node" label="Node.js">
|
||||||
|
|
||||||
```javascript
|
```typescript
|
||||||
// Cancel operation
|
// Cancel operation
|
||||||
await client.cancelOperation({
|
await sdk.cancelOperation({
|
||||||
bankId: 'my-bank',
|
client: apiClient,
|
||||||
operationId: 'op-abc123'
|
path: { bank_id: 'my-bank', operation_id: 'op-abc123' }
|
||||||
});
|
});
|
||||||
|
|
||||||
// Cancel all pending
|
// Cancel all pending
|
||||||
const pending = await client.listOperations({
|
const ops = await sdk.listOperations({
|
||||||
bankId: 'my-bank',
|
client: apiClient,
|
||||||
status: 'pending'
|
path: { bank_id: 'my-bank' }
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const op of pending) {
|
for (const op of ops.data.items) {
|
||||||
await client.cancelOperation({
|
if (op.status === 'pending') {
|
||||||
bankId: 'my-bank',
|
await sdk.cancelOperation({
|
||||||
operationId: op.operationId
|
client: apiClient,
|
||||||
|
path: { bank_id: 'my-bank', operation_id: op.id }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
@ -285,17 +229,14 @@ hindsight operations cancel my-bank --all-pending
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"operation_id": "op-abc123",
|
"id": "op-abc123",
|
||||||
"bank_id": "my-bank",
|
"bank_id": "my-bank",
|
||||||
"type": "batch_retain",
|
"task_type": "batch_retain",
|
||||||
"status": "running",
|
"status": "completed",
|
||||||
"progress": 450,
|
"items_count": 1000,
|
||||||
"total": 1000,
|
"document_id": "batch-001",
|
||||||
"created_at": "2024-03-15T10:00:00Z",
|
"created_at": "2024-03-15T10:00:00Z",
|
||||||
"started_at": "2024-03-15T10:00:05Z",
|
"error_message": null
|
||||||
"completed_at": null,
|
|
||||||
"error": null,
|
|
||||||
"result": null
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -303,39 +244,70 @@ hindsight operations cancel my-bank --all-pending
|
||||||
|
|
||||||
### Polling
|
### Polling
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<TabItem value="python" label="Python">
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import time
|
import time
|
||||||
|
|
||||||
def wait_for_operation(client, bank_id, operation_id, poll_interval=5):
|
def wait_for_operations(api, bank_id, poll_interval=5):
|
||||||
|
"""Wait for all pending/running operations to complete."""
|
||||||
while True:
|
while True:
|
||||||
status = client.get_operation(bank_id=bank_id, operation_id=operation_id)
|
response = api.list_operations(bank_id=bank_id)
|
||||||
|
|
||||||
if status['status'] == 'completed':
|
pending_or_running = [
|
||||||
return status['result']
|
op for op in response.items
|
||||||
elif status['status'] == 'failed':
|
if op.status in ['pending', 'running']
|
||||||
raise Exception(f"Operation failed: {status['error']}")
|
]
|
||||||
elif status['status'] == 'cancelled':
|
|
||||||
raise Exception("Operation was cancelled")
|
if not pending_or_running:
|
||||||
|
print("All operations completed!")
|
||||||
|
break
|
||||||
|
|
||||||
|
for op in pending_or_running:
|
||||||
|
print(f" {op.id}: {op.status} ({op.items_count} items)")
|
||||||
|
|
||||||
print(f"Progress: {status['progress']}/{status['total']}")
|
|
||||||
time.sleep(poll_interval)
|
time.sleep(poll_interval)
|
||||||
|
|
||||||
# Use it
|
# Use it
|
||||||
result = wait_for_operation(client, "my-bank", op_id)
|
wait_for_operations(api, "my-bank")
|
||||||
print(f"Created {len(result['memory_ids'])} memories")
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Webhooks (Coming Soon)
|
</TabItem>
|
||||||
|
<TabItem value="node" label="Node.js">
|
||||||
|
|
||||||
```python
|
```typescript
|
||||||
# Configure webhook for operation completion
|
async function waitForOperations(apiClient: any, bankId: string, pollInterval = 5000) {
|
||||||
client.configure_webhook(
|
while (true) {
|
||||||
bank_id="my-bank",
|
const response = await sdk.listOperations({
|
||||||
url="https://myapp.com/webhooks/hindsight",
|
client: apiClient,
|
||||||
events=["operation.completed", "operation.failed"]
|
path: { bank_id: bankId }
|
||||||
)
|
});
|
||||||
|
|
||||||
|
const pendingOrRunning = response.data.items.filter(
|
||||||
|
(op: any) => ['pending', 'running'].includes(op.status)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (pendingOrRunning.length === 0) {
|
||||||
|
console.log('All operations completed!');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const op of pendingOrRunning) {
|
||||||
|
console.log(` ${op.id}: ${op.status} (${op.items_count} items)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use it
|
||||||
|
await waitForOperations(apiClient, 'my-bank');
|
||||||
```
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
## Performance Tips
|
## Performance Tips
|
||||||
|
|
||||||
**Use async for large batches:**
|
**Use async for large batches:**
|
||||||
|
|
@ -343,11 +315,11 @@ client.configure_webhook(
|
||||||
- Async: > 100 items or > 100KB
|
- Async: > 100 items or > 100KB
|
||||||
|
|
||||||
**Monitor progress:**
|
**Monitor progress:**
|
||||||
- Check `progress` / `total` fields
|
- Check `items_count` field
|
||||||
- Poll every 5-10 seconds
|
- Poll every 5-10 seconds
|
||||||
|
|
||||||
**Handle failures:**
|
**Handle failures:**
|
||||||
- Check `error` field for details
|
- Check `error_message` field for details
|
||||||
- Retry with exponential backoff
|
- Retry with exponential backoff
|
||||||
- Break large batches into smaller chunks
|
- Break large batches into smaller chunks
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,8 @@ from hindsight_client import Hindsight
|
||||||
|
|
||||||
client = Hindsight(base_url="http://localhost:8888")
|
client = Hindsight(base_url="http://localhost:8888")
|
||||||
|
|
||||||
results = client.search(
|
results = client.recall(
|
||||||
agent_id="my-agent",
|
bank_id="my-bank",
|
||||||
query="What does Alice do?"
|
query="What does Alice do?"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -32,16 +32,13 @@ for r in results:
|
||||||
<TabItem value="node" label="Node.js">
|
<TabItem value="node" label="Node.js">
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { OpenAPI, SearchService } from '@hindsight/client';
|
import { HindsightClient } from '@hindsight/client';
|
||||||
|
|
||||||
OpenAPI.BASE = 'http://localhost:8888';
|
const client = new HindsightClient({ baseUrl: 'http://localhost:8888' });
|
||||||
|
|
||||||
const results = await SearchService.searchApiSearchPost({
|
const results = await client.recall('my-bank', 'What does Alice do?');
|
||||||
agent_id: 'my-agent',
|
|
||||||
query: 'What does Alice do?'
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const r of results.results) {
|
for (const r of results) {
|
||||||
console.log(`${r.text} (score: ${r.weight})`);
|
console.log(`${r.text} (score: ${r.weight})`);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -50,7 +47,7 @@ for (const r of results.results) {
|
||||||
<TabItem value="cli" label="CLI">
|
<TabItem value="cli" label="CLI">
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
hindsight memory search my-agent "What does Alice do?"
|
hindsight memory search my-bank "What does Alice do?"
|
||||||
```
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
@ -61,27 +58,85 @@ hindsight memory search my-agent "What does Alice do?"
|
||||||
| Parameter | Type | Default | Description |
|
| Parameter | Type | Default | Description |
|
||||||
|-----------|------|---------|-------------|
|
|-----------|------|---------|-------------|
|
||||||
| `query` | string | required | Natural language query |
|
| `query` | string | required | Natural language query |
|
||||||
| `top_k` | int | 10 | Maximum results to return |
|
| `types` | list | all | Filter: `world`, `agent`, `opinion` |
|
||||||
| `budget` | Budget | MID | Budget level: LOW (100), MID (300), HIGH (600) nodes |
|
| `budget` | string | "mid" | Budget level: "low", "mid", "high" |
|
||||||
| `fact_type` | list | all | Filter: `world`, `agent`, `opinion` |
|
|
||||||
| `max_tokens` | int | 4096 | Token budget for results |
|
| `max_tokens` | int | 4096 | Token budget for results |
|
||||||
|
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<TabItem value="python" label="Python">
|
<TabItem value="python" label="Python">
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from hindsight_api.engine.memory_engine import Budget
|
|
||||||
|
|
||||||
results = client.recall(
|
results = client.recall(
|
||||||
bank_id="my-agent",
|
bank_id="my-bank",
|
||||||
query="What does Alice do?",
|
query="What does Alice do?",
|
||||||
top_k=20,
|
types=["world", "agent"],
|
||||||
budget=Budget.HIGH,
|
budget="high",
|
||||||
fact_type=["world", "agent"],
|
|
||||||
max_tokens=8000
|
max_tokens=8000
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem value="node" label="Node.js">
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const results = await client.recall('my-bank', 'What does Alice do?', {
|
||||||
|
budget: 'high',
|
||||||
|
maxTokens: 8000
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
## Full-Featured Search
|
||||||
|
|
||||||
|
For more control, use the full-featured recall method:
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<TabItem value="python" label="Python">
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Full response with trace info
|
||||||
|
response = client.recall_memories(
|
||||||
|
bank_id="my-bank",
|
||||||
|
query="What does Alice do?",
|
||||||
|
types=["world", "agent"],
|
||||||
|
budget="high",
|
||||||
|
max_tokens=8000,
|
||||||
|
trace=True,
|
||||||
|
include_entities=True,
|
||||||
|
max_entity_tokens=500
|
||||||
|
)
|
||||||
|
|
||||||
|
# Access results
|
||||||
|
for r in response["results"]:
|
||||||
|
print(f"{r['text']} (score: {r['weight']:.2f})")
|
||||||
|
|
||||||
|
# Access entity observations (if include_entities=True)
|
||||||
|
if "entities" in response:
|
||||||
|
for entity in response["entities"]:
|
||||||
|
print(f"Entity: {entity['name']}")
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem value="node" label="Node.js">
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Full response with trace info
|
||||||
|
const response = await client.recallMemories('my-bank', {
|
||||||
|
query: 'What does Alice do?',
|
||||||
|
types: ['world', 'agent'],
|
||||||
|
budget: 'high',
|
||||||
|
maxTokens: 8000,
|
||||||
|
trace: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Access results
|
||||||
|
for (const r of response.results) {
|
||||||
|
console.log(`${r.text} (score: ${r.weight})`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
|
|
@ -94,17 +149,17 @@ Hindsight automatically detects time expressions and activates temporal search:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# These queries activate temporal-graph retrieval
|
# These queries activate temporal-graph retrieval
|
||||||
results = client.search(agent_id="my-agent", query="What did Alice do last spring?")
|
results = client.recall(bank_id="my-bank", query="What did Alice do last spring?")
|
||||||
results = client.search(agent_id="my-agent", query="What happened in June?")
|
results = client.recall(bank_id="my-bank", query="What happened in June?")
|
||||||
results = client.search(agent_id="my-agent", query="Events from last year")
|
results = client.recall(bank_id="my-bank", query="Events from last year")
|
||||||
```
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem value="cli" label="CLI">
|
<TabItem value="cli" label="CLI">
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
hindsight memory search my-agent "What did Alice do last spring?"
|
hindsight memory search my-bank "What did Alice do last spring?"
|
||||||
hindsight memory search my-agent "What happened between March and May?"
|
hindsight memory search my-bank "What happened between March and May?"
|
||||||
```
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
@ -129,31 +184,31 @@ Search specific memory networks:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Only world facts (objective information)
|
# Only world facts (objective information)
|
||||||
world_facts = client.search_memories(
|
world_facts = client.recall(
|
||||||
agent_id="my-agent",
|
bank_id="my-bank",
|
||||||
query="Where does Alice work?",
|
query="Where does Alice work?",
|
||||||
fact_type=["world"]
|
types=["world"]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Only agent facts (memory bank's own experiences)
|
# Only agent facts (memory bank's own experiences)
|
||||||
agent_facts = client.search_memories(
|
agent_facts = client.recall(
|
||||||
agent_id="my-agent",
|
bank_id="my-bank",
|
||||||
query="What have I recommended?",
|
query="What have I recommended?",
|
||||||
fact_type=["agent"]
|
types=["agent"]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Only opinions (formed beliefs)
|
# Only opinions (formed beliefs)
|
||||||
opinions = client.search_memories(
|
opinions = client.recall(
|
||||||
agent_id="my-agent",
|
bank_id="my-bank",
|
||||||
query="What do I think about Python?",
|
query="What do I think about Python?",
|
||||||
fact_type=["opinion"]
|
types=["opinion"]
|
||||||
)
|
)
|
||||||
|
|
||||||
# World and agent facts (exclude opinions)
|
# World and agent facts (exclude opinions)
|
||||||
facts = client.search_memories(
|
facts = client.recall(
|
||||||
agent_id="my-agent",
|
bank_id="my-bank",
|
||||||
query="What happened?",
|
query="What happened?",
|
||||||
fact_type=["world", "agent"]
|
types=["world", "agent"]
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -161,8 +216,8 @@ facts = client.search_memories(
|
||||||
<TabItem value="cli" label="CLI">
|
<TabItem value="cli" label="CLI">
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
hindsight memory search my-agent "Python" --fact-type opinion
|
hindsight memory search my-bank "Python" --fact-type opinion
|
||||||
hindsight memory search my-agent "Alice" --fact-type world,agent
|
hindsight memory search my-bank "Alice" --fact-type world,agent
|
||||||
```
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
@ -225,16 +280,31 @@ graph LR
|
||||||
|
|
||||||
The `budget` parameter controls graph traversal depth:
|
The `budget` parameter controls graph traversal depth:
|
||||||
|
|
||||||
- **Budget.LOW (100 nodes)**: Fast, shallow search — good for simple lookups
|
- **"low" (100 nodes)**: Fast, shallow search — good for simple lookups
|
||||||
- **Budget.MID (300 nodes)**: Balanced — default for most queries
|
- **"mid" (300 nodes)**: Balanced — default for most queries
|
||||||
- **Budget.HIGH (600 nodes)**: Deep exploration — finds indirect connections
|
- **"high" (600 nodes)**: Deep exploration — finds indirect connections
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<TabItem value="python" label="Python">
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from hindsight_api.engine.memory_engine import Budget
|
|
||||||
|
|
||||||
# Quick lookup
|
# Quick lookup
|
||||||
results = client.recall(bank_id="my-agent", query="Alice's email", budget=Budget.LOW)
|
results = client.recall(bank_id="my-bank", query="Alice's email", budget="low")
|
||||||
|
|
||||||
# Deep exploration
|
# Deep exploration
|
||||||
results = client.recall(bank_id="my-agent", query="How are Alice and Bob connected?", budget=Budget.HIGH)
|
results = client.recall(bank_id="my-bank", query="How are Alice and Bob connected?", budget="high")
|
||||||
```
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem value="node" label="Node.js">
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Quick lookup
|
||||||
|
const results = await client.recall('my-bank', "Alice's email", { budget: 'low' });
|
||||||
|
|
||||||
|
// Deep exploration
|
||||||
|
const deep = await client.recall('my-bank', 'How are Alice and Bob connected?', { budget: 'high' });
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
sidebar_position: 3
|
sidebar_position: 3
|
||||||
---
|
---
|
||||||
|
|
||||||
# Think
|
# Reflect
|
||||||
|
|
||||||
Generate personality-aware responses using retrieved memories.
|
Generate personality-aware responses using retrieved memories.
|
||||||
|
|
||||||
|
|
@ -19,38 +19,35 @@ from hindsight_client import Hindsight
|
||||||
|
|
||||||
client = Hindsight(base_url="http://localhost:8888")
|
client = Hindsight(base_url="http://localhost:8888")
|
||||||
|
|
||||||
answer = client.think(
|
response = client.reflect(
|
||||||
agent_id="my-agent",
|
bank_id="my-bank",
|
||||||
query="What should I know about Alice?"
|
query="What should I know about Alice?"
|
||||||
)
|
)
|
||||||
|
|
||||||
print(answer["text"])
|
print(response["answer"])
|
||||||
```
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem value="node" label="Node.js">
|
<TabItem value="node" label="Node.js">
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { OpenAPI, ReasoningService } from '@hindsight/client';
|
import { HindsightClient } from '@hindsight/client';
|
||||||
|
|
||||||
OpenAPI.BASE = 'http://localhost:8888';
|
const client = new HindsightClient({ baseUrl: 'http://localhost:8888' });
|
||||||
|
|
||||||
const response = await ReasoningService.thinkApiThinkPost({
|
const response = await client.reflect('my-bank', 'What should I know about Alice?');
|
||||||
agent_id: 'my-agent',
|
|
||||||
query: 'What should I know about Alice?'
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(response.text);
|
console.log(response.answer);
|
||||||
```
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem value="cli" label="CLI">
|
<TabItem value="cli" label="CLI">
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
hindsight memory think my-agent "What should I know about Alice?"
|
hindsight memory think my-bank "What should I know about Alice?"
|
||||||
|
|
||||||
# Verbose output shows reasoning and sources
|
# Verbose output shows reasoning and sources
|
||||||
hindsight memory think my-agent "What should I know about Alice?" -v
|
hindsight memory think my-bank "What should I know about Alice?" -v
|
||||||
```
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
@ -60,26 +57,21 @@ hindsight memory think my-agent "What should I know about Alice?" -v
|
||||||
|
|
||||||
```python
|
```python
|
||||||
{
|
{
|
||||||
"text": "Alice is a software engineer at Google who joined last year...",
|
"answer": "Alice is a software engineer at Google who joined last year...",
|
||||||
"based_on": {
|
"facts_used": [
|
||||||
"world": [
|
{"text": "Alice works at Google", "weight": 0.95, "id": "..."},
|
||||||
{"text": "Alice works at Google", "weight": 0.95, "id": "..."}
|
|
||||||
],
|
|
||||||
"agent": [],
|
|
||||||
"opinion": [
|
|
||||||
{"text": "Alice is very competent", "weight": 0.82, "id": "..."}
|
{"text": "Alice is very competent", "weight": 0.82, "id": "..."}
|
||||||
]
|
],
|
||||||
},
|
|
||||||
"new_opinions": [
|
"new_opinions": [
|
||||||
{"text": "Alice would be good for the ML project", "confidence": 0.75}
|
{"text": "Alice would be good for the ML project", "id": "..."}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
| Field | Description |
|
| Field | Description |
|
||||||
|-------|-------------|
|
|-------|-------------|
|
||||||
| `text` | Generated response |
|
| `answer` | Generated response |
|
||||||
| `based_on` | Memories used, grouped by type |
|
| `facts_used` | Memories used in generation |
|
||||||
| `new_opinions` | New opinions formed during reasoning |
|
| `new_opinions` | New opinions formed during reasoning |
|
||||||
|
|
||||||
## Parameters
|
## Parameters
|
||||||
|
|
@ -87,26 +79,35 @@ hindsight memory think my-agent "What should I know about Alice?" -v
|
||||||
| Parameter | Type | Default | Description |
|
| Parameter | Type | Default | Description |
|
||||||
|-----------|------|---------|-------------|
|
|-----------|------|---------|-------------|
|
||||||
| `query` | string | required | Question or prompt |
|
| `query` | string | required | Question or prompt |
|
||||||
| `budget` | Budget | LOW | Budget level: LOW (100), MID (300), HIGH (600) nodes |
|
| `budget` | string | "low" | Budget level: "low", "mid", "high" |
|
||||||
| `top_k` | int | 10 | Max memories to retrieve |
|
| `context` | string | None | Additional context for the query |
|
||||||
|
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<TabItem value="python" label="Python">
|
<TabItem value="python" label="Python">
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from hindsight_api.engine.memory_engine import Budget
|
response = client.reflect(
|
||||||
|
bank_id="my-bank",
|
||||||
answer = client.reflect(
|
|
||||||
bank_id="my-agent",
|
|
||||||
query="What do you think about remote work?",
|
query="What do you think about remote work?",
|
||||||
budget=Budget.MID
|
budget="mid",
|
||||||
|
context="We're considering a hybrid work policy"
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem value="node" label="Node.js">
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const response = await client.reflect('my-bank', 'What do you think about remote work?', {
|
||||||
|
budget: 'mid',
|
||||||
|
context: "We're considering a hybrid work policy"
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
## What Think Does
|
## What Reflect Does
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
sequenceDiagram
|
sequenceDiagram
|
||||||
|
|
@ -115,10 +116,10 @@ sequenceDiagram
|
||||||
participant M as Memory Store
|
participant M as Memory Store
|
||||||
participant L as LLM
|
participant L as LLM
|
||||||
|
|
||||||
C->>A: think("What about Alice?")
|
C->>A: reflect("What about Alice?")
|
||||||
A->>M: Search all networks
|
A->>M: Search all networks
|
||||||
M-->>A: World + Memory bank + Opinion facts
|
M-->>A: World + Bank + Opinion facts
|
||||||
A->>A: Load memory bank personality
|
A->>A: Load bank personality
|
||||||
A->>L: Generate with personality context
|
A->>L: Generate with personality context
|
||||||
L-->>A: Response + new opinions
|
L-->>A: Response + new opinions
|
||||||
A->>M: Store new opinions
|
A->>M: Store new opinions
|
||||||
|
|
@ -126,28 +127,28 @@ sequenceDiagram
|
||||||
```
|
```
|
||||||
|
|
||||||
1. **Retrieves** relevant memories from all three networks
|
1. **Retrieves** relevant memories from all three networks
|
||||||
2. **Loads** memory bank personality (Big Five traits + background)
|
2. **Loads** bank personality (Big Five traits + background)
|
||||||
3. **Generates** response influenced by personality
|
3. **Generates** response influenced by personality
|
||||||
4. **Forms opinions** if the query warrants it
|
4. **Forms opinions** if the query warrants it
|
||||||
5. **Returns** response with sources and any new opinions
|
5. **Returns** response with sources and any new opinions
|
||||||
|
|
||||||
## Opinion Formation
|
## Opinion Formation
|
||||||
|
|
||||||
Think can form new opinions based on evidence:
|
Reflect can form new opinions based on evidence:
|
||||||
|
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<TabItem value="python" label="Python">
|
<TabItem value="python" label="Python">
|
||||||
|
|
||||||
```python
|
```python
|
||||||
answer = client.think(
|
response = client.reflect(
|
||||||
agent_id="my-agent",
|
bank_id="my-bank",
|
||||||
query="What do you think about Python vs JavaScript for data science?"
|
query="What do you think about Python vs JavaScript for data science?"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Response might include:
|
# Response might include:
|
||||||
# text: "Based on what I know about data science workflows..."
|
# answer: "Based on what I know about data science workflows..."
|
||||||
# new_opinions: [
|
# new_opinions: [
|
||||||
# {"text": "Python is better for data science", "confidence": 0.85}
|
# {"text": "Python is better for data science", "id": "..."}
|
||||||
# ]
|
# ]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -158,9 +159,9 @@ New opinions are automatically stored and influence future responses.
|
||||||
|
|
||||||
## Personality Influence
|
## Personality Influence
|
||||||
|
|
||||||
The memory bank's personality affects Think responses:
|
The bank's personality affects reflect responses:
|
||||||
|
|
||||||
| Trait | Effect on Think |
|
| Trait | Effect on Reflect |
|
||||||
|-------|-----------------|
|
|-------|-----------------|
|
||||||
| High **Openness** | More willing to consider new ideas |
|
| High **Openness** | More willing to consider new ideas |
|
||||||
| High **Conscientiousness** | More structured, methodical responses |
|
| High **Conscientiousness** | More structured, methodical responses |
|
||||||
|
|
@ -168,10 +169,13 @@ The memory bank's personality affects Think responses:
|
||||||
| High **Agreeableness** | More diplomatic, harmony-seeking |
|
| High **Agreeableness** | More diplomatic, harmony-seeking |
|
||||||
| High **Neuroticism** | More risk-aware, cautious |
|
| High **Neuroticism** | More risk-aware, cautious |
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<TabItem value="python" label="Python">
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Create a memory bank with specific personality
|
# Create a bank with specific personality
|
||||||
client.create_agent(
|
client.create_bank(
|
||||||
agent_id="cautious-advisor",
|
bank_id="cautious-advisor",
|
||||||
background="I am a risk-aware financial advisor",
|
background="I am a risk-aware financial advisor",
|
||||||
personality={
|
personality={
|
||||||
"openness": 0.3,
|
"openness": 0.3,
|
||||||
|
|
@ -181,28 +185,69 @@ client.create_agent(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Think responses will reflect this personality
|
# Reflect responses will reflect this personality
|
||||||
answer = client.think(
|
response = client.reflect(
|
||||||
agent_id="cautious-advisor",
|
bank_id="cautious-advisor",
|
||||||
query="Should I invest in crypto?"
|
query="Should I invest in crypto?"
|
||||||
)
|
)
|
||||||
# Response will likely emphasize risks and caution
|
# Response will likely emphasize risks and caution
|
||||||
```
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem value="node" label="Node.js">
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Create a bank with specific personality
|
||||||
|
await client.createBank('cautious-advisor', {
|
||||||
|
background: 'I am a risk-aware financial advisor',
|
||||||
|
personality: {
|
||||||
|
openness: 0.3,
|
||||||
|
conscientiousness: 0.9,
|
||||||
|
neuroticism: 0.8,
|
||||||
|
bias_strength: 0.7
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reflect responses will reflect this personality
|
||||||
|
const response = await client.reflect('cautious-advisor', 'Should I invest in crypto?');
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
## Using Sources
|
## Using Sources
|
||||||
|
|
||||||
The `based_on` field shows which memories informed the response:
|
The `facts_used` field shows which memories informed the response:
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<TabItem value="python" label="Python">
|
||||||
|
|
||||||
```python
|
```python
|
||||||
answer = client.think(agent_id="my-agent", query="Tell me about Alice")
|
response = client.reflect(bank_id="my-bank", query="Tell me about Alice")
|
||||||
|
|
||||||
print("Response:", answer["text"])
|
print("Response:", response["answer"])
|
||||||
print("\nBased on:")
|
print("\nBased on:")
|
||||||
for fact in answer["based_on"]["world"]:
|
for fact in response.get("facts_used", []):
|
||||||
print(f" - {fact['text']} (relevance: {fact['weight']:.2f})")
|
print(f" - {fact['text']} (relevance: {fact['weight']:.2f})")
|
||||||
```
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem value="node" label="Node.js">
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const response = await client.reflect('my-bank', 'Tell me about Alice');
|
||||||
|
|
||||||
|
console.log('Response:', response.answer);
|
||||||
|
console.log('\nBased on:');
|
||||||
|
for (const fact of response.facts_used || []) {
|
||||||
|
console.log(` - ${fact.text} (relevance: ${fact.weight.toFixed(2)})`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
This enables:
|
This enables:
|
||||||
- **Transparency** — users see why the memory bank said something
|
- **Transparency** — users see why the bank said something
|
||||||
- **Verification** — check if the response is grounded in facts
|
- **Verification** — check if the response is grounded in facts
|
||||||
- **Debugging** — understand retrieval quality
|
- **Debugging** — understand retrieval quality
|
||||||
|
|
|
||||||
|
|
@ -45,8 +45,8 @@ from hindsight_client import Hindsight
|
||||||
|
|
||||||
client = Hindsight(base_url="http://localhost:8888")
|
client = Hindsight(base_url="http://localhost:8888")
|
||||||
|
|
||||||
client.store(
|
client.retain(
|
||||||
agent_id="my-agent",
|
bank_id="my-bank",
|
||||||
content="Alice works at Google as a software engineer"
|
content="Alice works at Google as a software engineer"
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
@ -55,21 +55,18 @@ client.store(
|
||||||
<TabItem value="node" label="Node.js">
|
<TabItem value="node" label="Node.js">
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { OpenAPI, MemoryStorageService } from '@hindsight/client';
|
import { HindsightClient } from '@hindsight/client';
|
||||||
|
|
||||||
OpenAPI.BASE = 'http://localhost:8888';
|
const client = new HindsightClient({ baseUrl: 'http://localhost:8888' });
|
||||||
|
|
||||||
await MemoryStorageService.putApiPutPost({
|
await client.retain('my-bank', 'Alice works at Google as a software engineer');
|
||||||
agent_id: 'my-agent',
|
|
||||||
content: 'Alice works at Google as a software engineer'
|
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem value="cli" label="CLI">
|
<TabItem value="cli" label="CLI">
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
hindsight memory put my-agent "Alice works at Google as a software engineer"
|
hindsight memory put my-bank "Alice works at Google as a software engineer"
|
||||||
```
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
@ -83,19 +80,29 @@ Add context and event dates for better retrieval:
|
||||||
<TabItem value="python" label="Python">
|
<TabItem value="python" label="Python">
|
||||||
|
|
||||||
```python
|
```python
|
||||||
client.store(
|
client.retain(
|
||||||
agent_id="my-agent",
|
bank_id="my-bank",
|
||||||
content="Alice got promoted to senior engineer",
|
content="Alice got promoted to senior engineer",
|
||||||
context="career update",
|
context="career update",
|
||||||
event_date="2024-03-15T10:00:00Z"
|
timestamp="2024-03-15T10:00:00Z"
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem value="node" label="Node.js">
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
await client.retain('my-bank', 'Alice got promoted to senior engineer', {
|
||||||
|
context: 'career update',
|
||||||
|
timestamp: '2024-03-15T10:00:00Z'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem value="cli" label="CLI">
|
<TabItem value="cli" label="CLI">
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
hindsight memory put my-agent "Alice got promoted" \
|
hindsight memory put my-bank "Alice got promoted" \
|
||||||
--context "career update" \
|
--context "career update" \
|
||||||
--event-date "2024-03-15"
|
--event-date "2024-03-15"
|
||||||
```
|
```
|
||||||
|
|
@ -103,7 +110,7 @@ hindsight memory put my-agent "Alice got promoted" \
|
||||||
</TabItem>
|
</TabItem>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
The `event_date` enables temporal queries like "What happened last spring?"
|
The `timestamp` enables temporal queries like "What happened last spring?"
|
||||||
|
|
||||||
## Batch Ingestion
|
## Batch Ingestion
|
||||||
|
|
||||||
|
|
@ -113,8 +120,8 @@ Store multiple memories in a single request:
|
||||||
<TabItem value="python" label="Python">
|
<TabItem value="python" label="Python">
|
||||||
|
|
||||||
```python
|
```python
|
||||||
client.store_batch(
|
client.retain_batch(
|
||||||
agent_id="my-agent",
|
bank_id="my-bank",
|
||||||
items=[
|
items=[
|
||||||
{"content": "Alice works at Google", "context": "career"},
|
{"content": "Alice works at Google", "context": "career"},
|
||||||
{"content": "Bob is a data scientist at Meta", "context": "career"},
|
{"content": "Bob is a data scientist at Meta", "context": "career"},
|
||||||
|
|
@ -128,14 +135,11 @@ client.store_batch(
|
||||||
<TabItem value="node" label="Node.js">
|
<TabItem value="node" label="Node.js">
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
await MemoryStorageService.batchApiMemoriesBatchPost({
|
await client.retainBatch('my-bank', [
|
||||||
agent_id: 'my-agent',
|
|
||||||
items: [
|
|
||||||
{ content: 'Alice works at Google', context: 'career' },
|
{ content: 'Alice works at Google', context: 'career' },
|
||||||
{ content: 'Bob is a data scientist at Meta', context: 'career' }
|
{ content: 'Bob is a data scientist at Meta', context: 'career' },
|
||||||
],
|
{ content: 'Alice and Bob are friends', context: 'relationship' }
|
||||||
document_id: 'conversation_001'
|
], { documentId: 'conversation_001' });
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
@ -150,13 +154,13 @@ The `document_id` groups related memories for later management.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Single file
|
# Single file
|
||||||
hindsight memory put-files my-agent document.txt
|
hindsight memory put-files my-bank document.txt
|
||||||
|
|
||||||
# Multiple files
|
# Multiple files
|
||||||
hindsight memory put-files my-agent doc1.txt doc2.md notes.txt
|
hindsight memory put-files my-bank doc1.txt doc2.md notes.txt
|
||||||
|
|
||||||
# With document ID
|
# With document ID
|
||||||
hindsight memory put-files my-agent report.pdf --document-id "q4-report"
|
hindsight memory put-files my-bank report.pdf --document-id "q4-report"
|
||||||
```
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
@ -191,15 +195,28 @@ For large batches, use async ingestion:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Start async ingestion
|
# Start async ingestion
|
||||||
operation = client.store_batch_async(
|
result = client.retain_batch(
|
||||||
agent_id="my-agent",
|
bank_id="my-bank",
|
||||||
items=[...large batch...],
|
items=[...large batch...],
|
||||||
document_id="large-doc"
|
document_id="large-doc",
|
||||||
|
async_=True
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check status
|
# Result contains operation_id for tracking
|
||||||
status = client.get_operation(operation["operation_id"])
|
print(result["operation_id"])
|
||||||
print(status["status"]) # "pending", "processing", "completed", "failed"
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem value="node" label="Node.js">
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Start async ingestion
|
||||||
|
const result = await client.retainBatch('my-bank', largeItems, {
|
||||||
|
documentId: 'large-doc',
|
||||||
|
async: true
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(result.operation_id);
|
||||||
```
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
@ -211,5 +228,5 @@ print(status["status"]) # "pending", "processing", "completed", "failed"
|
||||||
|----|-------|
|
|----|-------|
|
||||||
| Include context for better retrieval | Store raw unstructured dumps |
|
| Include context for better retrieval | Store raw unstructured dumps |
|
||||||
| Use document_id to group related content | Mix unrelated content in one batch |
|
| Use document_id to group related content | Mix unrelated content in one batch |
|
||||||
| Add event_date for temporal queries | Omit dates if time matters |
|
| Add timestamp for temporal queries | Omit dates if time matters |
|
||||||
| Store conversations as they happen | Wait to batch everything |
|
| Store conversations as they happen | Wait to batch everything |
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ class Server:
|
||||||
host: str = "127.0.0.1",
|
host: str = "127.0.0.1",
|
||||||
port: Optional[int] = None,
|
port: Optional[int] = None,
|
||||||
mcp_enabled: bool = False,
|
mcp_enabled: bool = False,
|
||||||
log_level: str = "warning",
|
log_level: str = "info",
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Initialize the Hindsight server.
|
Initialize the Hindsight server.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue