prepare for release
This commit is contained in:
+193
-66
@@ -6,8 +6,17 @@ on:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build-python-package:
|
||||
build-python-packages:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- name: hindsight-all
|
||||
path: hindsight
|
||||
- name: hindsight-api
|
||||
path: hindsight-api
|
||||
- name: hindsight-client
|
||||
path: hindsight-clients/python
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -22,15 +31,46 @@ jobs:
|
||||
with:
|
||||
python-version-file: ".python-version"
|
||||
|
||||
- name: Build hindsight package
|
||||
working-directory: ./hindsight
|
||||
- name: Build ${{ matrix.name }} package
|
||||
working-directory: ./${{ matrix.path }}
|
||||
run: uv build
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: python-hindsight-dist
|
||||
path: hindsight/dist/*
|
||||
name: python-${{ matrix.name }}-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
|
||||
|
||||
build-rust-cli:
|
||||
@@ -101,7 +141,14 @@ jobs:
|
||||
packages: write
|
||||
strategy:
|
||||
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:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -117,6 +164,9 @@ jobs:
|
||||
docker-images: true
|
||||
swap-storage: true
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
@@ -135,32 +185,21 @@ jobs:
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository_owner }}/hindsight-${{ matrix.component }}
|
||||
images: ghcr.io/${{ github.repository_owner }}/${{ matrix.image_name }}
|
||||
tags: |
|
||||
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}},value=${{ steps.get_version.outputs.VERSION }}
|
||||
type=raw,value=latest
|
||||
|
||||
- name: Build and push Docker image (api)
|
||||
if: matrix.component == 'api'
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/api.Dockerfile
|
||||
push: true
|
||||
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
|
||||
file: docker/standalone/Dockerfile
|
||||
target: ${{ matrix.target }}
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
@@ -192,9 +231,63 @@ jobs:
|
||||
path: helm-packages/*.tgz
|
||||
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:
|
||||
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:
|
||||
contents: write
|
||||
|
||||
@@ -205,11 +298,29 @@ jobs:
|
||||
id: get_version
|
||||
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Download Python package
|
||||
- name: Download hindsight-all
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: python-hindsight-dist
|
||||
path: ./artifacts/python-hindsight-dist
|
||||
name: python-hindsight-all-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)
|
||||
uses: actions/download-artifact@v4
|
||||
@@ -238,8 +349,12 @@ jobs:
|
||||
- name: Prepare release assets
|
||||
run: |
|
||||
mkdir -p release-assets
|
||||
# Python package
|
||||
cp artifacts/python-hindsight-dist/* release-assets/
|
||||
# Python packages
|
||||
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
|
||||
cp artifacts/rust-cli-hindsight-linux-amd64/hindsight-linux-amd64 release-assets/
|
||||
cp artifacts/rust-cli-hindsight-darwin-amd64/hindsight-darwin-amd64 release-assets/
|
||||
@@ -250,37 +365,61 @@ jobs:
|
||||
- name: Generate release notes
|
||||
id: release_notes
|
||||
run: |
|
||||
cat << EOF > release-notes.md
|
||||
cat << 'EOF' > release-notes.md
|
||||
# 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
|
||||
|
||||
### Python Package
|
||||
- \`hindsight-${{ steps.get_version.outputs.VERSION }}-py3-none-any.whl\`
|
||||
- \`hindsight-${{ steps.get_version.outputs.VERSION }}.tar.gz\`
|
||||
### Docker Images
|
||||
- `ghcr.io/${{ github.repository_owner }}/hindsight:${{ steps.get_version.outputs.VERSION }}` - **Standalone all-in-one** (recommended)
|
||||
- `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
|
||||
- \`hindsight-linux-amd64\` - Linux x86_64
|
||||
- \`hindsight-darwin-amd64\` - macOS Intel
|
||||
- \`hindsight-darwin-arm64\` - macOS Apple Silicon
|
||||
- `hindsight-linux-amd64` - Linux x86_64
|
||||
- `hindsight-darwin-amd64` - macOS Intel
|
||||
- `hindsight-darwin-arm64` - macOS Apple Silicon
|
||||
|
||||
### Helm Chart
|
||||
- \`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 }}\`
|
||||
- `hindsight-${{ steps.get_version.outputs.VERSION }}.tgz`
|
||||
|
||||
## 🚀 Installation
|
||||
|
||||
### Python Package
|
||||
\`\`\`bash
|
||||
pip install hindsight==${{ steps.get_version.outputs.VERSION }}
|
||||
\`\`\`
|
||||
### Python
|
||||
```bash
|
||||
# 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
|
||||
\`\`\`bash
|
||||
```bash
|
||||
# macOS (Apple Silicon)
|
||||
curl -L https://github.com/${{ github.repository }}/releases/download/v${{ steps.get_version.outputs.VERSION }}/hindsight-darwin-arm64 -o 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
|
||||
chmod +x hindsight
|
||||
sudo mv hindsight /usr/local/bin/
|
||||
\`\`\`
|
||||
```
|
||||
|
||||
### Helm Chart
|
||||
\`\`\`bash
|
||||
helm install hindsight hindsight-${{ steps.get_version.outputs.VERSION }}.tgz
|
||||
\`\`\`
|
||||
|
||||
### 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
|
||||
\`\`\`
|
||||
### Helm (Kubernetes)
|
||||
```bash
|
||||
helm install hindsight oci://ghcr.io/${{ github.repository_owner }}/charts/hindsight --version ${{ steps.get_version.outputs.VERSION }}
|
||||
```
|
||||
EOF
|
||||
cat release-notes.md
|
||||
|
||||
@@ -333,9 +459,10 @@ jobs:
|
||||
echo "# Release v${{ steps.get_version.outputs.VERSION }} Published Successfully" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $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 "- ✅ 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 "" >> $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
|
||||
|
||||
@@ -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.
|
||||
@@ -2,59 +2,96 @@
|
||||
|
||||
**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?
|
||||
|
||||
- **Temporal queries** — "What did Alice do last spring?" requires more than vector search
|
||||
- **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
|
||||
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.
|
||||
|
||||
## 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
|
||||
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
|
||||
import os
|
||||
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)
|
||||
|
||||
# Retain memories
|
||||
client.retain(bank_id="my-agent", content="Alice works at Google")
|
||||
client.retain(bank_id="my-agent", content="Bob prefers Python over JavaScript")
|
||||
|
||||
# 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")
|
||||
client.retain(bank_id="my-user", content="User prefers functional programming")
|
||||
response = client.reflect(bank_id="my-user", query="What coding style should I use?")
|
||||
print(response.text)
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 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
|
||||
- [Python Client](./hindsight-docs/docs/sdks/python.md) — Full API reference
|
||||
- [API Reference](./hindsight-docs/docs/api-reference/index.md) — REST API endpoints
|
||||
- [Personality](./hindsight-docs/docs/developer/personality.md) — Big Five traits and opinion formation
|
||||
- [Architecture](https://vectorize-io.github.io/hindsight/developer/architecture) — How ingestion, storage, and retrieval work
|
||||
- [Python Client](https://vectorize-io.github.io/hindsight/sdks/python) — Full API reference
|
||||
- [API Reference](https://vectorize-io.github.io/hindsight/api-reference) — REST API endpoints
|
||||
- [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
|
||||
|
||||
|
||||
@@ -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:
|
||||
+199
-29
@@ -1,6 +1,25 @@
|
||||
# Standalone All-in-One Hindsight Image
|
||||
# API with embedded pg0 + Control Plane
|
||||
FROM python:3.11-slim AS api-base
|
||||
# Hindsight Docker Image
|
||||
# Supports building API-only, Control Plane-only, or both
|
||||
#
|
||||
# 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
|
||||
|
||||
@@ -21,12 +40,18 @@ WORKDIR /app/api
|
||||
# Sync dependencies (will create lock file if needed)
|
||||
RUN uv sync
|
||||
|
||||
# Copy source code
|
||||
# Copy source code and alembic migrations
|
||||
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
|
||||
|
||||
ARG INCLUDE_CP
|
||||
RUN if [ "$INCLUDE_CP" != "true" ]; then echo "Skipping SDK build" && exit 0; fi
|
||||
|
||||
WORKDIR /app/sdk
|
||||
|
||||
COPY hindsight-clients/typescript/package*.json ./
|
||||
@@ -35,9 +60,14 @@ RUN npm ci
|
||||
COPY hindsight-clients/typescript/ ./
|
||||
RUN npm run build
|
||||
|
||||
# Build Control Plane
|
||||
# =============================================================================
|
||||
# Stage: Control Plane 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
|
||||
|
||||
# Copy built SDK
|
||||
@@ -59,8 +89,120 @@ RUN npm run build
|
||||
# Create public directory if it doesn't exist
|
||||
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
|
||||
|
||||
@@ -70,6 +212,7 @@ RUN apt-get update && apt-get install -y \
|
||||
libxml2 \
|
||||
libssl3 \
|
||||
libgssapi-krb5-2 \
|
||||
libossp-uuid16 \
|
||||
&& 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 - \
|
||||
&& apt-get install -y nodejs \
|
||||
@@ -80,22 +223,16 @@ RUN apt-get update && apt-get install -y \
|
||||
RUN useradd -m -s /bin/bash hindsight
|
||||
|
||||
# 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 --from=sdk-builder /app/sdk /app/sdk
|
||||
|
||||
# Copy Control Plane
|
||||
# Copy Control Plane standalone build
|
||||
WORKDIR /app/control-plane
|
||||
COPY --from=cp-builder /app/package*.json ./
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
# 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/.next/standalone ./
|
||||
COPY --from=cp-builder /app/.next/static ./.next/static
|
||||
COPY --from=cp-builder /app/public ./public
|
||||
COPY --from=cp-builder /app/next.config.ts ./next.config.ts
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -109,24 +246,57 @@ RUN mkdir -p /app/data && chown -R hindsight:hindsight /app
|
||||
# Switch to non-root user
|
||||
USER hindsight
|
||||
|
||||
# Install pg0
|
||||
RUN curl -fsSL https://raw.githubusercontent.com/vectorize-io/pg0/main/install.sh | bash
|
||||
# Set PATH for hindsight user
|
||||
ENV PATH="/home/hindsight/.hindsight/bin:/app/api/.venv/bin:${PATH}"
|
||||
|
||||
# Start pg0 once to verify it works and pre-download PostgreSQL libraries
|
||||
RUN pg0 --help && \
|
||||
pg0 start --wait && \
|
||||
pg0 stop
|
||||
# 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
|
||||
|
||||
# Expose ports
|
||||
EXPOSE 8888 3000
|
||||
# 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 9999
|
||||
|
||||
# Environment variables
|
||||
ENV HINDSIGHT_API_HOST=0.0.0.0
|
||||
ENV HINDSIGHT_API_PORT=8888
|
||||
ENV HINDSIGHT_API_LOG_LEVEL=info
|
||||
ENV NODE_ENV=production
|
||||
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"]
|
||||
|
||||
# =============================================================================
|
||||
# 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:
|
||||
hindsight:
|
||||
image: hindsight
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: docker/standalone/Dockerfile
|
||||
platform: linux/amd64
|
||||
env_file:
|
||||
- ../../.env
|
||||
ports:
|
||||
- "3000:3000"
|
||||
- "9999:9999"
|
||||
- "8888:8888"
|
||||
environment:
|
||||
# Pass through all HINDSIGHT_* environment variables from host
|
||||
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_LLM_PROVIDER: ${HINDSIGHT_API_LLM_PROVIDER:-}
|
||||
# These override env_file values only when set in host shell
|
||||
# Default values are applied only when not set in env_file or host
|
||||
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 can be set if you want to use an external database
|
||||
# If not set, embedded pg0 will be used automatically
|
||||
# Add any other HINDSIGHT_* vars you need here
|
||||
volumes:
|
||||
- hindsight_data:/app/data
|
||||
- hindsight_data:/home/hindsight/.pg0
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
|
||||
@@ -4,36 +4,75 @@ set -e
|
||||
echo "🚀 Starting Hindsight..."
|
||||
echo ""
|
||||
|
||||
# Start API (with embedded pg0)
|
||||
echo "⚡ Starting Hindsight API (with embedded database)..."
|
||||
cd /app/api
|
||||
python -m hindsight_api.web.server &
|
||||
API_PID=$!
|
||||
# Service flags (default to true if not set)
|
||||
ENABLE_API="${HINDSIGHT_ENABLE_API:-true}"
|
||||
ENABLE_CP="${HINDSIGHT_ENABLE_CP:-true}"
|
||||
|
||||
# Wait for API to be ready
|
||||
echo "⏳ Waiting for API..."
|
||||
for i in {1..30}; do
|
||||
if curl -sf http://localhost:8888/health &>/dev/null || curl -sf http://localhost:8888/docs &>/dev/null; then
|
||||
echo "✅ API is ready"
|
||||
break
|
||||
# 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
|
||||
sleep 1
|
||||
done
|
||||
fi
|
||||
|
||||
# Start Control Plane
|
||||
echo "🎛️ Starting Control Plane..."
|
||||
cd /app/control-plane
|
||||
node .next/standalone/server.js &
|
||||
CP_PID=$!
|
||||
# Track PIDs for wait
|
||||
PIDS=()
|
||||
|
||||
# Start API if enabled
|
||||
if [ "$ENABLE_API" = "true" ]; then
|
||||
cd /app/api
|
||||
python -m hindsight_api.web.server 2>&1 | sed -u 's/^/[api] /' &
|
||||
API_PID=$!
|
||||
PIDS+=($API_PID)
|
||||
|
||||
# Wait for API to be ready
|
||||
echo "⏳ Waiting for API..."
|
||||
for i in {1..60}; do
|
||||
if curl -sf http://localhost:8888/health &>/dev/null; then
|
||||
echo "✅ API is ready"
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
else
|
||||
echo "⏭️ API disabled (HINDSIGHT_ENABLE_API=false)"
|
||||
fi
|
||||
|
||||
# Start Control Plane if enabled
|
||||
if [ "$ENABLE_CP" = "true" ]; then
|
||||
echo "🎛️ Starting Control Plane..."
|
||||
cd /app/control-plane
|
||||
PORT=9999 node server.js 2>&1 | grep -v -E "^[[:space:]]*(▲|✓|-|$)" | sed -u 's/^/[control-plane] /' &
|
||||
CP_PID=$!
|
||||
PIDS+=($CP_PID)
|
||||
else
|
||||
echo "⏭️ Control Plane disabled (HINDSIGHT_ENABLE_CP=false)"
|
||||
fi
|
||||
|
||||
# Print status
|
||||
echo ""
|
||||
echo "✅ Hindsight is running!"
|
||||
echo ""
|
||||
echo "📍 Access:"
|
||||
echo " Control Plane: http://localhost:3000"
|
||||
echo " API: http://localhost:8888"
|
||||
if [ "$ENABLE_CP" = "true" ]; then
|
||||
echo " Control Plane: http://localhost:9999"
|
||||
fi
|
||||
if [ "$ENABLE_API" = "true" ]; then
|
||||
echo " API: http://localhost:8888"
|
||||
fi
|
||||
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 -n
|
||||
|
||||
|
||||
@@ -797,6 +797,24 @@ def create_app(memory: MemoryEngine, run_migrations: bool = True, initialize_mem
|
||||
def _register_routes(app: FastAPI):
|
||||
"""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(
|
||||
"/metrics",
|
||||
summary="Prometheus metrics endpoint",
|
||||
|
||||
@@ -2,11 +2,19 @@
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
from fastmcp import FastMCP
|
||||
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__)
|
||||
|
||||
|
||||
|
||||
@@ -10,13 +10,23 @@ import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CrossEncoderReranker(ABC):
|
||||
class CrossEncoderModel(ABC):
|
||||
"""
|
||||
Abstract base class for cross-encoder reranking.
|
||||
|
||||
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
|
||||
def predict(self, pairs: List[Tuple[str, str]]) -> List[float]:
|
||||
"""
|
||||
@@ -31,12 +41,11 @@ class CrossEncoderReranker(ABC):
|
||||
pass
|
||||
|
||||
|
||||
class SentenceTransformersCrossEncoder(CrossEncoderReranker):
|
||||
class SentenceTransformersCrossEncoder(CrossEncoderModel):
|
||||
"""
|
||||
Cross-encoder implementation using SentenceTransformers.
|
||||
|
||||
Uses lazy import so sentence-transformers is not required if another
|
||||
reranking backend is used.
|
||||
Call load() during initialization to load the model and avoid cold starts.
|
||||
|
||||
Default model is cross-encoder/ms-marco-MiniLM-L-6-v2:
|
||||
- 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"):
|
||||
"""
|
||||
Initialize SentenceTransformers cross-encoder and load model.
|
||||
Initialize SentenceTransformers cross-encoder.
|
||||
|
||||
Args:
|
||||
model_name: Name of the CrossEncoder model to use.
|
||||
Default: cross-encoder/ms-marco-MiniLM-L-6-v2
|
||||
"""
|
||||
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:
|
||||
from sentence_transformers import CrossEncoder
|
||||
@@ -76,5 +91,7 @@ class SentenceTransformersCrossEncoder(CrossEncoderReranker):
|
||||
Returns:
|
||||
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)
|
||||
|
||||
@@ -24,6 +24,16 @@ class Embeddings(ABC):
|
||||
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
|
||||
def encode(self, texts: List[str]) -> List[List[float]]:
|
||||
"""
|
||||
@@ -42,8 +52,7 @@ class SentenceTransformersEmbeddings(Embeddings):
|
||||
"""
|
||||
Embeddings implementation using SentenceTransformers.
|
||||
|
||||
Uses lazy import so sentence-transformers is not required if another
|
||||
embedding backend is used.
|
||||
Call load() during initialization to load the model and avoid cold starts.
|
||||
|
||||
Default model is BAAI/bge-small-en-v1.5 which produces 384-dimensional
|
||||
embeddings matching the database schema.
|
||||
@@ -60,32 +69,33 @@ class SentenceTransformersEmbeddings(Embeddings):
|
||||
"""
|
||||
self.model_name = model_name
|
||||
self._model = None
|
||||
self._load_model()
|
||||
|
||||
def _load_model(self):
|
||||
"""Lazy load and validate the SentenceTransformer model."""
|
||||
if self._model is None:
|
||||
try:
|
||||
from sentence_transformers import SentenceTransformer
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"sentence-transformers is required for SentenceTransformersEmbeddings. "
|
||||
"Install it with: pip install sentence-transformers"
|
||||
)
|
||||
def load(self) -> None:
|
||||
"""Load the embedding model."""
|
||||
if self._model is not None:
|
||||
return
|
||||
|
||||
logger.info(f"Loading embedding model: {self.model_name}...")
|
||||
self._model = SentenceTransformer(self.model_name)
|
||||
try:
|
||||
from sentence_transformers import SentenceTransformer
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"sentence-transformers is required for SentenceTransformersEmbeddings. "
|
||||
"Install it with: pip install sentence-transformers"
|
||||
)
|
||||
|
||||
# Validate dimension matches database schema
|
||||
model_dim = self._model.get_sentence_embedding_dimension()
|
||||
if model_dim != EMBEDDING_DIMENSION:
|
||||
raise ValueError(
|
||||
f"Model {self.model_name} produces {model_dim}-dimensional embeddings, "
|
||||
f"but database schema requires {EMBEDDING_DIMENSION} dimensions. "
|
||||
f"Use a model that produces {EMBEDDING_DIMENSION}-dimensional embeddings."
|
||||
)
|
||||
logger.info(f"Loading embedding model: {self.model_name}...")
|
||||
self._model = SentenceTransformer(self.model_name)
|
||||
|
||||
logger.info(f"Model loaded (embedding dim: {model_dim})")
|
||||
# Validate dimension matches database schema
|
||||
model_dim = self._model.get_sentence_embedding_dimension()
|
||||
if model_dim != EMBEDDING_DIMENSION:
|
||||
raise ValueError(
|
||||
f"Model {self.model_name} produces {model_dim}-dimensional embeddings, "
|
||||
f"but database schema requires {EMBEDDING_DIMENSION} dimensions. "
|
||||
f"Use a model that produces {EMBEDDING_DIMENSION}-dimensional embeddings."
|
||||
)
|
||||
|
||||
logger.info(f"Model loaded (embedding dim: {model_dim})")
|
||||
|
||||
def encode(self, texts: List[str]) -> List[List[float]]:
|
||||
"""
|
||||
@@ -97,5 +107,7 @@ class SentenceTransformersEmbeddings(Embeddings):
|
||||
Returns:
|
||||
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)
|
||||
return [emb.tolist() for emb in embeddings]
|
||||
|
||||
@@ -15,7 +15,7 @@ from typing import Any, Dict, List, Optional, Tuple, Union, TypedDict
|
||||
import asyncpg
|
||||
import asyncio
|
||||
from .embeddings import Embeddings, SentenceTransformersEmbeddings
|
||||
from .cross_encoder import CrossEncoderReranker as CrossEncoderModel
|
||||
from .cross_encoder import CrossEncoderModel
|
||||
import time
|
||||
import numpy as np
|
||||
import uuid
|
||||
@@ -362,15 +362,53 @@ class MemoryEngine:
|
||||
logger.error(f"Failed to mark operation as failed {operation_id}: {e}")
|
||||
|
||||
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:
|
||||
return
|
||||
|
||||
# Start pg0 embedded PostgreSQL if configured
|
||||
if self._use_pg0:
|
||||
self._pg0 = EmbeddedPostgres()
|
||||
self.db_url = await self._pg0.ensure_running()
|
||||
logger.info(f"Connecting to PostGre instance at {self.db_url}")
|
||||
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:
|
||||
self._pg0 = EmbeddedPostgres()
|
||||
self.db_url = await self._pg0.ensure_running()
|
||||
|
||||
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
|
||||
# For read-heavy workloads with many parallel think/search operations,
|
||||
@@ -414,6 +452,24 @@ class MemoryEngine:
|
||||
|
||||
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):
|
||||
"""Close the connection pool and shutdown background workers."""
|
||||
logger.info("close() started")
|
||||
@@ -503,8 +559,15 @@ class MemoryEngine:
|
||||
if not texts:
|
||||
return []
|
||||
|
||||
time_lower = event_date - timedelta(hours=time_window_hours)
|
||||
time_upper = event_date + timedelta(hours=time_window_hours)
|
||||
# Handle edge cases where event_date is at datetime boundaries
|
||||
try:
|
||||
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)
|
||||
except OverflowError:
|
||||
time_upper = datetime.max
|
||||
|
||||
# Fetch ALL existing facts in time window ONCE (much faster than N queries)
|
||||
import time as time_mod
|
||||
|
||||
@@ -46,6 +46,16 @@ class QueryAnalyzer(ABC):
|
||||
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
|
||||
def analyze(
|
||||
self, query: str, reference_date: Optional[datetime] = None
|
||||
@@ -94,21 +104,29 @@ class TransformerQueryAnalyzer(QueryAnalyzer):
|
||||
self._model = None
|
||||
self._tokenizer = None
|
||||
|
||||
def _load_model(self):
|
||||
"""Lazy load the T5 model for temporal extraction."""
|
||||
if self._model is None:
|
||||
try:
|
||||
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"transformers is required for TransformerQueryAnalyzer. "
|
||||
"Install it with: pip install transformers"
|
||||
)
|
||||
def load(self) -> None:
|
||||
"""Load the T5 model for temporal extraction."""
|
||||
if self._model is not None:
|
||||
return
|
||||
|
||||
self._tokenizer = AutoTokenizer.from_pretrained(self.model_name)
|
||||
self._model = AutoModelForSeq2SeqLM.from_pretrained(self.model_name)
|
||||
self._model.to(self.device)
|
||||
self._model.eval()
|
||||
try:
|
||||
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"transformers is required for TransformerQueryAnalyzer. "
|
||||
"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._model = AutoModelForSeq2SeqLM.from_pretrained(self.model_name)
|
||||
self._model.to(self.device)
|
||||
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(
|
||||
self, query: str, reference_date: Optional[datetime] = None
|
||||
|
||||
@@ -5,11 +5,107 @@ Link creation utilities for temporal, semantic, and entity links.
|
||||
import time
|
||||
import logging
|
||||
from typing import List
|
||||
from datetime import timedelta
|
||||
from datetime import timedelta, datetime, timezone
|
||||
|
||||
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'):
|
||||
"""Helper to log to buffer if available, otherwise use logger."""
|
||||
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")
|
||||
|
||||
# Fetch ALL potential temporal neighbors in ONE query (much faster!)
|
||||
# Get time range across all units
|
||||
all_dates = list(new_units.values())
|
||||
min_date = min(all_dates) - timedelta(hours=time_window_hours)
|
||||
max_date = max(all_dates) + timedelta(hours=time_window_hours)
|
||||
# Get time range across all units with overflow protection
|
||||
min_date, max_date = compute_temporal_query_bounds(new_units, time_window_hours)
|
||||
|
||||
fetch_neighbors_start = time_mod.time()
|
||||
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)
|
||||
link_gen_start = time_mod.time()
|
||||
links = []
|
||||
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))
|
||||
|
||||
links = compute_temporal_links(new_units, all_candidates, time_window_hours)
|
||||
_log(log_buffer, f" [7.3] Generate {len(links)} temporal links: {time_mod.time() - link_gen_start:.3f}s")
|
||||
|
||||
if links:
|
||||
|
||||
@@ -23,9 +23,11 @@ class CrossEncoderReranker:
|
||||
Args:
|
||||
cross_encoder: CrossEncoderReranker instance. If None, uses default
|
||||
SentenceTransformersCrossEncoder with ms-marco-MiniLM-L-6-v2
|
||||
(loaded lazily for faster startup)
|
||||
"""
|
||||
if cross_encoder is None:
|
||||
from hindsight_api.engine.cross_encoder import SentenceTransformersCrossEncoder
|
||||
# Model is loaded lazily - call ensure_loaded() during initialize()
|
||||
cross_encoder = SentenceTransformersCrossEncoder()
|
||||
self.cross_encoder = cross_encoder
|
||||
|
||||
|
||||
+143
-145
@@ -3,10 +3,10 @@ import json
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
@@ -14,8 +14,7 @@ import httpx
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_DATA_DIR = Path(os.environ.get("HINDSIGHT_API_PG0_DATA_DIR", Path.home() / ".hindsight" / "pg_data"))
|
||||
DEFAULT_INSTALL_DIR = Path.home() / ".hindsight" / "bin"
|
||||
# pg0 configuration
|
||||
BINARY_NAME = "pg0"
|
||||
DEFAULT_PORT = 5555
|
||||
DEFAULT_USERNAME = "hindsight"
|
||||
@@ -65,9 +64,7 @@ def get_download_url(
|
||||
version: str = "latest",
|
||||
repo: str = "vectorize-io/pg0",
|
||||
) -> str:
|
||||
"""
|
||||
"""
|
||||
# Check for direct URL override
|
||||
"""Get the download URL for pg0 binary."""
|
||||
binary_name = get_platform_binary_name()
|
||||
|
||||
if version == "latest":
|
||||
@@ -76,17 +73,32 @@ def get_download_url(
|
||||
return f"https://github.com/{repo}/releases/download/{version}/{binary_name}"
|
||||
|
||||
|
||||
def _find_pg0_binary() -> Optional[Path]:
|
||||
"""Find pg0 binary in PATH or default install location."""
|
||||
# First check PATH
|
||||
pg0_in_path = shutil.which("pg0")
|
||||
if pg0_in_path:
|
||||
return Path(pg0_in_path)
|
||||
|
||||
# Fall back to default install location
|
||||
default_path = Path.home() / ".hindsight" / "bin" / "pg0"
|
||||
if default_path.exists() and os.access(default_path, os.X_OK):
|
||||
return default_path
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class EmbeddedPostgres:
|
||||
"""
|
||||
Manages an embedded PostgreSQL server instance.
|
||||
Manages an embedded PostgreSQL server instance using pg0.
|
||||
|
||||
This class handles:
|
||||
- Downloading and installing the embedded-postgres CLI
|
||||
- Finding or downloading the pg0 CLI
|
||||
- Starting/stopping the PostgreSQL server
|
||||
- Getting the connection URI
|
||||
|
||||
Example:
|
||||
pg = EmbeddedPostgres(data_dir="~/.myapp/data")
|
||||
pg = EmbeddedPostgres()
|
||||
await pg.ensure_installed()
|
||||
await pg.start()
|
||||
uri = await pg.get_uri()
|
||||
@@ -96,8 +108,6 @@ class EmbeddedPostgres:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data_dir: Optional[Path] = None,
|
||||
install_dir: Optional[Path] = None,
|
||||
version: str = "latest",
|
||||
port: int = DEFAULT_PORT,
|
||||
username: str = DEFAULT_USERNAME,
|
||||
@@ -109,17 +119,13 @@ class EmbeddedPostgres:
|
||||
Initialize the embedded PostgreSQL manager.
|
||||
|
||||
Args:
|
||||
data_dir: Directory to store PostgreSQL data. Defaults to ~/.hindsight/pg_data
|
||||
install_dir: Directory to install the CLI binary. Defaults to ~/.hindsight/bin
|
||||
version: Version of embedded-postgres to use. Defaults to "latest"
|
||||
version: Version of pg0 to download if not found. Defaults to "latest"
|
||||
port: Port to listen on. Defaults to 5555
|
||||
username: Username for the database. Defaults to "hindsight"
|
||||
password: Password for the database. Defaults to "hindsight"
|
||||
database: Database name to create. Defaults to "hindsight"
|
||||
name: Instance name for pg0. Defaults to "hindsight"
|
||||
"""
|
||||
self.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.port = port
|
||||
self.username = username
|
||||
@@ -127,35 +133,42 @@ class EmbeddedPostgres:
|
||||
self.database = database
|
||||
self.name = name
|
||||
|
||||
# Binary path
|
||||
binary_name = "pg0.exe" if platform.system() == "Windows" else "pg0"
|
||||
self.binary_path = self.install_dir / binary_name
|
||||
# Will be set when binary is found/installed
|
||||
self._binary_path: Optional[Path] = _find_pg0_binary()
|
||||
|
||||
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:
|
||||
"""Check if the embedded-postgres CLI is installed."""
|
||||
return self.binary_path.exists() and os.access(self.binary_path, os.X_OK)
|
||||
"""Check if pg0 is available (in PATH or installed)."""
|
||||
self._binary_path = _find_pg0_binary()
|
||||
return self._binary_path is not 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():
|
||||
logger.info(f"pg0 already installed at {self.binary_path}")
|
||||
logger.debug(f"pg0 found at {self._binary_path}")
|
||||
return
|
||||
|
||||
logger.info("Installing pg0 CLI...")
|
||||
logger.info("pg0 not found, downloading...")
|
||||
|
||||
# Log platform information
|
||||
binary_name = get_platform_binary_name()
|
||||
logger.info(f"Detected platform: system={platform.system()}, machine={platform.machine()}")
|
||||
logger.info(f"Will download binary: {binary_name}")
|
||||
|
||||
# Create install directory
|
||||
self.install_dir.mkdir(parents=True, exist_ok=True)
|
||||
# Install to default location
|
||||
install_dir = Path.home() / ".hindsight" / "bin"
|
||||
install_dir.mkdir(parents=True, exist_ok=True)
|
||||
install_path = install_dir / "pg0"
|
||||
|
||||
# Download the binary
|
||||
download_url = get_download_url(self.version)
|
||||
@@ -167,85 +180,115 @@ class EmbeddedPostgres:
|
||||
response.raise_for_status()
|
||||
|
||||
# Write binary to disk
|
||||
with open(self.binary_path, "wb") as f:
|
||||
with open(install_path, "wb") as f:
|
||||
f.write(response.content)
|
||||
|
||||
# Make executable on Unix
|
||||
if platform.system() != "Windows":
|
||||
st = os.stat(self.binary_path)
|
||||
os.chmod(self.binary_path, st.st_mode | stat.S_IEXEC)
|
||||
st = os.stat(install_path)
|
||||
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:
|
||||
raise RuntimeError(f"Failed to download pg0: {e}") from e
|
||||
|
||||
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]
|
||||
|
||||
return subprocess.run(
|
||||
cmd,
|
||||
capture_output=capture_output,
|
||||
text=True,
|
||||
)
|
||||
def run_sync():
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
)
|
||||
return result.returncode, result.stdout, result.stderr
|
||||
except subprocess.TimeoutExpired:
|
||||
return 1, "", "Command timed out"
|
||||
|
||||
async def _run_command_async(self, *args: str) -> tuple[int, str, str]:
|
||||
"""Run an embedded-postgres command asynchronously."""
|
||||
cmd = [str(self.binary_path), *args]
|
||||
loop = asyncio.get_event_loop()
|
||||
return await loop.run_in_executor(None, run_sync)
|
||||
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
*cmd,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
def _extract_uri_from_output(self, output: str) -> Optional[str]:
|
||||
"""Extract the PostgreSQL URI from pg0 start output."""
|
||||
match = re.search(r"Connection URI:\s*(postgresql://[^\s]+)", output)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return None
|
||||
|
||||
stdout, stderr = await process.communicate()
|
||||
return process.returncode, stdout.decode(), stderr.decode()
|
||||
|
||||
async def start(self) -> str:
|
||||
async def start(self, max_retries: int = 3, retry_delay: float = 2.0) -> 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:
|
||||
The connection URI for the started server.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the server fails to start.
|
||||
RuntimeError: If the server fails to start after all retries.
|
||||
"""
|
||||
if not self.is_installed():
|
||||
raise RuntimeError("pg0 is not installed. Call ensure_installed() first.")
|
||||
|
||||
# Create data directory
|
||||
self.data_dir.mkdir(parents=True, exist_ok=True)
|
||||
logger.info(f"Starting embedded PostgreSQL (name: {self.name}, port: {self.port})...")
|
||||
|
||||
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(
|
||||
"start",
|
||||
"--name", self.name,
|
||||
"--port", str(self.port),
|
||||
"--username", self.username,
|
||||
"--password", self.password,
|
||||
"--database", self.database,
|
||||
timeout=300,
|
||||
)
|
||||
|
||||
returncode, stdout, stderr = await self._run_command_async(
|
||||
"start",
|
||||
"--name", self.name,
|
||||
"--port", str(self.port),
|
||||
"--username", self.username,
|
||||
"--password", self.password,
|
||||
"--database", self.database,
|
||||
"--data-dir", self.data_dir.as_posix()
|
||||
)
|
||||
# Try to extract URI from output
|
||||
uri = self._extract_uri_from_output(stdout)
|
||||
if uri:
|
||||
logger.info(f"PostgreSQL started on port {self.port}")
|
||||
return uri
|
||||
|
||||
if returncode != 0:
|
||||
raise RuntimeError(f"Failed to start PostgreSQL: {stderr}")
|
||||
# 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
|
||||
|
||||
logger.info("Embedded PostgreSQL started")
|
||||
# Start failed, log and retry
|
||||
last_error = stderr or f"pg0 start returned exit code {returncode}"
|
||||
if attempt < max_retries:
|
||||
delay = retry_delay * (2 ** (attempt - 1))
|
||||
logger.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()}")
|
||||
|
||||
# Get and return the URI
|
||||
return await self.get_uri()
|
||||
# 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:
|
||||
"""
|
||||
Stop the PostgreSQL server.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the server fails to stop.
|
||||
"""
|
||||
"""Stop the PostgreSQL server."""
|
||||
if not self.is_installed():
|
||||
return
|
||||
|
||||
@@ -254,7 +297,6 @@ class EmbeddedPostgres:
|
||||
returncode, stdout, stderr = await self._run_command_async("stop", "--name", self.name)
|
||||
|
||||
if returncode != 0:
|
||||
# Don't raise if server wasn't running
|
||||
if "not running" in stderr.lower():
|
||||
return
|
||||
raise RuntimeError(f"Failed to stop PostgreSQL: {stderr}")
|
||||
@@ -262,20 +304,13 @@ class EmbeddedPostgres:
|
||||
logger.info("Embedded PostgreSQL stopped")
|
||||
|
||||
async def _get_info(self) -> dict:
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
"""Get info from pg0 using the `info -o json` command."""
|
||||
if not self.is_installed():
|
||||
raise RuntimeError("pg0 is not installed.")
|
||||
|
||||
returncode, stdout, stderr = await self._run_command_async(
|
||||
"info", "--name", self.name, "-o", "json")
|
||||
"info", "--name", self.name, "-o", "json"
|
||||
)
|
||||
|
||||
if returncode != 0:
|
||||
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}")
|
||||
|
||||
async def get_uri(self) -> str:
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
"""Get the connection URI for the PostgreSQL server."""
|
||||
info = await self._get_info()
|
||||
uri = info.get("uri")
|
||||
if not uri:
|
||||
@@ -302,12 +329,7 @@ class EmbeddedPostgres:
|
||||
return uri
|
||||
|
||||
async def status(self) -> dict:
|
||||
"""
|
||||
Get the status of the PostgreSQL server.
|
||||
|
||||
Returns:
|
||||
Dictionary with status information including 'running' boolean and 'uri'.
|
||||
"""
|
||||
"""Get the status of the PostgreSQL server."""
|
||||
if not self.is_installed():
|
||||
return {"installed": False, "running": False}
|
||||
|
||||
@@ -317,16 +339,9 @@ class EmbeddedPostgres:
|
||||
"installed": True,
|
||||
"running": info.get("running", False),
|
||||
"uri": info.get("uri"),
|
||||
"data_dir": str(self.data_dir),
|
||||
"binary_path": str(self.binary_path),
|
||||
}
|
||||
except RuntimeError:
|
||||
return {
|
||||
"installed": True,
|
||||
"running": False,
|
||||
"data_dir": str(self.data_dir),
|
||||
"binary_path": str(self.binary_path),
|
||||
}
|
||||
return {"installed": True, "running": False}
|
||||
|
||||
async def is_running(self) -> bool:
|
||||
"""Check if the PostgreSQL server is currently running."""
|
||||
@@ -355,59 +370,42 @@ class EmbeddedPostgres:
|
||||
return await self.start()
|
||||
|
||||
def uninstall(self) -> None:
|
||||
"""Remove the embedded-postgres binary."""
|
||||
if self.binary_path.exists():
|
||||
self.binary_path.unlink()
|
||||
logger.info(f"Removed {self.binary_path}")
|
||||
"""Remove the pg0 binary (only if we installed it)."""
|
||||
default_path = Path.home() / ".hindsight" / "bin" / "pg0"
|
||||
if default_path.exists():
|
||||
default_path.unlink()
|
||||
logger.info(f"Removed {default_path}")
|
||||
|
||||
def clear_data(self) -> None:
|
||||
"""Remove all PostgreSQL data (destructive!)."""
|
||||
if self.data_dir.exists():
|
||||
shutil.rmtree(self.data_dir)
|
||||
logger.info(f"Removed data directory {self.data_dir}")
|
||||
result = self._run_command("drop", "--name", self.name, "--force")
|
||||
if result.returncode == 0:
|
||||
logger.info(f"Dropped pg0 instance {self.name}")
|
||||
else:
|
||||
logger.warning(f"Failed to drop pg0 instance {self.name}: {result.stderr}")
|
||||
|
||||
|
||||
# Convenience functions for simple usage
|
||||
# Convenience functions
|
||||
|
||||
_default_instance: Optional[EmbeddedPostgres] = None
|
||||
|
||||
|
||||
def get_embedded_postgres(
|
||||
data_dir: Optional[Path] = None,
|
||||
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
|
||||
"""
|
||||
def get_embedded_postgres() -> EmbeddedPostgres:
|
||||
"""Get or create the default EmbeddedPostgres instance."""
|
||||
global _default_instance
|
||||
|
||||
if _default_instance is None or data_dir or install_dir:
|
||||
_default_instance = EmbeddedPostgres(
|
||||
data_dir=data_dir,
|
||||
install_dir=install_dir,
|
||||
)
|
||||
if _default_instance is None:
|
||||
_default_instance = EmbeddedPostgres()
|
||||
|
||||
return _default_instance
|
||||
|
||||
|
||||
async def start_embedded_postgres(
|
||||
data_dir: Optional[Path] = None,
|
||||
) -> str:
|
||||
async def start_embedded_postgres() -> str:
|
||||
"""
|
||||
Quick start function for embedded PostgreSQL.
|
||||
|
||||
Downloads, installs, and starts PostgreSQL in one call.
|
||||
|
||||
Args:
|
||||
data_dir: Directory to store PostgreSQL data
|
||||
|
||||
Returns:
|
||||
Connection URI string
|
||||
|
||||
@@ -415,7 +413,7 @@ async def start_embedded_postgres(
|
||||
db_url = await start_embedded_postgres()
|
||||
conn = await asyncpg.connect(db_url)
|
||||
"""
|
||||
pg = get_embedded_postgres(data_dir=data_dir)
|
||||
pg = get_embedded_postgres()
|
||||
return await pg.ensure_running()
|
||||
|
||||
|
||||
@@ -424,4 +422,4 @@ async def stop_embedded_postgres() -> None:
|
||||
global _default_instance
|
||||
|
||||
if _default_instance:
|
||||
await _default_instance.stop()
|
||||
await _default_instance.stop()
|
||||
|
||||
@@ -79,7 +79,10 @@ app = create_app(
|
||||
if __name__ == "__main__":
|
||||
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
|
||||
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("--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("--log-level", default="info", choices=["critical", "error", "warning", "info", "debug", "trace"],
|
||||
help="Log level (default: info)")
|
||||
parser.add_argument("--log-level", default=env_log_level, choices=["critical", "error", "warning", "info", "debug", "trace"],
|
||||
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("--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")
|
||||
@@ -99,6 +102,21 @@ if __name__ == "__main__":
|
||||
|
||||
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"
|
||||
|
||||
# Prepare uvicorn config
|
||||
|
||||
@@ -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
@@ -12,7 +12,9 @@
|
||||
"@hindsight/client": "file:../hindsight-clients/typescript",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-label": "^2.1.8",
|
||||
"@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-slot": "^1.2.4",
|
||||
"@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": {
|
||||
"version": "1.1.15",
|
||||
"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": {
|
||||
"version": "2.2.6",
|
||||
"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",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-label": "^2.1.8",
|
||||
"@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-slot": "^1.2.4",
|
||||
"@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 { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useBank } from '@/lib/bank-context';
|
||||
import { client } from '@/lib/api';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Command,
|
||||
@@ -18,19 +19,105 @@ import {
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} 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';
|
||||
|
||||
function BankSelectorInner() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { currentBank, setCurrentBank, banks } = useBank();
|
||||
const { currentBank, setCurrentBank, banks, loadBanks } = useBank();
|
||||
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(() => {
|
||||
return [...banks].sort((a, b) => a.localeCompare(b));
|
||||
}, [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 (
|
||||
<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">
|
||||
@@ -81,6 +168,163 @@ function BankSelectorInner() {
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</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>
|
||||
);
|
||||
|
||||
@@ -7,8 +7,8 @@ import cytoscape from 'cytoscape';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
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 } from 'lucide-react';
|
||||
import { Copy, Check, X, Calendar, ZoomIn, ZoomOut, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, FileText, Layers } from 'lucide-react';
|
||||
import { MemoryDetailPanel } from './memory-detail-panel';
|
||||
|
||||
type FactType = 'world' | 'bank' | 'opinion';
|
||||
type ViewMode = 'graph' | 'table' | 'timeline';
|
||||
@@ -31,6 +31,8 @@ export function DataView({ factType }: DataViewProps) {
|
||||
const [selectedChunk, setSelectedChunk] = useState<any>(null);
|
||||
const [loadingChunk, setLoadingChunk] = useState(false);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [selectedGraphNode, setSelectedGraphNode] = useState<any>(null);
|
||||
const [selectedTableMemory, setSelectedTableMemory] = useState<any>(null);
|
||||
const itemsPerPage = 100;
|
||||
const cyRef = useRef<any>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -195,6 +197,23 @@ export function DataView({ factType }: DataViewProps) {
|
||||
] as any,
|
||||
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(() => {
|
||||
@@ -265,70 +284,95 @@ export function DataView({ factType }: DataViewProps) {
|
||||
</div>
|
||||
|
||||
{viewMode === 'graph' && (
|
||||
<div className="relative">
|
||||
<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">
|
||||
<label className="font-semibold text-card-foreground">Limit nodes:</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={nodeLimit}
|
||||
onChange={(e) => setNodeLimit(parseInt(e.target.value))}
|
||||
min="10"
|
||||
max="1000"
|
||||
step="10"
|
||||
className="w-20"
|
||||
<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="flex items-center gap-2">
|
||||
<label className="font-semibold text-card-foreground">Limit nodes:</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={nodeLimit}
|
||||
onChange={(e) => setNodeLimit(parseInt(e.target.value))}
|
||||
min="10"
|
||||
max="1000"
|
||||
step="10"
|
||||
className="w-20"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="font-semibold text-card-foreground">Layout:</label>
|
||||
<Select value={layout} onValueChange={setLayout}>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="circle">Circle (fast)</SelectItem>
|
||||
<SelectItem value="grid">Grid (fast)</SelectItem>
|
||||
<SelectItem value="cose">Force-directed (slow)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground ml-auto">
|
||||
Click on a node to view details
|
||||
</div>
|
||||
</div>
|
||||
<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]">
|
||||
<h3 className="font-bold mb-2 border-b-2 border-primary pb-1 text-card-foreground">Legend</h3>
|
||||
<h4 className="font-bold mt-2 mb-1 text-sm text-card-foreground">Link Types:</h4>
|
||||
<div className="flex items-center my-2">
|
||||
<div className="w-8 h-0.5 mr-2.5 bg-cyan-500 border-t border-dashed border-cyan-500" />
|
||||
<span className="text-sm"><strong>Temporal</strong></span>
|
||||
</div>
|
||||
<div className="flex items-center my-2">
|
||||
<div className="w-8 h-0.5 mr-2.5 bg-pink-500" />
|
||||
<span className="text-sm"><strong>Semantic</strong></span>
|
||||
</div>
|
||||
<div className="flex items-center my-2">
|
||||
<div className="w-8 h-0.5 mr-2.5 bg-yellow-500" />
|
||||
<span className="text-sm"><strong>Entity</strong></span>
|
||||
</div>
|
||||
<h4 className="font-bold mt-2 mb-1 text-sm">Nodes:</h4>
|
||||
<div className="flex items-center my-2">
|
||||
<div className="w-5 h-5 mr-2.5 bg-gray-300 border border-gray-500 rounded" />
|
||||
<span className="text-sm">No entities</span>
|
||||
</div>
|
||||
<div className="flex items-center my-2">
|
||||
<div className="w-5 h-5 mr-2.5 bg-blue-300 border border-gray-500 rounded" />
|
||||
<span className="text-sm">1 entity</span>
|
||||
</div>
|
||||
<div className="flex items-center my-2">
|
||||
<div className="w-5 h-5 mr-2.5 bg-blue-500 border border-gray-500 rounded" />
|
||||
<span className="text-sm">2+ entities</span>
|
||||
</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 className="flex items-center gap-2">
|
||||
<label className="font-semibold text-card-foreground">Layout:</label>
|
||||
<Select value={layout} onValueChange={setLayout}>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="circle">Circle (fast)</SelectItem>
|
||||
<SelectItem value="grid">Grid (fast)</SelectItem>
|
||||
<SelectItem value="cose">Force-directed (slow)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<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]">
|
||||
<h3 className="font-bold mb-2 border-b-2 border-primary pb-1 text-card-foreground">Legend</h3>
|
||||
<h4 className="font-bold mt-2 mb-1 text-sm text-card-foreground">Link Types:</h4>
|
||||
<div className="flex items-center my-2">
|
||||
<div className="w-8 h-0.5 mr-2.5 bg-cyan-500 border-t border-dashed border-cyan-500" />
|
||||
<span className="text-sm"><strong>Temporal</strong></span>
|
||||
</div>
|
||||
<div className="flex items-center my-2">
|
||||
<div className="w-8 h-0.5 mr-2.5 bg-pink-500" />
|
||||
<span className="text-sm"><strong>Semantic</strong></span>
|
||||
</div>
|
||||
<div className="flex items-center my-2">
|
||||
<div className="w-8 h-0.5 mr-2.5 bg-yellow-500" />
|
||||
<span className="text-sm"><strong>Entity</strong></span>
|
||||
</div>
|
||||
<h4 className="font-bold mt-2 mb-1 text-sm">Nodes:</h4>
|
||||
<div className="flex items-center my-2">
|
||||
<div className="w-5 h-5 mr-2.5 bg-gray-300 border border-gray-500 rounded" />
|
||||
<span className="text-sm">No entities</span>
|
||||
</div>
|
||||
<div className="flex items-center my-2">
|
||||
<div className="w-5 h-5 mr-2.5 bg-blue-300 border border-gray-500 rounded" />
|
||||
<span className="text-sm">1 entity</span>
|
||||
</div>
|
||||
<div className="flex items-center my-2">
|
||||
<div className="w-5 h-5 mr-2.5 bg-blue-500 border border-gray-500 rounded" />
|
||||
<span className="text-sm">2+ entities</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{viewMode === 'table' && (
|
||||
<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">
|
||||
<Input
|
||||
type="text"
|
||||
@@ -338,173 +382,211 @@ export function DataView({ factType }: DataViewProps) {
|
||||
className="max-w-2xl"
|
||||
/>
|
||||
</div>
|
||||
<div className="overflow-x-auto 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 ? (
|
||||
(() => {
|
||||
const filteredRows = data.table_rows.filter((row: any) => {
|
||||
if (!searchQuery) return true;
|
||||
const query = searchQuery.toLowerCase();
|
||||
return (
|
||||
row.text?.toLowerCase().includes(query) ||
|
||||
row.context?.toLowerCase().includes(query)
|
||||
);
|
||||
});
|
||||
<div className="px-5 pb-5">
|
||||
{data.table_rows && data.table_rows.length > 0 ? (
|
||||
(() => {
|
||||
const filteredRows = data.table_rows.filter((row: any) => {
|
||||
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);
|
||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||
const endIndex = startIndex + itemsPerPage;
|
||||
const paginatedRows = filteredRows.slice(startIndex, endIndex);
|
||||
const totalPages = Math.ceil(filteredRows.length / itemsPerPage);
|
||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||
const endIndex = startIndex + itemsPerPage;
|
||||
const paginatedRows = filteredRows.slice(startIndex, endIndex);
|
||||
|
||||
return paginatedRows.map((row: any, idx: number) => {
|
||||
// Format temporal range
|
||||
let occurredDisplay = 'N/A';
|
||||
if (row.occurred_start && row.occurred_end) {
|
||||
const start = new Date(row.occurred_start).toLocaleString();
|
||||
const end = new Date(row.occurred_end).toLocaleString();
|
||||
occurredDisplay = start === end ? start : `${start} - ${end}`;
|
||||
} else if (row.date) {
|
||||
// Fallback to old date field
|
||||
occurredDisplay = row.date;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className="grid gap-3">
|
||||
{paginatedRows.map((row: any, idx: number) => {
|
||||
const occurredDisplay = row.occurred_start
|
||||
? new Date(row.occurred_start).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
|
||||
: null;
|
||||
|
||||
const mentionedDisplay = row.mentioned_at
|
||||
? new Date(row.mentioned_at).toLocaleString()
|
||||
: 'N/A';
|
||||
return (
|
||||
<div
|
||||
key={row.id || idx}
|
||||
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'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
{/* Main content */}
|
||||
<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)}...
|
||||
</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>
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={idx}
|
||||
className={selectedDocument?.id === row.document_id ? 'bg-accent' : ''}
|
||||
>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<span title={row.id} className="text-muted-foreground">
|
||||
{row.id.substring(0, 8)}...
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
copyToClipboard(row.id);
|
||||
}}
|
||||
>
|
||||
{copiedId === row.id ? (
|
||||
<Check className="h-3 w-3 text-green-600" />
|
||||
) : (
|
||||
<Copy className="h-3 w-3" />
|
||||
)}
|
||||
</Button>
|
||||
{/* Action buttons */}
|
||||
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
{row.document_id && (
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
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);
|
||||
}}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-8 w-8 p-0"
|
||||
title="Copy ID"
|
||||
>
|
||||
{copiedId === row.id ? (
|
||||
<Check className="h-4 w-4 text-green-600" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</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>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
});
|
||||
})()
|
||||
) : (
|
||||
<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>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Pagination Controls */}
|
||||
{data.table_rows && data.table_rows.length > 0 && (() => {
|
||||
const filteredRows = data.table_rows.filter((row: any) => {
|
||||
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">
|
||||
Showing {((currentPage - 1) * itemsPerPage) + 1} to {Math.min(currentPage * itemsPerPage, filteredRows.length)} of {filteredRows.length} results
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
<div className="flex items-center gap-2 px-3">
|
||||
<span className="text-sm">
|
||||
Page {currentPage} of {totalPages}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
{/* Pagination Controls */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center justify-between mt-4 pt-4 border-t">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Showing {startIndex + 1} to {Math.min(endIndex, filteredRows.length)} of {filteredRows.length}
|
||||
</div>
|
||||
<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
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
|
||||
disabled={currentPage === 1}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<span className="text-sm px-3">
|
||||
{currentPage} / {totalPages}
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
|
||||
disabled={currentPage === totalPages}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<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>
|
||||
</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>
|
||||
|
||||
{/* 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 */}
|
||||
{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="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
@@ -571,7 +653,7 @@ export function DataView({ factType }: DataViewProps) {
|
||||
|
||||
{/* Chunk Detail Panel */}
|
||||
{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="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
@@ -1000,87 +1082,12 @@ function TimelineView({ data, onViewDocument }: { data: any; onViewDocument: (id
|
||||
{/* Detail Panel */}
|
||||
{selectedItem && (
|
||||
<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">
|
||||
<div className="flex justify-between items-start mb-3">
|
||||
<h3 className="text-sm font-semibold text-card-foreground">Details</h3>
|
||||
<Button
|
||||
variant="ghost"
|
||||
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>
|
||||
<MemoryDetailPanel
|
||||
memory={selectedItem}
|
||||
onClose={() => setSelectedItem(null)}
|
||||
onViewDocument={onViewDocument}
|
||||
compact
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -3,11 +3,8 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { client } from '@/lib/api';
|
||||
import { useBank } from '@/lib/bank-context';
|
||||
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
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 { X } from 'lucide-react';
|
||||
|
||||
@@ -18,16 +15,6 @@ export function DocumentsView() {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
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
|
||||
const [selectedDocument, setSelectedDocument] = useState<any>(null);
|
||||
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
|
||||
useEffect(() => {
|
||||
if (currentBank) {
|
||||
@@ -123,94 +66,6 @@ export function DocumentsView() {
|
||||
|
||||
return (
|
||||
<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 */}
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
|
||||
@@ -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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
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 { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { Info } from 'lucide-react';
|
||||
@@ -149,6 +151,7 @@ export function SearchDebugView() {
|
||||
trace: data.trace || null,
|
||||
loading: false,
|
||||
currentRetrievalFactType: defaultFactType,
|
||||
currentPhase: 'final',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error running search:', error);
|
||||
@@ -651,52 +654,43 @@ export function SearchDebugView() {
|
||||
|
||||
{/* Phase Controls */}
|
||||
{pane.trace && (
|
||||
<div className="p-2.5 bg-card border-b-2 border-primary flex gap-3">
|
||||
<label className="flex items-center gap-1.5 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name={`phase-${pane.id}`}
|
||||
checked={pane.currentPhase === 'retrieval'}
|
||||
onChange={() => updatePane(pane.id, { currentPhase: 'retrieval' })}
|
||||
/>
|
||||
<span className="text-xs font-bold">1. Retrieval</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-1.5 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name={`phase-${pane.id}`}
|
||||
checked={pane.currentPhase === 'rrf'}
|
||||
onChange={() => updatePane(pane.id, { currentPhase: 'rrf' })}
|
||||
/>
|
||||
<span className="text-xs font-bold">2. RRF Merge</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-1.5 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name={`phase-${pane.id}`}
|
||||
checked={pane.currentPhase === 'rerank'}
|
||||
onChange={() => updatePane(pane.id, { currentPhase: 'rerank' })}
|
||||
/>
|
||||
<span className="text-xs font-bold">3. Reranking</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-1.5 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name={`phase-${pane.id}`}
|
||||
checked={pane.currentPhase === 'json'}
|
||||
onChange={() => updatePane(pane.id, { currentPhase: 'json' })}
|
||||
/>
|
||||
<span className="text-xs font-bold">4. Raw JSON</span>
|
||||
</label>
|
||||
<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 className="p-2.5 bg-card border-b-2 border-primary">
|
||||
<RadioGroup
|
||||
value={pane.currentPhase}
|
||||
onValueChange={(value) => updatePane(pane.id, { currentPhase: value as Phase })}
|
||||
className="flex gap-3"
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<RadioGroupItem value="retrieval" id={`phase-retrieval-${pane.id}`} />
|
||||
<Label htmlFor={`phase-retrieval-${pane.id}`} className="text-xs font-bold cursor-pointer">
|
||||
1. Retrieval
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<RadioGroupItem value="rrf" id={`phase-rrf-${pane.id}`} />
|
||||
<Label htmlFor={`phase-rrf-${pane.id}`} className="text-xs font-bold cursor-pointer">
|
||||
2. RRF Merge
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<RadioGroupItem value="rerank" id={`phase-rerank-${pane.id}`} />
|
||||
<Label htmlFor={`phase-rerank-${pane.id}`} className="text-xs font-bold cursor-pointer">
|
||||
3. Reranking
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<RadioGroupItem value="json" id={`phase-json-${pane.id}`} />
|
||||
<Label htmlFor={`phase-json-${pane.id}`} className="text-xs font-bold cursor-pointer">
|
||||
4. Raw JSON
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<RadioGroupItem value="final" id={`phase-final-${pane.id}`} />
|
||||
<Label htmlFor={`phase-final-${pane.id}`} className="text-xs font-bold cursor-pointer">
|
||||
5. Final Results
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -2,142 +2,121 @@
|
||||
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { XIcon } from "lucide-react"
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||
}
|
||||
const Dialog = DialogPrimitive.Root
|
||||
|
||||
function DialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||
}
|
||||
const DialogTrigger = DialogPrimitive.Trigger
|
||||
|
||||
function DialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||
}
|
||||
const DialogPortal = DialogPrimitive.Portal
|
||||
|
||||
function DialogClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||
}
|
||||
const DialogClose = DialogPrimitive.Close
|
||||
|
||||
function DialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"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
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
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 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
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
))
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close
|
||||
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>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<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(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogTitle({
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("text-lg leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogHeader.displayName = "DialogHeader"
|
||||
|
||||
function DialogDescription({
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<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
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogOverlay,
|
||||
DialogClose,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
@@ -913,12 +913,12 @@ class BenchmarkRunner:
|
||||
# Only clear on first item for shared agent_id
|
||||
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:
|
||||
has_data = await self._agent_has_data(item_agent_id)
|
||||
if has_data:
|
||||
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 - agent '{item_agent_id}' already has indexed data")
|
||||
if item_id in existing_item_ids:
|
||||
console.print(f"\n[bold blue]Item {i}/{len(items)}[/bold blue] (ID: {item_id})")
|
||||
console.print(f" [yellow]⊘[/yellow] Skipping - already has results in output file")
|
||||
continue
|
||||
|
||||
result = await self.process_single_item(
|
||||
@@ -981,12 +981,11 @@ class BenchmarkRunner:
|
||||
item_id = self.dataset.get_item_id(item)
|
||||
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:
|
||||
has_data = await self._agent_has_data(item_agent_id)
|
||||
if has_data:
|
||||
if item_id in existing_item_ids:
|
||||
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
|
||||
|
||||
# Process the item
|
||||
|
||||
@@ -551,7 +551,7 @@ if __name__ == "__main__":
|
||||
parser.add_argument(
|
||||
"--fill",
|
||||
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(
|
||||
"--question-id",
|
||||
|
||||
@@ -30,6 +30,10 @@ Associate retained content with a document:
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
from hindsight_client import Hindsight
|
||||
|
||||
client = Hindsight(base_url="http://localhost:8888")
|
||||
|
||||
# Retain with document ID
|
||||
client.retain(
|
||||
bank_id="my-bank",
|
||||
@@ -40,7 +44,7 @@ client.retain(
|
||||
# Batch retain for a document
|
||||
client.retain_batch(
|
||||
bank_id="my-bank",
|
||||
contents=[
|
||||
items=[
|
||||
{"content": "Item 1: Product launch delayed to Q2"},
|
||||
{"content": "Item 2: New hiring targets announced"},
|
||||
{"content": "Item 3: Budget approved for ML team"}
|
||||
@@ -60,24 +64,22 @@ with open("notes.txt") as f:
|
||||
</TabItem>
|
||||
<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
|
||||
await client.retain({
|
||||
bankId: 'my-bank',
|
||||
content: 'Alice presented the Q4 roadmap...',
|
||||
documentId: 'meeting-2024-03-15'
|
||||
await client.retain('my-bank', 'Alice presented the Q4 roadmap...', {
|
||||
document_id: 'meeting-2024-03-15'
|
||||
});
|
||||
|
||||
// Batch retain
|
||||
await client.retainBatch({
|
||||
bankId: 'my-bank',
|
||||
contents: [
|
||||
{ content: 'Item 1: Product launch delayed to Q2' },
|
||||
{ content: 'Item 2: New hiring targets announced' },
|
||||
{ content: 'Item 3: Budget approved for ML team' }
|
||||
],
|
||||
documentId: 'meeting-2024-03-15'
|
||||
});
|
||||
await client.retainBatch('my-bank', [
|
||||
{ content: 'Item 1: Product launch delayed to Q2' },
|
||||
{ content: 'Item 2: New hiring targets announced' },
|
||||
{ content: 'Item 3: Budget approved for ML team' }
|
||||
], { documentId: 'meeting-2024-03-15' });
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
@@ -120,19 +122,15 @@ client.retain(
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
|
||||
```javascript
|
||||
```typescript
|
||||
// Original
|
||||
await client.retain({
|
||||
bankId: 'my-bank',
|
||||
content: 'Project deadline: March 31',
|
||||
documentId: 'project-plan'
|
||||
await client.retain('my-bank', 'Project deadline: March 31', {
|
||||
document_id: 'project-plan'
|
||||
});
|
||||
|
||||
// Update
|
||||
await client.retain({
|
||||
bankId: 'my-bank',
|
||||
content: 'Project deadline: April 15 (extended)',
|
||||
documentId: 'project-plan'
|
||||
await client.retain('my-bank', 'Project deadline: April 15 (extended)', {
|
||||
document_id: 'project-plan'
|
||||
});
|
||||
```
|
||||
|
||||
@@ -158,16 +156,23 @@ View all documents in a memory bank:
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
# List all documents
|
||||
documents = client.list_documents(bank_id="my-bank")
|
||||
# Using the low-level API
|
||||
from hindsight_client_api import ApiClient, Configuration
|
||||
from hindsight_client_api.api import DefaultApi
|
||||
|
||||
for doc in documents:
|
||||
print(f"{doc['id']}: {doc['memory_count']} memories")
|
||||
print(f" Created: {doc['created_at']}")
|
||||
print(f" Updated: {doc['updated_at']}")
|
||||
config = Configuration(host="http://localhost:8888")
|
||||
api_client = ApiClient(config)
|
||||
api = DefaultApi(api_client)
|
||||
|
||||
# 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
|
||||
documents = client.list_documents(
|
||||
response = api.list_documents(
|
||||
bank_id="my-bank",
|
||||
limit=50,
|
||||
offset=0
|
||||
@@ -177,17 +182,21 @@ documents = client.list_documents(
|
||||
</TabItem>
|
||||
<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
|
||||
const documents = await client.listDocuments({
|
||||
bankId: 'my-bank'
|
||||
const response = await sdk.listDocuments({
|
||||
client: apiClient,
|
||||
path: { bank_id: 'my-bank' }
|
||||
});
|
||||
|
||||
documents.forEach(doc => {
|
||||
console.log(`${doc.id}: ${doc.memoryCount} memories`);
|
||||
console.log(` Created: ${doc.createdAt}`);
|
||||
console.log(` Updated: ${doc.updatedAt}`);
|
||||
});
|
||||
for (const doc of response.data.items) {
|
||||
console.log(`${doc.id}: ${doc.memory_unit_count} memories`);
|
||||
console.log(` Created: ${doc.created_at}`);
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
@@ -206,52 +215,36 @@ hindsight documents list my-bank --limit 50
|
||||
|
||||
## Get Document Details
|
||||
|
||||
Retrieve a specific document with its memories:
|
||||
Retrieve a specific document with its content:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
# Get document
|
||||
doc = client.get_document(
|
||||
doc = api.get_document(
|
||||
bank_id="my-bank",
|
||||
document_id="meeting-2024-03-15"
|
||||
)
|
||||
|
||||
print(f"Document: {doc['id']}")
|
||||
print(f"Original text: {doc['original_text'][:200]}...")
|
||||
print(f"Memories: {doc['memory_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']}")
|
||||
print(f"Document: {doc.id}")
|
||||
print(f"Original text: {doc.original_text[:200]}...")
|
||||
print(f"Memories: {doc.memory_unit_count}")
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
|
||||
```javascript
|
||||
```typescript
|
||||
// Get document
|
||||
const doc = await client.getDocument({
|
||||
bankId: 'my-bank',
|
||||
documentId: 'meeting-2024-03-15'
|
||||
const doc = await sdk.getDocument({
|
||||
client: apiClient,
|
||||
path: { bank_id: 'my-bank', document_id: 'meeting-2024-03-15' }
|
||||
});
|
||||
|
||||
console.log(`Document: ${doc.id}`);
|
||||
console.log(`Memories: ${doc.memoryCount}`);
|
||||
|
||||
// Get with memories
|
||||
const withMemories = await client.getDocument({
|
||||
bankId: 'my-bank',
|
||||
documentId: 'meeting-2024-03-15',
|
||||
includeMemories: true
|
||||
});
|
||||
console.log(`Document: ${doc.data.id}`);
|
||||
console.log(`Original text: ${doc.data.original_text.substring(0, 200)}...`);
|
||||
console.log(`Memories: ${doc.data.memory_unit_count}`);
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
@@ -260,9 +253,6 @@ const withMemories = await client.getDocument({
|
||||
```bash
|
||||
# Get document
|
||||
hindsight documents get my-bank meeting-2024-03-15
|
||||
|
||||
# With memories
|
||||
hindsight documents get my-bank meeting-2024-03-15 --include-memories
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
@@ -277,31 +267,31 @@ Remove a document and all its memories:
|
||||
|
||||
```python
|
||||
# Delete document (removes all associated memories)
|
||||
client.delete_document(
|
||||
api.delete_document(
|
||||
bank_id="my-bank",
|
||||
document_id="old-meeting"
|
||||
)
|
||||
|
||||
# Bulk delete
|
||||
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 value="node" label="Node.js">
|
||||
|
||||
```javascript
|
||||
```typescript
|
||||
// Delete document
|
||||
await client.deleteDocument({
|
||||
bankId: 'my-bank',
|
||||
documentId: 'old-meeting'
|
||||
await sdk.deleteDocument({
|
||||
client: apiClient,
|
||||
path: { bank_id: 'my-bank', document_id: 'old-meeting' }
|
||||
});
|
||||
|
||||
// Bulk delete
|
||||
for (const docId of ['old-1', 'old-2', 'old-3']) {
|
||||
await client.deleteDocument({
|
||||
bankId: 'my-bank',
|
||||
documentId: docId
|
||||
await sdk.deleteDocument({
|
||||
client: apiClient,
|
||||
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",
|
||||
"bank_id": "my-bank",
|
||||
"original_text": "Alice presented the Q4 roadmap...",
|
||||
"content_hash": "sha256:abc123...",
|
||||
"memory_count": 12,
|
||||
"memory_unit_count": 12,
|
||||
"created_at": "2024-03-15T14:00:00Z",
|
||||
"updated_at": "2024-03-15T14:00:00Z",
|
||||
"metadata": {}
|
||||
"retain_params": {
|
||||
"context": "team meeting",
|
||||
"event_date": "2024-03-15"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -339,7 +330,12 @@ hindsight documents delete my-bank old-meeting --confirm
|
||||
|
||||
### Meeting Notes
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
from datetime import date
|
||||
|
||||
# Store meeting notes with date-based IDs
|
||||
client.retain(
|
||||
bank_id="team-memory",
|
||||
@@ -348,10 +344,21 @@ client.retain(
|
||||
)
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Documentation
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
|
||||
# Store docs with version tracking
|
||||
docs_dir = Path("docs")
|
||||
version = "1.0"
|
||||
|
||||
for file in docs_dir.glob("*.md"):
|
||||
client.retain(
|
||||
bank_id="docs-memory",
|
||||
@@ -360,8 +367,14 @@ for file in docs_dir.glob("*.md"):
|
||||
)
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Conversation History
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
# Store chat history with session IDs
|
||||
client.retain(
|
||||
@@ -371,6 +384,9 @@ client.retain(
|
||||
)
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [**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:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
from hindsight_client import Hindsight
|
||||
|
||||
client = Hindsight(base_url="http://localhost:8888")
|
||||
|
||||
client.retain(
|
||||
bank_id="my-bank",
|
||||
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:**
|
||||
- **Alice** (person)
|
||||
- **Google** (organization)
|
||||
@@ -46,14 +67,22 @@ Get all entities tracked in a memory bank:
|
||||
<TabItem value="python" label="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
|
||||
entities = client.list_entities(bank_id="my-bank")
|
||||
response = api.list_entities(bank_id="my-bank")
|
||||
|
||||
for entity in entities:
|
||||
print(f"{entity['name']}: {entity['mention_count']} mentions")
|
||||
for entity in response.items:
|
||||
print(f"{entity.canonical_name}: {entity.mention_count} mentions")
|
||||
|
||||
# List with filters
|
||||
entities = client.list_entities(
|
||||
# List with pagination
|
||||
response = api.list_entities(
|
||||
bank_id="my-bank",
|
||||
limit=50,
|
||||
offset=0
|
||||
@@ -63,21 +92,26 @@ entities = client.list_entities(
|
||||
</TabItem>
|
||||
<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
|
||||
const entities = await client.listEntities({
|
||||
bankId: 'my-bank'
|
||||
const response = await sdk.listEntities({
|
||||
client: apiClient,
|
||||
path: { bank_id: 'my-bank' }
|
||||
});
|
||||
|
||||
entities.forEach(e => {
|
||||
console.log(`${e.name}: ${e.mentionCount} mentions`);
|
||||
});
|
||||
for (const entity of response.data.items) {
|
||||
console.log(`${entity.canonical_name}: ${entity.mention_count} mentions`);
|
||||
}
|
||||
|
||||
// List with filters
|
||||
const filtered = await client.listEntities({
|
||||
bankId: 'my-bank',
|
||||
limit: 50,
|
||||
offset: 0
|
||||
// List with pagination
|
||||
const paginated = await sdk.listEntities({
|
||||
client: apiClient,
|
||||
path: { bank_id: 'my-bank' },
|
||||
query: { limit: 50, offset: 0 }
|
||||
});
|
||||
```
|
||||
|
||||
@@ -103,58 +137,39 @@ Retrieve detailed information about a specific entity:
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
# Get entity state (observations + related facts)
|
||||
entity = client.get_entity(
|
||||
# Get entity details with observations
|
||||
entity = api.get_entity(
|
||||
bank_id="my-bank",
|
||||
entity_id="entity-uuid"
|
||||
)
|
||||
|
||||
print(f"Entity: {entity['name']}")
|
||||
print(f"First seen: {entity['first_seen']}")
|
||||
print(f"Mentions: {entity['mention_count']}")
|
||||
print(f"Entity: {entity.canonical_name}")
|
||||
print(f"First seen: {entity.first_seen}")
|
||||
print(f"Mentions: {entity.mention_count}")
|
||||
|
||||
# Observations (synthesized summaries)
|
||||
for obs in entity['observations']:
|
||||
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']}")
|
||||
for obs in entity.observations:
|
||||
print(f" - {obs.text}")
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
|
||||
```javascript
|
||||
// Get entity state
|
||||
const entity = await client.getEntity({
|
||||
bankId: 'my-bank',
|
||||
entityId: 'entity-uuid'
|
||||
```typescript
|
||||
// Get entity details
|
||||
const entity = await sdk.getEntity({
|
||||
client: apiClient,
|
||||
path: { bank_id: 'my-bank', entity_id: 'entity-uuid' }
|
||||
});
|
||||
|
||||
console.log(`Entity: ${entity.name}`);
|
||||
console.log(`First seen: ${entity.firstSeen}`);
|
||||
console.log(`Mentions: ${entity.mentionCount}`);
|
||||
console.log(`Entity: ${entity.data.canonical_name}`);
|
||||
console.log(`First seen: ${entity.data.first_seen}`);
|
||||
console.log(`Mentions: ${entity.data.mention_count}`);
|
||||
|
||||
// Observations
|
||||
entity.observations.forEach(obs => {
|
||||
for (const obs of entity.data.observations) {
|
||||
console.log(` - ${obs.text}`);
|
||||
});
|
||||
|
||||
// Include related facts
|
||||
const withFacts = await client.getEntity({
|
||||
bankId: 'my-bank',
|
||||
entityId: 'entity-uuid',
|
||||
includeFacts: true,
|
||||
maxFacts: 20
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
@@ -163,9 +178,6 @@ const withFacts = await client.getEntity({
|
||||
```bash
|
||||
# Get entity details
|
||||
hindsight entities get my-bank entity-uuid
|
||||
|
||||
# With related facts
|
||||
hindsight entities get my-bank entity-uuid --include-facts
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
@@ -185,50 +197,30 @@ Observations are high-level summaries automatically synthesized from multiple fa
|
||||
|
||||
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>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
# Search by name
|
||||
entities = client.search_entities(
|
||||
# Regenerate observations for an entity
|
||||
api.regenerate_entity_observations(
|
||||
bank_id="my-bank",
|
||||
query="Alice"
|
||||
)
|
||||
|
||||
# Fuzzy matching handles variations
|
||||
entities = client.search_entities(
|
||||
bank_id="my-bank",
|
||||
query="Alic" # Matches "Alice", "Alicia", etc.
|
||||
entity_id="entity-uuid"
|
||||
)
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
|
||||
```javascript
|
||||
// Search by name
|
||||
const entities = await client.searchEntities({
|
||||
bankId: 'my-bank',
|
||||
query: 'Alice'
|
||||
```typescript
|
||||
// Regenerate observations
|
||||
await sdk.regenerateEntityObservations({
|
||||
client: apiClient,
|
||||
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>
|
||||
@@ -239,7 +231,6 @@ hindsight entities search my-bank "Alice"
|
||||
```json
|
||||
{
|
||||
"id": "entity-uuid",
|
||||
"name": "Alice Chen",
|
||||
"canonical_name": "Alice Chen",
|
||||
"first_seen": "2024-01-15T10:30:00Z",
|
||||
"last_seen": "2024-03-20T14:22:00Z",
|
||||
@@ -247,7 +238,7 @@ hindsight entities search my-bank "Alice"
|
||||
"observations": [
|
||||
{
|
||||
"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
|
||||
---
|
||||
|
||||
# Memory bank Identity
|
||||
# Memory Bank Identity
|
||||
|
||||
Configure memory bank personality, background, and behavior.
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
## Creating an Memory bank
|
||||
## Creating a Memory Bank
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
@@ -19,8 +19,8 @@ from hindsight_client import Hindsight
|
||||
|
||||
client = Hindsight(base_url="http://localhost:8888")
|
||||
|
||||
client.create_agent(
|
||||
agent_id="my-agent",
|
||||
client.create_bank(
|
||||
bank_id="my-bank",
|
||||
name="Research Assistant",
|
||||
background="I am a research assistant specializing in machine learning",
|
||||
personality={
|
||||
@@ -38,11 +38,11 @@ client.create_agent(
|
||||
<TabItem value="node" label="Node.js">
|
||||
|
||||
```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',
|
||||
background: 'I am a research assistant specializing in machine learning',
|
||||
personality: {
|
||||
@@ -61,10 +61,10 @@ await ManagementService.createAgentApiAgentsAgentIdPut('my-agent', {
|
||||
|
||||
```bash
|
||||
# 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
|
||||
hindsight memory bank personality my-agent \
|
||||
hindsight agent personality my-bank \
|
||||
--openness 0.8 \
|
||||
--conscientiousness 0.7 \
|
||||
--extraversion 0.5 \
|
||||
@@ -90,72 +90,83 @@ Each trait is scored 0.0 to 1.0:
|
||||
|
||||
### 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
|
||||
# High openness agent
|
||||
# High openness bank
|
||||
"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."
|
||||
```
|
||||
|
||||
**Conscientiousness** affects structure and thoroughness:
|
||||
|
||||
```python
|
||||
# High conscientiousness agent
|
||||
# High conscientiousness bank
|
||||
"Here's a detailed, step-by-step analysis..."
|
||||
|
||||
# Low conscientiousness agent
|
||||
# Low conscientiousness bank
|
||||
"Quick take: this should work, let's try it."
|
||||
```
|
||||
|
||||
**Extraversion** shapes collaboration preferences:
|
||||
|
||||
```python
|
||||
# High extraversion agent
|
||||
# High extraversion bank
|
||||
"We should get the team together to discuss this."
|
||||
|
||||
# Low extraversion agent
|
||||
# Low extraversion bank
|
||||
"I'll analyze this independently and share my findings."
|
||||
```
|
||||
|
||||
**Agreeableness** affects how disagreements are handled:
|
||||
|
||||
```python
|
||||
# High agreeableness agent
|
||||
# High agreeableness bank
|
||||
"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."
|
||||
```
|
||||
|
||||
**Neuroticism** influences risk assessment:
|
||||
|
||||
```python
|
||||
# High neuroticism agent
|
||||
# High neuroticism bank
|
||||
"We should consider what could go wrong here..."
|
||||
|
||||
# Low neuroticism agent
|
||||
# Low neuroticism bank
|
||||
"The risks seem manageable, let's proceed."
|
||||
```
|
||||
|
||||
## Background
|
||||
|
||||
The background is a first-person narrative providing agent context:
|
||||
The background is a first-person narrative providing bank context:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
client.create_agent(
|
||||
agent_id="financial-advisor",
|
||||
client.create_bank(
|
||||
bank_id="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 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>
|
||||
</Tabs>
|
||||
|
||||
@@ -164,91 +175,43 @@ Background influences:
|
||||
- Perspective in responses
|
||||
- Opinion formation context
|
||||
|
||||
### Merging Background
|
||||
|
||||
New background information is merged intelligently:
|
||||
## Getting Bank Profile
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
# Original background
|
||||
client.create_agent(
|
||||
agent_id="assistant",
|
||||
background="I am a helpful AI assistant"
|
||||
)
|
||||
# Using the low-level API
|
||||
from hindsight_client_api import ApiClient, Configuration
|
||||
from hindsight_client_api.api import DefaultApi
|
||||
|
||||
# Add more context (merged, not replaced)
|
||||
client.update_background(
|
||||
agent_id="assistant",
|
||||
background="I specialize in Python programming"
|
||||
)
|
||||
config = Configuration(host="http://localhost:8888")
|
||||
api_client = ApiClient(config)
|
||||
api = DefaultApi(api_client)
|
||||
|
||||
# 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>
|
||||
</Tabs>
|
||||
<TabItem value="node" label="Node.js">
|
||||
|
||||
Merging rules:
|
||||
- **Conflicts**: New overwrites old
|
||||
- **Additions**: Non-conflicting info is added
|
||||
- **Normalization**: "You are..." → "I am..."
|
||||
```typescript
|
||||
const profile = await client.getBankProfile('my-bank');
|
||||
|
||||
## Getting Memory bank Profile
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
profile = client.get_profile(agent_id="my-agent")
|
||||
|
||||
print(f"Background: {profile['background']}")
|
||||
print(f"Personality: {profile['personality']}")
|
||||
console.log(`Name: ${profile.name}`);
|
||||
console.log(`Background: ${profile.background}`);
|
||||
console.log(`Personality:`, profile.personality);
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="cli" label="CLI">
|
||||
|
||||
```bash
|
||||
hindsight memory bank profile my-agent
|
||||
```
|
||||
|
||||
</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
|
||||
hindsight agent profile my-bank
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
@@ -256,7 +219,7 @@ hindsight agent list
|
||||
|
||||
## Default Values
|
||||
|
||||
If not specified, memory banks use neutral defaults:
|
||||
If not specified, banks use neutral defaults:
|
||||
|
||||
```python
|
||||
{
|
||||
@@ -287,9 +250,9 @@ Common personality configurations:
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
# Customer support agent
|
||||
client.create_agent(
|
||||
agent_id="support",
|
||||
# Customer support bank
|
||||
client.create_bank(
|
||||
bank_id="support",
|
||||
background="I am a friendly customer support agent",
|
||||
personality={
|
||||
"openness": 0.5,
|
||||
@@ -301,9 +264,9 @@ client.create_agent(
|
||||
}
|
||||
)
|
||||
|
||||
# Code reviewer agent
|
||||
client.create_agent(
|
||||
agent_id="reviewer",
|
||||
# Code reviewer bank
|
||||
client.create_bank(
|
||||
bank_id="reviewer",
|
||||
background="I am a thorough code reviewer focused on quality",
|
||||
personality={
|
||||
"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>
|
||||
</Tabs>
|
||||
|
||||
## Memory bank Isolation
|
||||
## Bank Isolation
|
||||
|
||||
Each agent has:
|
||||
- **Separate memories** — memory banks don't share memories
|
||||
- **Own personality** — traits are per-agent
|
||||
Each bank has:
|
||||
- **Separate memories** — banks don't share memories
|
||||
- **Own personality** — traits are per-bank
|
||||
- **Independent opinions** — formed from their own experiences
|
||||
|
||||
```python
|
||||
# Store to agent A
|
||||
client.store(agent_id="agent-a", content="Python is great")
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
# Memory bank B doesn't see it
|
||||
results = client.search(agent_id="agent-b", query="Python")
|
||||
```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
|
||||
```
|
||||
|
||||
</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">
|
||||
|
||||
```python
|
||||
from hindsight_client import Hindsight
|
||||
|
||||
client = Hindsight(base_url="http://localhost:8888")
|
||||
|
||||
# Start async batch retain
|
||||
operation = client.retain_batch_async(
|
||||
result = client.retain_batch(
|
||||
bank_id="my-bank",
|
||||
contents=[
|
||||
items=[
|
||||
{"content": doc1_text},
|
||||
{"content": doc2_text},
|
||||
# ... hundreds or thousands of documents
|
||||
]
|
||||
],
|
||||
async_=True # Enable async mode
|
||||
)
|
||||
|
||||
print(f"Operation ID: {operation['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")
|
||||
print(f"Operation ID: {result.get('operation_id')}")
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<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
|
||||
const operation = await client.retainBatchAsync({
|
||||
bankId: 'my-bank',
|
||||
contents: [
|
||||
{ content: doc1Text },
|
||||
{ content: doc2Text },
|
||||
// ... hundreds or thousands of documents
|
||||
]
|
||||
});
|
||||
const result = await client.retainBatch('my-bank', [
|
||||
{ content: doc1Text },
|
||||
{ content: doc2Text },
|
||||
// ... hundreds or thousands of documents
|
||||
], { async: true });
|
||||
|
||||
console.log(`Operation ID: ${operation.operationId}`);
|
||||
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`);
|
||||
}
|
||||
console.log(`Operation ID: ${result.operation_id}`);
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
@@ -119,12 +75,6 @@ if (finalStatus.status === 'completed') {
|
||||
hindsight retain my-bank --files docs/*.md --async
|
||||
|
||||
# Returns operation ID: op-abc123...
|
||||
|
||||
# Check status
|
||||
hindsight operations get my-bank op-abc123
|
||||
|
||||
# Watch progress
|
||||
hindsight operations watch my-bank op-abc123
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
@@ -138,54 +88,45 @@ View all operations for a memory bank:
|
||||
<TabItem value="python" label="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
|
||||
operations = client.list_operations(bank_id="my-bank")
|
||||
response = api.list_operations(bank_id="my-bank")
|
||||
|
||||
for op in operations:
|
||||
print(f"{op['operation_id']}: {op['type']} - {op['status']}")
|
||||
if op['status'] == 'running':
|
||||
print(f" Progress: {op['progress']}/{op['total']}")
|
||||
|
||||
# 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
|
||||
)
|
||||
for op in response.items:
|
||||
print(f"{op.id}: {op.task_type} - {op.status}")
|
||||
print(f" Items: {op.items_count}")
|
||||
if op.error_message:
|
||||
print(f" Error: {op.error_message}")
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<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
|
||||
const operations = await client.listOperations({
|
||||
bankId: 'my-bank'
|
||||
const response = await sdk.listOperations({
|
||||
client: apiClient,
|
||||
path: { bank_id: 'my-bank' }
|
||||
});
|
||||
|
||||
operations.forEach(op => {
|
||||
console.log(`${op.operationId}: ${op.type} - ${op.status}`);
|
||||
if (op.status === 'running') {
|
||||
console.log(` Progress: ${op.progress}/${op.total}`);
|
||||
for (const op of response.data.items) {
|
||||
console.log(`${op.id}: ${op.task_type} - ${op.status}`);
|
||||
console.log(` Items: ${op.items_count}`);
|
||||
if (op.error_message) {
|
||||
console.log(` Error: ${op.error_message}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Filter by status
|
||||
const pending = await client.listOperations({
|
||||
bankId: 'my-bank',
|
||||
status: 'pending'
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
@@ -214,38 +155,41 @@ Stop a running or pending operation:
|
||||
|
||||
```python
|
||||
# Cancel operation
|
||||
client.cancel_operation(
|
||||
api.cancel_operation(
|
||||
bank_id="my-bank",
|
||||
operation_id="op-abc123"
|
||||
)
|
||||
|
||||
# Cancel all pending operations
|
||||
operations = client.list_operations(bank_id="my-bank", status="pending")
|
||||
for op in operations:
|
||||
client.cancel_operation(bank_id="my-bank", operation_id=op['operation_id'])
|
||||
response = api.list_operations(bank_id="my-bank")
|
||||
for op in response.items:
|
||||
if op.status == "pending":
|
||||
api.cancel_operation(bank_id="my-bank", operation_id=op.id)
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
|
||||
```javascript
|
||||
```typescript
|
||||
// Cancel operation
|
||||
await client.cancelOperation({
|
||||
bankId: 'my-bank',
|
||||
operationId: 'op-abc123'
|
||||
await sdk.cancelOperation({
|
||||
client: apiClient,
|
||||
path: { bank_id: 'my-bank', operation_id: 'op-abc123' }
|
||||
});
|
||||
|
||||
// Cancel all pending
|
||||
const pending = await client.listOperations({
|
||||
bankId: 'my-bank',
|
||||
status: 'pending'
|
||||
const ops = await sdk.listOperations({
|
||||
client: apiClient,
|
||||
path: { bank_id: 'my-bank' }
|
||||
});
|
||||
|
||||
for (const op of pending) {
|
||||
await client.cancelOperation({
|
||||
bankId: 'my-bank',
|
||||
operationId: op.operationId
|
||||
});
|
||||
for (const op of ops.data.items) {
|
||||
if (op.status === 'pending') {
|
||||
await sdk.cancelOperation({
|
||||
client: apiClient,
|
||||
path: { bank_id: 'my-bank', operation_id: op.id }
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -285,17 +229,14 @@ hindsight operations cancel my-bank --all-pending
|
||||
|
||||
```json
|
||||
{
|
||||
"operation_id": "op-abc123",
|
||||
"id": "op-abc123",
|
||||
"bank_id": "my-bank",
|
||||
"type": "batch_retain",
|
||||
"status": "running",
|
||||
"progress": 450,
|
||||
"total": 1000,
|
||||
"task_type": "batch_retain",
|
||||
"status": "completed",
|
||||
"items_count": 1000,
|
||||
"document_id": "batch-001",
|
||||
"created_at": "2024-03-15T10:00:00Z",
|
||||
"started_at": "2024-03-15T10:00:05Z",
|
||||
"completed_at": null,
|
||||
"error": null,
|
||||
"result": null
|
||||
"error_message": null
|
||||
}
|
||||
```
|
||||
|
||||
@@ -303,39 +244,70 @@ hindsight operations cancel my-bank --all-pending
|
||||
|
||||
### Polling
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
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:
|
||||
status = client.get_operation(bank_id=bank_id, operation_id=operation_id)
|
||||
response = api.list_operations(bank_id=bank_id)
|
||||
|
||||
if status['status'] == 'completed':
|
||||
return status['result']
|
||||
elif status['status'] == 'failed':
|
||||
raise Exception(f"Operation failed: {status['error']}")
|
||||
elif status['status'] == 'cancelled':
|
||||
raise Exception("Operation was cancelled")
|
||||
pending_or_running = [
|
||||
op for op in response.items
|
||||
if op.status in ['pending', 'running']
|
||||
]
|
||||
|
||||
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)
|
||||
|
||||
# Use it
|
||||
result = wait_for_operation(client, "my-bank", op_id)
|
||||
print(f"Created {len(result['memory_ids'])} memories")
|
||||
wait_for_operations(api, "my-bank")
|
||||
```
|
||||
|
||||
### Webhooks (Coming Soon)
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
|
||||
```python
|
||||
# Configure webhook for operation completion
|
||||
client.configure_webhook(
|
||||
bank_id="my-bank",
|
||||
url="https://myapp.com/webhooks/hindsight",
|
||||
events=["operation.completed", "operation.failed"]
|
||||
)
|
||||
```typescript
|
||||
async function waitForOperations(apiClient: any, bankId: string, pollInterval = 5000) {
|
||||
while (true) {
|
||||
const response = await sdk.listOperations({
|
||||
client: apiClient,
|
||||
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
|
||||
|
||||
**Use async for large batches:**
|
||||
@@ -343,11 +315,11 @@ client.configure_webhook(
|
||||
- Async: > 100 items or > 100KB
|
||||
|
||||
**Monitor progress:**
|
||||
- Check `progress` / `total` fields
|
||||
- Check `items_count` field
|
||||
- Poll every 5-10 seconds
|
||||
|
||||
**Handle failures:**
|
||||
- Check `error` field for details
|
||||
- Check `error_message` field for details
|
||||
- Retry with exponential backoff
|
||||
- Break large batches into smaller chunks
|
||||
|
||||
|
||||
@@ -19,8 +19,8 @@ from hindsight_client import Hindsight
|
||||
|
||||
client = Hindsight(base_url="http://localhost:8888")
|
||||
|
||||
results = client.search(
|
||||
agent_id="my-agent",
|
||||
results = client.recall(
|
||||
bank_id="my-bank",
|
||||
query="What does Alice do?"
|
||||
)
|
||||
|
||||
@@ -32,16 +32,13 @@ for r in results:
|
||||
<TabItem value="node" label="Node.js">
|
||||
|
||||
```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({
|
||||
agent_id: 'my-agent',
|
||||
query: 'What does Alice do?'
|
||||
});
|
||||
const results = await client.recall('my-bank', 'What does Alice do?');
|
||||
|
||||
for (const r of results.results) {
|
||||
for (const r of results) {
|
||||
console.log(`${r.text} (score: ${r.weight})`);
|
||||
}
|
||||
```
|
||||
@@ -50,7 +47,7 @@ for (const r of results.results) {
|
||||
<TabItem value="cli" label="CLI">
|
||||
|
||||
```bash
|
||||
hindsight memory search my-agent "What does Alice do?"
|
||||
hindsight memory search my-bank "What does Alice do?"
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
@@ -61,27 +58,85 @@ hindsight memory search my-agent "What does Alice do?"
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `query` | string | required | Natural language query |
|
||||
| `top_k` | int | 10 | Maximum results to return |
|
||||
| `budget` | Budget | MID | Budget level: LOW (100), MID (300), HIGH (600) nodes |
|
||||
| `fact_type` | list | all | Filter: `world`, `agent`, `opinion` |
|
||||
| `types` | list | all | Filter: `world`, `agent`, `opinion` |
|
||||
| `budget` | string | "mid" | Budget level: "low", "mid", "high" |
|
||||
| `max_tokens` | int | 4096 | Token budget for results |
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
from hindsight_api.engine.memory_engine import Budget
|
||||
|
||||
results = client.recall(
|
||||
bank_id="my-agent",
|
||||
bank_id="my-bank",
|
||||
query="What does Alice do?",
|
||||
top_k=20,
|
||||
budget=Budget.HIGH,
|
||||
fact_type=["world", "agent"],
|
||||
types=["world", "agent"],
|
||||
budget="high",
|
||||
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>
|
||||
</Tabs>
|
||||
|
||||
@@ -94,17 +149,17 @@ Hindsight automatically detects time expressions and activates temporal search:
|
||||
|
||||
```python
|
||||
# These queries activate temporal-graph retrieval
|
||||
results = client.search(agent_id="my-agent", query="What did Alice do last spring?")
|
||||
results = client.search(agent_id="my-agent", 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="What did Alice do last spring?")
|
||||
results = client.recall(bank_id="my-bank", query="What happened in June?")
|
||||
results = client.recall(bank_id="my-bank", query="Events from last year")
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="cli" label="CLI">
|
||||
|
||||
```bash
|
||||
hindsight memory search my-agent "What did Alice do last spring?"
|
||||
hindsight memory search my-agent "What happened between March and May?"
|
||||
hindsight memory search my-bank "What did Alice do last spring?"
|
||||
hindsight memory search my-bank "What happened between March and May?"
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
@@ -129,31 +184,31 @@ Search specific memory networks:
|
||||
|
||||
```python
|
||||
# Only world facts (objective information)
|
||||
world_facts = client.search_memories(
|
||||
agent_id="my-agent",
|
||||
world_facts = client.recall(
|
||||
bank_id="my-bank",
|
||||
query="Where does Alice work?",
|
||||
fact_type=["world"]
|
||||
types=["world"]
|
||||
)
|
||||
|
||||
# Only agent facts (memory bank's own experiences)
|
||||
agent_facts = client.search_memories(
|
||||
agent_id="my-agent",
|
||||
agent_facts = client.recall(
|
||||
bank_id="my-bank",
|
||||
query="What have I recommended?",
|
||||
fact_type=["agent"]
|
||||
types=["agent"]
|
||||
)
|
||||
|
||||
# Only opinions (formed beliefs)
|
||||
opinions = client.search_memories(
|
||||
agent_id="my-agent",
|
||||
opinions = client.recall(
|
||||
bank_id="my-bank",
|
||||
query="What do I think about Python?",
|
||||
fact_type=["opinion"]
|
||||
types=["opinion"]
|
||||
)
|
||||
|
||||
# World and agent facts (exclude opinions)
|
||||
facts = client.search_memories(
|
||||
agent_id="my-agent",
|
||||
facts = client.recall(
|
||||
bank_id="my-bank",
|
||||
query="What happened?",
|
||||
fact_type=["world", "agent"]
|
||||
types=["world", "agent"]
|
||||
)
|
||||
```
|
||||
|
||||
@@ -161,8 +216,8 @@ facts = client.search_memories(
|
||||
<TabItem value="cli" label="CLI">
|
||||
|
||||
```bash
|
||||
hindsight memory search my-agent "Python" --fact-type opinion
|
||||
hindsight memory search my-agent "Alice" --fact-type world,agent
|
||||
hindsight memory search my-bank "Python" --fact-type opinion
|
||||
hindsight memory search my-bank "Alice" --fact-type world,agent
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
@@ -225,16 +280,31 @@ graph LR
|
||||
|
||||
The `budget` parameter controls graph traversal depth:
|
||||
|
||||
- **Budget.LOW (100 nodes)**: Fast, shallow search — good for simple lookups
|
||||
- **Budget.MID (300 nodes)**: Balanced — default for most queries
|
||||
- **Budget.HIGH (600 nodes)**: Deep exploration — finds indirect connections
|
||||
- **"low" (100 nodes)**: Fast, shallow search — good for simple lookups
|
||||
- **"mid" (300 nodes)**: Balanced — default for most queries
|
||||
- **"high" (600 nodes)**: Deep exploration — finds indirect connections
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
from hindsight_api.engine.memory_engine import Budget
|
||||
|
||||
# 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
|
||||
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
|
||||
---
|
||||
|
||||
# Think
|
||||
# Reflect
|
||||
|
||||
Generate personality-aware responses using retrieved memories.
|
||||
|
||||
@@ -19,38 +19,35 @@ from hindsight_client import Hindsight
|
||||
|
||||
client = Hindsight(base_url="http://localhost:8888")
|
||||
|
||||
answer = client.think(
|
||||
agent_id="my-agent",
|
||||
response = client.reflect(
|
||||
bank_id="my-bank",
|
||||
query="What should I know about Alice?"
|
||||
)
|
||||
|
||||
print(answer["text"])
|
||||
print(response["answer"])
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
|
||||
```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({
|
||||
agent_id: 'my-agent',
|
||||
query: 'What should I know about Alice?'
|
||||
});
|
||||
const response = await client.reflect('my-bank', 'What should I know about Alice?');
|
||||
|
||||
console.log(response.text);
|
||||
console.log(response.answer);
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="cli" label="CLI">
|
||||
|
||||
```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
|
||||
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>
|
||||
@@ -60,26 +57,21 @@ hindsight memory think my-agent "What should I know about Alice?" -v
|
||||
|
||||
```python
|
||||
{
|
||||
"text": "Alice is a software engineer at Google who joined last year...",
|
||||
"based_on": {
|
||||
"world": [
|
||||
{"text": "Alice works at Google", "weight": 0.95, "id": "..."}
|
||||
],
|
||||
"agent": [],
|
||||
"opinion": [
|
||||
{"text": "Alice is very competent", "weight": 0.82, "id": "..."}
|
||||
]
|
||||
},
|
||||
"answer": "Alice is a software engineer at Google who joined last year...",
|
||||
"facts_used": [
|
||||
{"text": "Alice works at Google", "weight": 0.95, "id": "..."},
|
||||
{"text": "Alice is very competent", "weight": 0.82, "id": "..."}
|
||||
],
|
||||
"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 |
|
||||
|-------|-------------|
|
||||
| `text` | Generated response |
|
||||
| `based_on` | Memories used, grouped by type |
|
||||
| `answer` | Generated response |
|
||||
| `facts_used` | Memories used in generation |
|
||||
| `new_opinions` | New opinions formed during reasoning |
|
||||
|
||||
## Parameters
|
||||
@@ -87,26 +79,35 @@ hindsight memory think my-agent "What should I know about Alice?" -v
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `query` | string | required | Question or prompt |
|
||||
| `budget` | Budget | LOW | Budget level: LOW (100), MID (300), HIGH (600) nodes |
|
||||
| `top_k` | int | 10 | Max memories to retrieve |
|
||||
| `budget` | string | "low" | Budget level: "low", "mid", "high" |
|
||||
| `context` | string | None | Additional context for the query |
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
from hindsight_api.engine.memory_engine import Budget
|
||||
|
||||
answer = client.reflect(
|
||||
bank_id="my-agent",
|
||||
response = client.reflect(
|
||||
bank_id="my-bank",
|
||||
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>
|
||||
</Tabs>
|
||||
|
||||
## What Think Does
|
||||
## What Reflect Does
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
@@ -115,10 +116,10 @@ sequenceDiagram
|
||||
participant M as Memory Store
|
||||
participant L as LLM
|
||||
|
||||
C->>A: think("What about Alice?")
|
||||
C->>A: reflect("What about Alice?")
|
||||
A->>M: Search all networks
|
||||
M-->>A: World + Memory bank + Opinion facts
|
||||
A->>A: Load memory bank personality
|
||||
M-->>A: World + Bank + Opinion facts
|
||||
A->>A: Load bank personality
|
||||
A->>L: Generate with personality context
|
||||
L-->>A: Response + new opinions
|
||||
A->>M: Store new opinions
|
||||
@@ -126,28 +127,28 @@ sequenceDiagram
|
||||
```
|
||||
|
||||
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
|
||||
4. **Forms opinions** if the query warrants it
|
||||
5. **Returns** response with sources and any new opinions
|
||||
|
||||
## Opinion Formation
|
||||
|
||||
Think can form new opinions based on evidence:
|
||||
Reflect can form new opinions based on evidence:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
answer = client.think(
|
||||
agent_id="my-agent",
|
||||
response = client.reflect(
|
||||
bank_id="my-bank",
|
||||
query="What do you think about Python vs JavaScript for data science?"
|
||||
)
|
||||
|
||||
# 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: [
|
||||
# {"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
|
||||
|
||||
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 **Conscientiousness** | More structured, methodical responses |
|
||||
@@ -168,10 +169,13 @@ The memory bank's personality affects Think responses:
|
||||
| High **Agreeableness** | More diplomatic, harmony-seeking |
|
||||
| High **Neuroticism** | More risk-aware, cautious |
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
# Create a memory bank with specific personality
|
||||
client.create_agent(
|
||||
agent_id="cautious-advisor",
|
||||
# Create a bank with specific personality
|
||||
client.create_bank(
|
||||
bank_id="cautious-advisor",
|
||||
background="I am a risk-aware financial advisor",
|
||||
personality={
|
||||
"openness": 0.3,
|
||||
@@ -181,28 +185,69 @@ client.create_agent(
|
||||
}
|
||||
)
|
||||
|
||||
# Think responses will reflect this personality
|
||||
answer = client.think(
|
||||
agent_id="cautious-advisor",
|
||||
# Reflect responses will reflect this personality
|
||||
response = client.reflect(
|
||||
bank_id="cautious-advisor",
|
||||
query="Should I invest in crypto?"
|
||||
)
|
||||
# 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
|
||||
|
||||
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
|
||||
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:")
|
||||
for fact in answer["based_on"]["world"]:
|
||||
for fact in response.get("facts_used", []):
|
||||
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:
|
||||
- **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
|
||||
- **Debugging** — understand retrieval quality
|
||||
|
||||
@@ -45,8 +45,8 @@ from hindsight_client import Hindsight
|
||||
|
||||
client = Hindsight(base_url="http://localhost:8888")
|
||||
|
||||
client.store(
|
||||
agent_id="my-agent",
|
||||
client.retain(
|
||||
bank_id="my-bank",
|
||||
content="Alice works at Google as a software engineer"
|
||||
)
|
||||
```
|
||||
@@ -55,21 +55,18 @@ client.store(
|
||||
<TabItem value="node" label="Node.js">
|
||||
|
||||
```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({
|
||||
agent_id: 'my-agent',
|
||||
content: 'Alice works at Google as a software engineer'
|
||||
});
|
||||
await client.retain('my-bank', 'Alice works at Google as a software engineer');
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="cli" label="CLI">
|
||||
|
||||
```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>
|
||||
@@ -83,19 +80,29 @@ Add context and event dates for better retrieval:
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
client.store(
|
||||
agent_id="my-agent",
|
||||
client.retain(
|
||||
bank_id="my-bank",
|
||||
content="Alice got promoted to senior engineer",
|
||||
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 value="cli" label="CLI">
|
||||
|
||||
```bash
|
||||
hindsight memory put my-agent "Alice got promoted" \
|
||||
hindsight memory put my-bank "Alice got promoted" \
|
||||
--context "career update" \
|
||||
--event-date "2024-03-15"
|
||||
```
|
||||
@@ -103,7 +110,7 @@ hindsight memory put my-agent "Alice got promoted" \
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
The `event_date` enables temporal queries like "What happened last spring?"
|
||||
The `timestamp` enables temporal queries like "What happened last spring?"
|
||||
|
||||
## Batch Ingestion
|
||||
|
||||
@@ -113,8 +120,8 @@ Store multiple memories in a single request:
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
client.store_batch(
|
||||
agent_id="my-agent",
|
||||
client.retain_batch(
|
||||
bank_id="my-bank",
|
||||
items=[
|
||||
{"content": "Alice works at Google", "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">
|
||||
|
||||
```typescript
|
||||
await MemoryStorageService.batchApiMemoriesBatchPost({
|
||||
agent_id: 'my-agent',
|
||||
items: [
|
||||
{ content: 'Alice works at Google', context: 'career' },
|
||||
{ content: 'Bob is a data scientist at Meta', context: 'career' }
|
||||
],
|
||||
document_id: 'conversation_001'
|
||||
});
|
||||
await client.retainBatch('my-bank', [
|
||||
{ content: 'Alice works at Google', context: 'career' },
|
||||
{ content: 'Bob is a data scientist at Meta', context: 'career' },
|
||||
{ content: 'Alice and Bob are friends', context: 'relationship' }
|
||||
], { documentId: 'conversation_001' });
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
@@ -150,13 +154,13 @@ The `document_id` groups related memories for later management.
|
||||
|
||||
```bash
|
||||
# Single file
|
||||
hindsight memory put-files my-agent document.txt
|
||||
hindsight memory put-files my-bank document.txt
|
||||
|
||||
# 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
|
||||
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>
|
||||
@@ -191,15 +195,28 @@ For large batches, use async ingestion:
|
||||
|
||||
```python
|
||||
# Start async ingestion
|
||||
operation = client.store_batch_async(
|
||||
agent_id="my-agent",
|
||||
result = client.retain_batch(
|
||||
bank_id="my-bank",
|
||||
items=[...large batch...],
|
||||
document_id="large-doc"
|
||||
document_id="large-doc",
|
||||
async_=True
|
||||
)
|
||||
|
||||
# Check status
|
||||
status = client.get_operation(operation["operation_id"])
|
||||
print(status["status"]) # "pending", "processing", "completed", "failed"
|
||||
# Result contains operation_id for tracking
|
||||
print(result["operation_id"])
|
||||
```
|
||||
|
||||
</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>
|
||||
@@ -211,5 +228,5 @@ print(status["status"]) # "pending", "processing", "completed", "failed"
|
||||
|----|-------|
|
||||
| Include context for better retrieval | Store raw unstructured dumps |
|
||||
| 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 |
|
||||
|
||||
@@ -63,7 +63,7 @@ class Server:
|
||||
host: str = "127.0.0.1",
|
||||
port: Optional[int] = None,
|
||||
mcp_enabled: bool = False,
|
||||
log_level: str = "warning",
|
||||
log_level: str = "info",
|
||||
):
|
||||
"""
|
||||
Initialize the Hindsight server.
|
||||
|
||||
Reference in New Issue
Block a user