Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68e44aceef | ||
|
|
1b270d9787 | ||
|
|
698b8f2d32 | ||
|
|
78dff638d6 | ||
|
|
9ae2bd3085 | ||
|
|
9c8a33e93c | ||
|
|
e03561054e | ||
|
|
e4f9ef0550 | ||
|
|
3d0ddd62a6 | ||
|
|
b712652491 | ||
|
|
ff26ca4331 | ||
|
|
3b3d433113 | ||
|
|
a255f6d16c | ||
|
|
04f2217887 | ||
|
|
4661e95078 | ||
|
|
0fca907fd0 | ||
|
|
67fe57bd92 | ||
|
|
85d4afb11e | ||
|
|
dca8ff6843 |
@@ -0,0 +1,11 @@
|
||||
name: 'Setup pg0'
|
||||
description: 'Install pg0 embedded PostgreSQL'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Install pg0
|
||||
shell: bash
|
||||
run: |
|
||||
curl -fsSL https://raw.githubusercontent.com/vectorize-io/pg0/main/install.sh | bash
|
||||
echo "$HOME/.pg0/bin" >> $GITHUB_PATH
|
||||
+253
-49
@@ -4,55 +4,11 @@ on:
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-python-packages:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- name: hindsight-all
|
||||
path: hindsight
|
||||
- name: hindsight-api
|
||||
path: hindsight-api
|
||||
- name: hindsight-client
|
||||
path: hindsight-clients/python
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version-file: ".python-version"
|
||||
|
||||
- name: Build ${{ matrix.name }}
|
||||
working-directory: ./${{ matrix.path }}
|
||||
run: uv build
|
||||
|
||||
build-typescript-client:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ./hindsight-clients/typescript
|
||||
run: npm ci
|
||||
|
||||
- name: Build TypeScript client
|
||||
working-directory: ./hindsight-clients/typescript
|
||||
run: npm run build
|
||||
|
||||
build-docs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -147,11 +103,11 @@ jobs:
|
||||
|
||||
test-api:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-python-packages]
|
||||
env:
|
||||
HINDSIGHT_API_LLM_PROVIDER: groq
|
||||
HINDSIGHT_API_LLM_API_KEY: ${{ secrets.GROQ_API_KEY }}
|
||||
HINDSIGHT_API_LLM_MODEL: openai/gpt-oss-20b
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -160,12 +116,20 @@ jobs:
|
||||
uses: astral-sh/setup-uv@v5
|
||||
with:
|
||||
enable-cache: true
|
||||
prune-cache: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version-file: ".python-version"
|
||||
|
||||
- name: Install pg0
|
||||
uses: ./.github/actions/setup-pg0
|
||||
|
||||
- name: Build API
|
||||
working-directory: ./hindsight-api
|
||||
run: uv build
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ./hindsight-api
|
||||
run: uv sync --extra test
|
||||
@@ -173,3 +137,243 @@ jobs:
|
||||
- name: Run tests
|
||||
working-directory: ./hindsight-api
|
||||
run: uv run pytest tests -v
|
||||
|
||||
test-python-client:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
HINDSIGHT_API_LLM_PROVIDER: groq
|
||||
HINDSIGHT_API_LLM_API_KEY: ${{ secrets.GROQ_API_KEY }}
|
||||
HINDSIGHT_API_LLM_MODEL: openai/gpt-oss-20b
|
||||
HINDSIGHT_API_URL: http://localhost:8888
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
with:
|
||||
enable-cache: true
|
||||
prune-cache: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version-file: ".python-version"
|
||||
|
||||
- name: Install pg0
|
||||
uses: ./.github/actions/setup-pg0
|
||||
|
||||
- name: Build API
|
||||
working-directory: ./hindsight-api
|
||||
run: uv build
|
||||
|
||||
- name: Build Python client
|
||||
working-directory: ./hindsight-clients/python
|
||||
run: uv build
|
||||
|
||||
- name: Install client test dependencies
|
||||
working-directory: ./hindsight-clients/python
|
||||
run: uv sync --extra test
|
||||
|
||||
- name: Install API dependencies
|
||||
working-directory: ./hindsight-api
|
||||
run: uv sync
|
||||
|
||||
- name: Create .env file
|
||||
run: |
|
||||
cat > .env << EOF
|
||||
HINDSIGHT_API_LLM_PROVIDER=${{ env.HINDSIGHT_API_LLM_PROVIDER }}
|
||||
HINDSIGHT_API_LLM_API_KEY=${{ env.HINDSIGHT_API_LLM_API_KEY }}
|
||||
HINDSIGHT_API_LLM_MODEL=${{ env.HINDSIGHT_API_LLM_MODEL }}
|
||||
EOF
|
||||
|
||||
- name: Start API server
|
||||
run: |
|
||||
./scripts/dev/start-api.sh > /tmp/api-server.log 2>&1 &
|
||||
echo "Waiting for API server to be ready..."
|
||||
for i in {1..60}; do
|
||||
if curl -sf http://localhost:8888/health > /dev/null 2>&1; then
|
||||
echo "API server is ready after ${i}s"
|
||||
break
|
||||
fi
|
||||
if [ $i -eq 60 ]; then
|
||||
echo "API server failed to start after 60s"
|
||||
cat /tmp/api-server.log
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
- name: Run Python client tests
|
||||
working-directory: ./hindsight-clients/python
|
||||
run: uv run pytest tests -v
|
||||
|
||||
- name: Show API server logs
|
||||
if: always()
|
||||
run: |
|
||||
echo "=== API Server Logs ==="
|
||||
cat /tmp/api-server.log || echo "No API server log found"
|
||||
|
||||
test-typescript-client:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
HINDSIGHT_API_LLM_PROVIDER: groq
|
||||
HINDSIGHT_API_LLM_API_KEY: ${{ secrets.GROQ_API_KEY }}
|
||||
HINDSIGHT_API_LLM_MODEL: openai/gpt-oss-20b
|
||||
HINDSIGHT_API_URL: http://localhost:8888
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
with:
|
||||
enable-cache: true
|
||||
prune-cache: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version-file: ".python-version"
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install pg0
|
||||
uses: ./.github/actions/setup-pg0
|
||||
|
||||
- name: Build API
|
||||
working-directory: ./hindsight-api
|
||||
run: uv build
|
||||
|
||||
- name: Install API dependencies
|
||||
working-directory: ./hindsight-api
|
||||
run: uv sync
|
||||
|
||||
- name: Install TypeScript client dependencies
|
||||
working-directory: ./hindsight-clients/typescript
|
||||
run: npm ci
|
||||
|
||||
- name: Build TypeScript client
|
||||
working-directory: ./hindsight-clients/typescript
|
||||
run: npm run build
|
||||
|
||||
- name: Create .env file
|
||||
run: |
|
||||
cat > .env << EOF
|
||||
HINDSIGHT_API_LLM_PROVIDER=${{ env.HINDSIGHT_API_LLM_PROVIDER }}
|
||||
HINDSIGHT_API_LLM_API_KEY=${{ env.HINDSIGHT_API_LLM_API_KEY }}
|
||||
HINDSIGHT_API_LLM_MODEL=${{ env.HINDSIGHT_API_LLM_MODEL }}
|
||||
EOF
|
||||
|
||||
- name: Start API server
|
||||
run: |
|
||||
./scripts/dev/start-api.sh > /tmp/api-server.log 2>&1 &
|
||||
echo "Waiting for API server to be ready..."
|
||||
for i in {1..60}; do
|
||||
if curl -sf http://localhost:8888/health > /dev/null 2>&1; then
|
||||
echo "API server is ready after ${i}s"
|
||||
break
|
||||
fi
|
||||
if [ $i -eq 60 ]; then
|
||||
echo "API server failed to start after 60s"
|
||||
cat /tmp/api-server.log
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
- name: Run TypeScript client tests
|
||||
working-directory: ./hindsight-clients/typescript
|
||||
run: npm test
|
||||
|
||||
- name: Show API server logs
|
||||
if: always()
|
||||
run: |
|
||||
echo "=== API Server Logs ==="
|
||||
cat /tmp/api-server.log || echo "No API server log found"
|
||||
|
||||
test-rust-client:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
HINDSIGHT_API_LLM_PROVIDER: groq
|
||||
HINDSIGHT_API_LLM_API_KEY: ${{ secrets.GROQ_API_KEY }}
|
||||
HINDSIGHT_API_LLM_MODEL: openai/gpt-oss-20b
|
||||
HINDSIGHT_API_URL: http://localhost:8888
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
with:
|
||||
enable-cache: true
|
||||
prune-cache: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version-file: ".python-version"
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Cache cargo
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
hindsight-clients/rust/target
|
||||
key: ${{ runner.os }}-cargo-client-${{ hashFiles('hindsight-clients/rust/Cargo.lock') }}
|
||||
|
||||
- name: Install pg0
|
||||
uses: ./.github/actions/setup-pg0
|
||||
|
||||
- name: Build API
|
||||
working-directory: ./hindsight-api
|
||||
run: uv build
|
||||
|
||||
- name: Install API dependencies
|
||||
working-directory: ./hindsight-api
|
||||
run: uv sync
|
||||
|
||||
- name: Create .env file
|
||||
run: |
|
||||
cat > .env << EOF
|
||||
HINDSIGHT_API_LLM_PROVIDER=${{ env.HINDSIGHT_API_LLM_PROVIDER }}
|
||||
HINDSIGHT_API_LLM_API_KEY=${{ env.HINDSIGHT_API_LLM_API_KEY }}
|
||||
HINDSIGHT_API_LLM_MODEL=${{ env.HINDSIGHT_API_LLM_MODEL }}
|
||||
EOF
|
||||
|
||||
- name: Start API server
|
||||
run: |
|
||||
./scripts/dev/start-api.sh > /tmp/api-server.log 2>&1 &
|
||||
echo "Waiting for API server to be ready..."
|
||||
for i in {1..60}; do
|
||||
if curl -sf http://localhost:8888/health > /dev/null 2>&1; then
|
||||
echo "API server is ready after ${i}s"
|
||||
break
|
||||
fi
|
||||
if [ $i -eq 60 ]; then
|
||||
echo "API server failed to start after 60s"
|
||||
cat /tmp/api-server.log
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
- name: Run Rust client tests
|
||||
working-directory: ./hindsight-clients/rust
|
||||
run: cargo test --lib
|
||||
|
||||
- name: Show API server logs
|
||||
if: always()
|
||||
run: |
|
||||
echo "=== API Server Logs ==="
|
||||
cat /tmp/api-server.log || echo "No API server log found"
|
||||
|
||||
@@ -17,18 +17,17 @@ def create_app(
|
||||
http_api_enabled: bool = True,
|
||||
mcp_api_enabled: bool = False,
|
||||
mcp_mount_path: str = "/mcp",
|
||||
run_migrations: bool = True,
|
||||
initialize_memory: bool = True
|
||||
) -> FastAPI:
|
||||
"""
|
||||
Create and configure the unified Hindsight API application.
|
||||
|
||||
Args:
|
||||
memory: MemoryEngine instance (already initialized with required parameters)
|
||||
memory: MemoryEngine instance (already initialized with required parameters).
|
||||
Migrations are controlled by the MemoryEngine's run_migrations parameter.
|
||||
http_api_enabled: Whether to enable HTTP REST API endpoints (default: True)
|
||||
mcp_api_enabled: Whether to enable MCP server (default: False)
|
||||
mcp_mount_path: Path to mount MCP server (default: /mcp)
|
||||
run_migrations: Whether to run database migrations on startup (default: True)
|
||||
initialize_memory: Whether to initialize memory system on startup (default: True)
|
||||
|
||||
Returns:
|
||||
@@ -50,7 +49,6 @@ def create_app(
|
||||
from .http import create_app as create_http_app
|
||||
app = create_http_app(
|
||||
memory=memory,
|
||||
run_migrations=run_migrations,
|
||||
initialize_memory=initialize_memory
|
||||
)
|
||||
logger.info("HTTP REST API enabled")
|
||||
|
||||
@@ -43,21 +43,6 @@ from hindsight_api.metrics import get_metrics_collector, initialize_metrics, cre
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MetadataFilter(BaseModel):
|
||||
"""Filter for metadata fields. Matches records where (key=value) OR (key not set) when match_unset=True."""
|
||||
model_config = ConfigDict(json_schema_extra={
|
||||
"example": {
|
||||
"key": "source",
|
||||
"value": "slack",
|
||||
"match_unset": True
|
||||
}
|
||||
})
|
||||
|
||||
key: str = Field(description="Metadata key to filter on")
|
||||
value: Optional[str] = Field(default=None, description="Value to match. If None with match_unset=True, matches any record where key is not set.")
|
||||
match_unset: bool = Field(default=True, description="If True, also match records where this metadata key is not set")
|
||||
|
||||
|
||||
class EntityIncludeOptions(BaseModel):
|
||||
"""Options for including entity observations in recall results."""
|
||||
max_tokens: int = Field(default=500, description="Maximum tokens for entity observations")
|
||||
@@ -90,7 +75,6 @@ class RecallRequest(BaseModel):
|
||||
"max_tokens": 4096,
|
||||
"trace": True,
|
||||
"query_timestamp": "2023-05-30T23:40:00",
|
||||
"filters": [{"key": "source", "value": "slack", "match_unset": True}],
|
||||
"include": {
|
||||
"entities": {
|
||||
"max_tokens": 500
|
||||
@@ -105,7 +89,6 @@ class RecallRequest(BaseModel):
|
||||
max_tokens: int = 4096
|
||||
trace: bool = False
|
||||
query_timestamp: Optional[str] = Field(default=None, description="ISO format date string (e.g., '2023-05-30T23:40:00')")
|
||||
filters: Optional[List[MetadataFilter]] = Field(default=None, description="Filter by metadata. Multiple filters are ANDed together.")
|
||||
include: IncludeOptions = Field(default_factory=IncludeOptions, description="Options for including additional data (entities are included by default)")
|
||||
|
||||
|
||||
@@ -363,7 +346,6 @@ class ReflectRequest(BaseModel):
|
||||
"query": "What do you think about artificial intelligence?",
|
||||
"budget": "low",
|
||||
"context": "This is for a research paper on AI ethics",
|
||||
"filters": [{"key": "source", "value": "slack", "match_unset": True}],
|
||||
"include": {
|
||||
"facts": {}
|
||||
}
|
||||
@@ -373,7 +355,6 @@ class ReflectRequest(BaseModel):
|
||||
query: str
|
||||
budget: Budget = Budget.LOW
|
||||
context: Optional[str] = None
|
||||
filters: Optional[List[MetadataFilter]] = Field(default=None, description="Filter by metadata. Multiple filters are ANDed together.")
|
||||
include: ReflectIncludeOptions = Field(default_factory=ReflectIncludeOptions, description="Options for including additional data (disabled by default)")
|
||||
|
||||
|
||||
@@ -698,13 +679,13 @@ class DeleteResponse(BaseModel):
|
||||
success: bool
|
||||
|
||||
|
||||
def create_app(memory: MemoryEngine, run_migrations: bool = True, initialize_memory: bool = True) -> FastAPI:
|
||||
def create_app(memory: MemoryEngine, initialize_memory: bool = True) -> FastAPI:
|
||||
"""
|
||||
Create and configure the FastAPI application.
|
||||
|
||||
Args:
|
||||
memory: MemoryEngine instance (already initialized with required parameters)
|
||||
run_migrations: Whether to run database migrations on startup (default: True)
|
||||
memory: MemoryEngine instance (already initialized with required parameters).
|
||||
Migrations are controlled by the MemoryEngine's run_migrations parameter.
|
||||
initialize_memory: Whether to initialize memory system on startup (default: True)
|
||||
|
||||
Returns:
|
||||
@@ -735,16 +716,11 @@ def create_app(memory: MemoryEngine, run_migrations: bool = True, initialize_mem
|
||||
app.state.prometheus_reader = None
|
||||
# Metrics collector is already initialized as no-op by default
|
||||
|
||||
# Startup: Initialize database and memory system
|
||||
# Startup: Initialize database and memory system (migrations run inside initialize if enabled)
|
||||
if initialize_memory:
|
||||
await memory.initialize()
|
||||
logging.info("Memory system initialized")
|
||||
|
||||
if run_migrations:
|
||||
from hindsight_api.migrations import run_migrations as do_migrations
|
||||
do_migrations(memory.db_url)
|
||||
logging.info("Database migrations applied")
|
||||
|
||||
|
||||
|
||||
yield
|
||||
|
||||
@@ -102,7 +102,6 @@ def main():
|
||||
http_api_enabled=True,
|
||||
mcp_api_enabled=True,
|
||||
mcp_mount_path="/mcp",
|
||||
run_migrations=True,
|
||||
initialize_memory=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -78,7 +78,12 @@ class SentenceTransformersCrossEncoder(CrossEncoderModel):
|
||||
)
|
||||
|
||||
logger.info(f"Loading cross-encoder model: {self.model_name}...")
|
||||
self._model = CrossEncoder(self.model_name)
|
||||
# Disable lazy loading (meta tensors) which causes issues with newer transformers/accelerate
|
||||
# Setting low_cpu_mem_usage=False and device_map=None ensures tensors are fully materialized
|
||||
self._model = CrossEncoder(
|
||||
self.model_name,
|
||||
model_kwargs={"low_cpu_mem_usage": False, "device_map": None},
|
||||
)
|
||||
logger.info("Cross-encoder model loaded")
|
||||
|
||||
def predict(self, pairs: List[Tuple[str, str]]) -> List[float]:
|
||||
|
||||
@@ -84,7 +84,12 @@ class SentenceTransformersEmbeddings(Embeddings):
|
||||
)
|
||||
|
||||
logger.info(f"Loading embedding model: {self.model_name}...")
|
||||
self._model = SentenceTransformer(self.model_name)
|
||||
# Disable lazy loading (meta tensors) which causes issues with newer transformers/accelerate
|
||||
# Setting low_cpu_mem_usage=False and device_map=None ensures tensors are fully materialized
|
||||
self._model = SentenceTransformer(
|
||||
self.model_name,
|
||||
model_kwargs={"low_cpu_mem_usage": False, "device_map": None},
|
||||
)
|
||||
|
||||
# Validate dimension matches database schema
|
||||
model_dim = self._model.get_sentence_embedding_dimension()
|
||||
|
||||
@@ -110,12 +110,14 @@ class MemoryEngine:
|
||||
pool_min_size: int = 5,
|
||||
pool_max_size: int = 100,
|
||||
task_backend: Optional[TaskBackend] = None,
|
||||
run_migrations: bool = True,
|
||||
):
|
||||
"""
|
||||
Initialize the temporal + semantic memory system.
|
||||
|
||||
Args:
|
||||
db_url: PostgreSQL connection URL (postgresql://user:pass@host:port/dbname). Required.
|
||||
Also supports pg0 URLs: "pg0" or "pg0://instance-name" or "pg0://instance-name:port"
|
||||
memory_llm_provider: LLM provider for memory operations: "openai", "groq", or "ollama". Required.
|
||||
memory_llm_api_key: API key for the LLM provider. Required.
|
||||
memory_llm_model: Model name to use for all memory operations (put/think/opinions). Required.
|
||||
@@ -129,16 +131,38 @@ class MemoryEngine:
|
||||
pool_max_size: Maximum number of connections in the pool (default: 100)
|
||||
Increase for parallel think/search operations (e.g., 200-300 for 100+ parallel thinks)
|
||||
task_backend: Custom task backend for async task execution. If not provided, uses AsyncIOQueueBackend
|
||||
run_migrations: Whether to run database migrations during initialize(). Default: True
|
||||
"""
|
||||
if not db_url:
|
||||
raise ValueError("Database url is required")
|
||||
# Track pg0 instance (if used)
|
||||
self._pg0: Optional[EmbeddedPostgres] = None
|
||||
self._pg0_instance_name: Optional[str] = None
|
||||
|
||||
# Initialize PostgreSQL connection URL
|
||||
# The actual URL will be set during initialize() after starting the server
|
||||
self._use_pg0 = db_url == "pg0"
|
||||
self.db_url = db_url if not self._use_pg0 else None
|
||||
# Supports: "pg0" (default instance), "pg0://instance-name" (named instance), or regular postgresql:// URL
|
||||
if db_url == "pg0":
|
||||
self._use_pg0 = True
|
||||
self._pg0_instance_name = "hindsight"
|
||||
self._pg0_port = None # Use default port
|
||||
self.db_url = None
|
||||
elif db_url.startswith("pg0://"):
|
||||
self._use_pg0 = True
|
||||
# Parse instance name and optional port: pg0://instance-name or pg0://instance-name:port
|
||||
url_part = db_url[6:] # Remove "pg0://"
|
||||
if ":" in url_part:
|
||||
self._pg0_instance_name, port_str = url_part.rsplit(":", 1)
|
||||
self._pg0_port = int(port_str)
|
||||
else:
|
||||
self._pg0_instance_name = url_part or "hindsight"
|
||||
self._pg0_port = None # Use default port
|
||||
self.db_url = None
|
||||
else:
|
||||
self._use_pg0 = False
|
||||
self._pg0_instance_name = None
|
||||
self._pg0_port = None
|
||||
self.db_url = db_url
|
||||
|
||||
|
||||
# Set default base URL if not provided
|
||||
@@ -155,6 +179,7 @@ class MemoryEngine:
|
||||
self._initialized = False
|
||||
self._pool_min_size = pool_min_size
|
||||
self._pool_max_size = pool_max_size
|
||||
self._run_migrations = run_migrations
|
||||
|
||||
# Initialize entity resolver (will be created in initialize())
|
||||
self.entity_resolver = None
|
||||
@@ -378,8 +403,16 @@ class MemoryEngine:
|
||||
async def start_pg0():
|
||||
"""Start pg0 if configured."""
|
||||
if self._use_pg0:
|
||||
self._pg0 = EmbeddedPostgres()
|
||||
self.db_url = await self._pg0.ensure_running()
|
||||
kwargs = {"name": self._pg0_instance_name}
|
||||
if self._pg0_port is not None:
|
||||
kwargs["port"] = self._pg0_port
|
||||
pg0 = EmbeddedPostgres(**kwargs)
|
||||
# Check if pg0 is already running before we start it
|
||||
was_already_running = await pg0.is_running()
|
||||
self.db_url = await pg0.ensure_running()
|
||||
# Only track pg0 (to stop later) if WE started it
|
||||
if not was_already_running:
|
||||
self._pg0 = pg0
|
||||
|
||||
def load_embeddings():
|
||||
"""Load embedding model (CPU-bound)."""
|
||||
@@ -408,6 +441,12 @@ class MemoryEngine:
|
||||
pg0_task, embeddings_future, cross_encoder_future, query_analyzer_future
|
||||
)
|
||||
|
||||
# Run database migrations if enabled
|
||||
if self._run_migrations:
|
||||
from ..migrations import run_migrations
|
||||
logger.info("Running database migrations...")
|
||||
run_migrations(self.db_url)
|
||||
|
||||
logger.info(f"Connecting to PostgreSQL at {self.db_url}")
|
||||
|
||||
# Create connection pool
|
||||
@@ -1402,7 +1441,6 @@ class MemoryEngine:
|
||||
mentioned_at=result_dict.get("mentioned_at"),
|
||||
document_id=result_dict.get("document_id"),
|
||||
chunk_id=result_dict.get("chunk_id"),
|
||||
activation=result_dict.get("weight") # Use final weight as activation
|
||||
))
|
||||
|
||||
# Fetch entity observations if requested
|
||||
@@ -2592,7 +2630,13 @@ Guidelines:
|
||||
if self._llm_config is None:
|
||||
raise ValueError("Memory LLM API key not set. Set HINDSIGHT_API_LLM_API_KEY environment variable.")
|
||||
|
||||
reflect_start = time.time()
|
||||
reflect_id = f"{bank_id[:8]}-{int(time.time() * 1000) % 100000}"
|
||||
log_buffer = []
|
||||
log_buffer.append(f"[REFLECT {reflect_id}] Query: '{query[:50]}...'")
|
||||
|
||||
# Steps 1-3: Run multi-fact-type search (12-way retrieval: 4 methods × 3 fact types)
|
||||
recall_start = time.time()
|
||||
search_result = await self.recall_async(
|
||||
bank_id=bank_id,
|
||||
query=query,
|
||||
@@ -2602,24 +2646,22 @@ Guidelines:
|
||||
fact_type=['experience', 'world', 'opinion'],
|
||||
include_entities=True
|
||||
)
|
||||
recall_time = time.time() - recall_start
|
||||
|
||||
all_results = search_result.results
|
||||
logger.info(f"[THINK] Search returned {len(all_results)} results")
|
||||
|
||||
# Split results by fact type for structured response
|
||||
agent_results = [r for r in all_results if r.fact_type == 'experience']
|
||||
world_results = [r for r in all_results if r.fact_type == 'world']
|
||||
opinion_results = [r for r in all_results if r.fact_type == 'opinion']
|
||||
|
||||
logger.info(f"[THINK] Split results - agent: {len(agent_results)}, world: {len(world_results)}, opinion: {len(opinion_results)}")
|
||||
log_buffer.append(f"[REFLECT {reflect_id}] Recall: {len(all_results)} facts (experience={len(agent_results)}, world={len(world_results)}, opinion={len(opinion_results)}) in {recall_time:.3f}s")
|
||||
|
||||
# Format facts for LLM
|
||||
agent_facts_text = think_utils.format_facts_for_prompt(agent_results)
|
||||
world_facts_text = think_utils.format_facts_for_prompt(world_results)
|
||||
opinion_facts_text = think_utils.format_facts_for_prompt(opinion_results)
|
||||
|
||||
logger.info(f"[THINK] Formatted facts - agent: {len(agent_facts_text)} chars, world: {len(world_facts_text)} chars, opinion: {len(opinion_facts_text)} chars")
|
||||
|
||||
# Get bank profile (name, disposition + background)
|
||||
profile = await self.get_bank_profile(bank_id)
|
||||
name = profile["name"]
|
||||
@@ -2638,10 +2680,11 @@ Guidelines:
|
||||
context=context,
|
||||
)
|
||||
|
||||
logger.info(f"[THINK] Full prompt length: {len(prompt)} chars")
|
||||
log_buffer.append(f"[REFLECT {reflect_id}] Prompt: {len(prompt)} chars")
|
||||
|
||||
system_message = think_utils.get_system_message(disposition)
|
||||
|
||||
llm_start = time.time()
|
||||
answer_text = await self._llm_config.call(
|
||||
messages=[
|
||||
{"role": "system", "content": system_message},
|
||||
@@ -2651,6 +2694,7 @@ Guidelines:
|
||||
temperature=0.9,
|
||||
max_tokens=1000
|
||||
)
|
||||
llm_time = time.time() - llm_start
|
||||
|
||||
answer_text = answer_text.strip()
|
||||
|
||||
@@ -2662,6 +2706,10 @@ Guidelines:
|
||||
'query': query
|
||||
})
|
||||
|
||||
total_time = time.time() - reflect_start
|
||||
log_buffer.append(f"[REFLECT {reflect_id}] Complete: {len(answer_text)} chars response, LLM {llm_time:.3f}s, total {total_time:.3f}s")
|
||||
logger.info("\n" + "\n".join(log_buffer))
|
||||
|
||||
# Return response with facts split by type
|
||||
return ReflectResult(
|
||||
text=answer_text,
|
||||
@@ -2710,7 +2758,7 @@ Guidelines:
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"[THINK] Failed to extract/store opinions: {str(e)}")
|
||||
logger.warning(f"[REFLECT] Failed to extract/store opinions: {str(e)}")
|
||||
|
||||
async def get_entity_observations(
|
||||
self,
|
||||
|
||||
@@ -72,9 +72,6 @@ class MemoryFact(BaseModel):
|
||||
metadata: Optional[Dict[str, str]] = Field(None, description="User-defined metadata")
|
||||
chunk_id: Optional[str] = Field(None, description="ID of the chunk this fact was extracted from (format: bank_id_document_id_chunk_index)")
|
||||
|
||||
# Internal metrics (used by system but may not be exposed in API)
|
||||
activation: Optional[float] = Field(None, description="Internal activation score")
|
||||
|
||||
|
||||
class ChunkInfo(BaseModel):
|
||||
"""Information about a chunk."""
|
||||
|
||||
@@ -382,13 +382,42 @@ WRONG output:
|
||||
- where: (missing) ← WRONG - include the location!
|
||||
|
||||
══════════════════════════════════════════════════════════════════════════
|
||||
TEMPORAL HANDLING
|
||||
FACT_KIND CLASSIFICATION (CRITICAL FOR TEMPORAL HANDLING)
|
||||
══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
For EVENTS (fact_kind="event"):
|
||||
- Convert relative dates → absolute WITH DAY OF WEEK: "yesterday" on Saturday March 15 → "Friday, March 14, 2024"
|
||||
⚠️ MUST set fact_kind correctly - this determines whether occurred_start/end are set!
|
||||
|
||||
fact_kind="event" - USE FOR:
|
||||
- Actions that happened at a specific time: "went to", "attended", "visited", "bought", "made"
|
||||
- Past events: "yesterday I...", "last week...", "in March 2020..."
|
||||
- Future plans with dates: "will go to", "scheduled for"
|
||||
- Examples: "I went to a pottery workshop" → event
|
||||
"Alice visited Paris in February" → event
|
||||
"I bought a new car yesterday" → event
|
||||
"The user graduated from MIT in March 2020" → event
|
||||
|
||||
fact_kind="conversation" - USE FOR:
|
||||
- Ongoing states: "works as", "lives in", "is married to"
|
||||
- Preferences: "loves", "prefers", "enjoys"
|
||||
- Traits/abilities: "speaks fluent French", "knows Python"
|
||||
- Examples: "I love Italian food" → conversation
|
||||
"Alice works at Google" → conversation
|
||||
"I prefer outdoor dining" → conversation
|
||||
|
||||
══════════════════════════════════════════════════════════════════════════
|
||||
TEMPORAL HANDLING (CRITICAL - USE EVENT DATE AS REFERENCE)
|
||||
══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
⚠️ IMPORTANT: Use the "Event Date" provided in the input as your reference point!
|
||||
All relative dates ("yesterday", "last week", "recently") must be resolved relative to the Event Date, NOT today's date.
|
||||
|
||||
For EVENTS (fact_kind="event") - MUST SET BOTH occurred_start AND occurred_end:
|
||||
- Convert relative dates → absolute using Event Date as reference
|
||||
- If Event Date is "Saturday, March 15, 2020", then "yesterday" = Friday, March 14, 2020
|
||||
- Dates mentioned in text (e.g., "in March 2020") should use THAT year, not current year
|
||||
- Always include the day name (Monday, Tuesday, etc.) in the 'when' field
|
||||
- Set occurred_start/occurred_end to WHEN IT HAPPENED (not when mentioned)
|
||||
- Set occurred_start AND occurred_end to WHEN IT HAPPENED (not when mentioned)
|
||||
- For single-day/point events: set occurred_end = occurred_start (same timestamp)
|
||||
|
||||
For CONVERSATIONS (fact_kind="conversation"):
|
||||
- General info, preferences, ongoing states → NO occurred dates
|
||||
@@ -440,7 +469,7 @@ Extract entities that help link related facts together. Include:
|
||||
EXAMPLES
|
||||
══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
Example 1 - World Facts (Context: June 10, 2024):
|
||||
Example 1 - World Facts (Event Date: Tuesday, June 10, 2024):
|
||||
Input: "I'm planning my wedding and want a small outdoor ceremony. I just got back from my college roommate Emily's wedding - she married Sarah at a rooftop garden, it was so romantic!"
|
||||
|
||||
Output facts:
|
||||
@@ -459,12 +488,13 @@ Output facts:
|
||||
- fact_type: "world", fact_kind: "conversation"
|
||||
- entities: ["user", "wedding"]
|
||||
|
||||
3. Emily's wedding (THE EVENT)
|
||||
3. Emily's wedding (THE EVENT - note occurred_start AND occurred_end both set)
|
||||
- what: "Emily got married to Sarah at a rooftop garden ceremony in the city"
|
||||
- who: "Emily (user's college roommate), Sarah (Emily's partner)"
|
||||
- why: "User found it romantic and beautiful"
|
||||
- fact_type: "world", fact_kind: "event"
|
||||
- occurred_start: "2024-06-09T00:00:00Z" (recently, user "just got back")
|
||||
- occurred_start: "2024-06-09T00:00:00Z" (recently, user "just got back" - relative to Event Date June 10, 2024)
|
||||
- occurred_end: "2024-06-09T23:59:59Z" (same day - point event)
|
||||
- entities: ["user", "Emily", "Sarah", "wedding", "rooftop garden"]
|
||||
|
||||
Example 2 - Assistant Facts (Context: March 5, 2024):
|
||||
@@ -479,16 +509,17 @@ Output fact:
|
||||
- fact_type: "assistant", fact_kind: "conversation"
|
||||
- entities: ["user", "API", "Redis"]
|
||||
|
||||
Example 3 - Kitchen Items with Concept Inference (Context: May 30, 2024):
|
||||
Example 3 - Kitchen Items with Concept Inference (Event Date: Thursday, May 30, 2024):
|
||||
Input: "I finally donated my old coffee maker to Goodwill. I upgraded to that new espresso machine last month and the old one was just taking up counter space."
|
||||
|
||||
Output fact:
|
||||
- what: "User donated their old coffee maker to Goodwill after upgrading to a new espresso machine"
|
||||
- when: "May 30, 2024"
|
||||
- when: "Thursday, May 30, 2024"
|
||||
- who: "user"
|
||||
- why: "The old coffee maker was taking up counter space after the upgrade"
|
||||
- fact_type: "world", fact_kind: "event"
|
||||
- occurred_start: "2024-05-30T00:00:00Z"
|
||||
- occurred_start: "2024-05-30T00:00:00Z" (uses Event Date year)
|
||||
- occurred_end: "2024-05-30T23:59:59Z" (same day - point event)
|
||||
- entities: ["user", "coffee maker", "Goodwill", "espresso machine", "kitchen"]
|
||||
|
||||
Note: "kitchen" is inferred as a concept because coffee makers and espresso machines are kitchen appliances.
|
||||
@@ -656,8 +687,11 @@ Text:
|
||||
occurred_end = get_value('occurred_end')
|
||||
if occurred_start:
|
||||
fact_data['occurred_start'] = occurred_start
|
||||
if occurred_end:
|
||||
fact_data['occurred_end'] = occurred_end
|
||||
# For point events: if occurred_end not set, default to occurred_start
|
||||
if occurred_end:
|
||||
fact_data['occurred_end'] = occurred_end
|
||||
else:
|
||||
fact_data['occurred_end'] = occurred_start
|
||||
|
||||
# Add entities if present (validate as Entity objects)
|
||||
# LLM sometimes returns strings instead of {"text": "..."} format
|
||||
|
||||
@@ -96,10 +96,6 @@ def format_facts_for_prompt(facts: List[MemoryFact]) -> str:
|
||||
elif isinstance(occurred_start, datetime):
|
||||
fact_obj["occurred_start"] = occurred_start.strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
# Add activation if available
|
||||
if fact.activation is not None:
|
||||
fact_obj["score"] = fact.activation
|
||||
|
||||
formatted.append(fact_obj)
|
||||
|
||||
return json.dumps(formatted, indent=2)
|
||||
|
||||
@@ -3,8 +3,8 @@ Database migration management using Alembic.
|
||||
|
||||
This module provides programmatic access to run database migrations
|
||||
on application startup. It is designed to be safe for concurrent
|
||||
execution - Alembic uses PostgreSQL transactions to prevent
|
||||
conflicts when multiple instances start simultaneously.
|
||||
execution using PostgreSQL advisory locks to coordinate between
|
||||
distributed workers.
|
||||
|
||||
Important: All migrations must be backward-compatible to allow
|
||||
safe rolling deployments.
|
||||
@@ -19,19 +19,51 @@ from typing import Optional
|
||||
|
||||
from alembic import command
|
||||
from alembic.config import Config
|
||||
from sqlalchemy import create_engine, text
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Advisory lock ID for migrations (arbitrary unique number)
|
||||
MIGRATION_LOCK_ID = 123456789
|
||||
|
||||
|
||||
def _run_migrations_internal(database_url: str, script_location: str) -> None:
|
||||
"""
|
||||
Internal function to run migrations without locking.
|
||||
"""
|
||||
logger.info(f"Running database migrations to head...")
|
||||
logger.info(f"Database URL: {database_url}")
|
||||
logger.info(f"Script location: {script_location}")
|
||||
|
||||
# Create Alembic configuration programmatically (no alembic.ini needed)
|
||||
alembic_cfg = Config()
|
||||
|
||||
# Set the script location (where alembic versions are stored)
|
||||
alembic_cfg.set_main_option("script_location", script_location)
|
||||
|
||||
# Set the database URL
|
||||
alembic_cfg.set_main_option("sqlalchemy.url", database_url)
|
||||
|
||||
# Configure logging (optional, but helps with debugging)
|
||||
# Uses Python's logging system instead of alembic.ini
|
||||
alembic_cfg.set_main_option("prepend_sys_path", ".")
|
||||
|
||||
# Set path_separator to avoid deprecation warning
|
||||
alembic_cfg.set_main_option("path_separator", "os")
|
||||
|
||||
# Run migrations to head (latest version)
|
||||
command.upgrade(alembic_cfg, "head")
|
||||
|
||||
logger.info("Database migrations completed successfully")
|
||||
|
||||
|
||||
def run_migrations(database_url: str, script_location: Optional[str] = None) -> None:
|
||||
"""
|
||||
Run database migrations to the latest version using programmatic Alembic configuration.
|
||||
|
||||
This function is safe to call on every application startup:
|
||||
- Alembic checks the current schema version in the database
|
||||
- Only missing migrations are applied
|
||||
- PostgreSQL transactions prevent concurrent migration conflicts
|
||||
This function is safe to call from multiple distributed workers simultaneously:
|
||||
- Uses PostgreSQL advisory lock to ensure only one worker runs migrations at a time
|
||||
- Other workers wait for the lock, then verify migrations are complete
|
||||
- If schema is already up-to-date, this is a fast no-op
|
||||
|
||||
Args:
|
||||
@@ -69,32 +101,22 @@ def run_migrations(database_url: str, script_location: Optional[str] = None) ->
|
||||
"Database migrations cannot be run."
|
||||
)
|
||||
|
||||
logger.info(f"Running database migrations to head...")
|
||||
logger.info(f"Database URL: {database_url}")
|
||||
logger.info(f"Script location: {script_location}")
|
||||
# Use PostgreSQL advisory lock to coordinate between distributed workers
|
||||
engine = create_engine(database_url)
|
||||
with engine.connect() as conn:
|
||||
# pg_advisory_lock blocks until the lock is acquired
|
||||
# The lock is automatically released when the connection closes
|
||||
logger.debug(f"Acquiring migration advisory lock (id={MIGRATION_LOCK_ID})...")
|
||||
conn.execute(text(f"SELECT pg_advisory_lock({MIGRATION_LOCK_ID})"))
|
||||
logger.debug("Migration advisory lock acquired")
|
||||
|
||||
# Create Alembic configuration programmatically (no alembic.ini needed)
|
||||
alembic_cfg = Config()
|
||||
|
||||
# Set the script location (where alembic versions are stored)
|
||||
alembic_cfg.set_main_option("script_location", script_location)
|
||||
|
||||
# Set the database URL
|
||||
alembic_cfg.set_main_option("sqlalchemy.url", database_url)
|
||||
|
||||
# Configure logging (optional, but helps with debugging)
|
||||
# Uses Python's logging system instead of alembic.ini
|
||||
alembic_cfg.set_main_option("prepend_sys_path", ".")
|
||||
|
||||
# Set path_separator to avoid deprecation warning
|
||||
alembic_cfg.set_main_option("path_separator", "os")
|
||||
|
||||
# Run migrations to head (latest version)
|
||||
# Note: Alembic may call sys.exit() on errors instead of raising exceptions
|
||||
# We rely on the outer try/except and logging to catch issues
|
||||
command.upgrade(alembic_cfg, "head")
|
||||
|
||||
logger.info("Database migrations completed successfully")
|
||||
try:
|
||||
# Run migrations while holding the lock
|
||||
_run_migrations_internal(database_url, script_location)
|
||||
finally:
|
||||
# Explicitly release the lock (also released on connection close)
|
||||
conn.execute(text(f"SELECT pg_advisory_unlock({MIGRATION_LOCK_ID})"))
|
||||
logger.debug("Migration advisory lock released")
|
||||
|
||||
except FileNotFoundError:
|
||||
logger.error(f"Alembic script location not found at {script_location}")
|
||||
|
||||
@@ -153,46 +153,18 @@ class EmbeddedPostgres:
|
||||
"""
|
||||
Ensure pg0 is available.
|
||||
|
||||
First checks PATH, then default location, then downloads if needed.
|
||||
Checks PATH and default location. If not found, raises an error
|
||||
instructing the user to install pg0 manually.
|
||||
"""
|
||||
if self.is_installed():
|
||||
logger.debug(f"pg0 found at {self._binary_path}")
|
||||
return
|
||||
|
||||
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()}")
|
||||
|
||||
# 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)
|
||||
logger.info(f"Downloading from {download_url}")
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(follow_redirects=True, timeout=300.0) as client:
|
||||
response = await client.get(download_url)
|
||||
response.raise_for_status()
|
||||
|
||||
# Write binary to disk
|
||||
with open(install_path, "wb") as f:
|
||||
f.write(response.content)
|
||||
|
||||
# Make executable on Unix
|
||||
if platform.system() != "Windows":
|
||||
st = os.stat(install_path)
|
||||
os.chmod(install_path, st.st_mode | stat.S_IEXEC)
|
||||
|
||||
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
|
||||
raise RuntimeError(
|
||||
"pg0 is not installed. Please install it manually:\n"
|
||||
" curl -fsSL https://github.com/vectorize-io/pg0/releases/latest/download/pg0-linux-amd64 -o ~/.local/bin/pg0 && chmod +x ~/.local/bin/pg0\n"
|
||||
"Or visit: https://github.com/vectorize-io/pg0/releases"
|
||||
)
|
||||
|
||||
def _run_command(self, *args: str, capture_output: bool = True) -> subprocess.CompletedProcess:
|
||||
"""Run a pg0 command synchronously."""
|
||||
@@ -227,6 +199,13 @@ class EmbeddedPostgres:
|
||||
return match.group(1)
|
||||
return None
|
||||
|
||||
async def _get_version(self) -> str:
|
||||
"""Get the pg0 version."""
|
||||
returncode, stdout, stderr = await self._run_command_async("--version", timeout=10)
|
||||
if returncode == 0 and stdout:
|
||||
return stdout.strip()
|
||||
return "unknown"
|
||||
|
||||
async def start(self, max_retries: int = 3, retry_delay: float = 2.0) -> str:
|
||||
"""
|
||||
Start the PostgreSQL server with retry logic.
|
||||
@@ -244,7 +223,9 @@ class EmbeddedPostgres:
|
||||
if not self.is_installed():
|
||||
raise RuntimeError("pg0 is not installed. Call ensure_installed() first.")
|
||||
|
||||
logger.info(f"Starting embedded PostgreSQL (name: {self.name}, port: {self.port})...")
|
||||
# Log pg0 version
|
||||
version = await self._get_version()
|
||||
logger.info(f"Starting embedded PostgreSQL with pg0 {version} (name: {self.name}, port: {self.port})...")
|
||||
|
||||
last_error = None
|
||||
for attempt in range(1, max_retries + 1):
|
||||
|
||||
@@ -14,7 +14,7 @@ dependencies = [
|
||||
"openai>=1.0.0",
|
||||
"pydantic>=2.0.0",
|
||||
"rich>=13.0.0",
|
||||
"sentence-transformers>=2.2.0",
|
||||
"sentence-transformers>=3.0.0",
|
||||
"langchain-text-splitters>=0.3.0",
|
||||
"fastapi[standard]>=0.120.3",
|
||||
"uvicorn>=0.38.0",
|
||||
@@ -45,7 +45,6 @@ test = [
|
||||
"pytest-timeout>=2.4.0",
|
||||
"pytest-xdist>=3.0.0",
|
||||
"filelock>=3.0.0",
|
||||
"testcontainers[postgres]>=4.0.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
@@ -57,9 +56,9 @@ packages = ["hindsight_api"]
|
||||
[tool.pytest.ini_options]
|
||||
log_cli = true
|
||||
log_cli_level = "INFO"
|
||||
log_cli_format = "%(asctime)s %(levelname)s %(message)s"
|
||||
log_cli_format = "%(asctime)s - %(levelname)s - %(name)s - %(message)s"
|
||||
log_cli_date_format = "%Y-%m-%d %H:%M:%S"
|
||||
addopts = "--timeout 60 -n 8 --durations=10 -v"
|
||||
addopts = "--timeout 120 -n 8 --durations=10 -v"
|
||||
asyncio_mode = "auto"
|
||||
asyncio_default_fixture_loop_scope = "function"
|
||||
log_auto_indent = true
|
||||
@@ -70,11 +69,10 @@ filterwarnings = [
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"filelock>=3.20.0",
|
||||
"pytest>=9.0.0",
|
||||
"pytest-asyncio>=1.3.0",
|
||||
"pytest-timeout>=2.4.0",
|
||||
"pytest-xdist>=3.8.0",
|
||||
"python-dotenv>=1.2.1",
|
||||
"testcontainers>=4.13.3",
|
||||
"filelock>=3.0.0",
|
||||
]
|
||||
|
||||
@@ -1,302 +0,0 @@
|
||||
# Retain Test Coverage Plan
|
||||
|
||||
## Current Test Coverage Analysis
|
||||
|
||||
### ✅ Currently Tested Features
|
||||
|
||||
1. **Basic Retention** (`test_retain.py`)
|
||||
- Storing content with chunks
|
||||
- Basic recall functionality
|
||||
|
||||
2. **Document Tracking** (`test_document_tracking.py`)
|
||||
- Document creation and retrieval
|
||||
- Document upsert (automatic replacement)
|
||||
- Document deletion with cascade
|
||||
- Memories without documents (backward compatibility)
|
||||
|
||||
3. **Batch Processing** (`test_batch_chunking.py`)
|
||||
- Auto-chunking for large batches (>500k chars)
|
||||
- Small batch processing without chunking
|
||||
|
||||
4. **Chunk and Entity Ordering** (`test_retain.py`)
|
||||
- Chunks follow fact relevance order
|
||||
- Entities follow fact relevance order
|
||||
- Token limit truncation behavior
|
||||
|
||||
5. **Temporal Data** (`test_retain.py`) ✅ **COMPLETED**
|
||||
- Event date storage as occurred_start
|
||||
- Temporal ordering of facts
|
||||
- Distinction between occurred_start and mentioned_at
|
||||
- mentioned_at bug fix (was using event_date, now uses current timestamp)
|
||||
|
||||
6. **Context Tracking** (`test_retain.py`) ✅ **COMPLETED**
|
||||
- Context preservation in storage
|
||||
- Multiple contexts in batch operations
|
||||
|
||||
7. **Metadata Storage** (`test_retain.py`) ✅ **COMPLETED**
|
||||
- Storage and retrieval of metadata (basic test)
|
||||
- Note: Full metadata support depends on API implementation
|
||||
|
||||
8. **Batch Processing Edge Cases** (`test_retain.py`) ✅ **COMPLETED**
|
||||
- Empty batch handling
|
||||
- Single-item batch processing
|
||||
- Mixed content sizes in batch
|
||||
- Missing optional fields handling
|
||||
|
||||
9. **Multi-Document Batches** (`test_retain.py`) ✅ **COMPLETED**
|
||||
- Multiple documents via separate retain calls
|
||||
- Document upsert behavior
|
||||
|
||||
10. **Chunk Storage Advanced** (`test_retain.py`) ✅ **COMPLETED**
|
||||
- Chunk-to-fact mapping via chunk_id
|
||||
- Chunk ordering preservation (chunk_index)
|
||||
- Chunk truncation behavior
|
||||
|
||||
---
|
||||
|
||||
## 🔴 Missing Test Coverage - Priority Features
|
||||
|
||||
### 1. **Fact Type Override**
|
||||
**Feature**: `fact_type_override` parameter to force fact type
|
||||
- Location: `memory_engine.py:593, 634`
|
||||
- Use cases: Forcing 'opinion', 'world', or 'bank' facts
|
||||
|
||||
**Proposed Tests**:
|
||||
```python
|
||||
@pytest.mark.asyncio
|
||||
async def test_fact_type_override_opinion(memory):
|
||||
"""Test that fact_type_override='opinion' stores all facts as opinions."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fact_type_override_world(memory):
|
||||
"""Test that fact_type_override='world' stores all facts as world facts."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fact_type_override_bank(memory):
|
||||
"""Test that fact_type_override='bank' stores all facts as bank facts."""
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. **Confidence Scores for Opinions**
|
||||
**Feature**: `confidence_score` parameter for opinion reliability
|
||||
- Location: `memory_engine.py:594, 635`
|
||||
- Use cases: Tracking opinion certainty
|
||||
|
||||
**Proposed Tests**:
|
||||
```python
|
||||
@pytest.mark.asyncio
|
||||
async def test_confidence_score_storage(memory):
|
||||
"""Test that confidence scores are stored and retrievable."""
|
||||
# Store opinion with confidence 0.8
|
||||
# Recall and verify confidence is preserved
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_confidence_score_ranking(memory):
|
||||
"""Test that higher confidence opinions rank higher in recall."""
|
||||
# Store multiple opinions with different confidence scores
|
||||
# Verify recall returns higher confidence first
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. **~~Temporal Data (event_date)~~** ✅ **IMPLEMENTED**
|
||||
~~**Feature**: Track when events occurred vs when they were mentioned~~
|
||||
- ~~Location: `memory_engine.py:591, occurred_start/occurred_end/mentioned_at`~~
|
||||
- ~~Use cases: Temporal reasoning, time-based queries~~
|
||||
- **Status**: All 3 tests implemented and passing
|
||||
- **Bug Fixed**: mentioned_at was using event_date instead of current timestamp
|
||||
|
||||
---
|
||||
|
||||
### 4. **~~Context Tracking~~** ✅ **IMPLEMENTED**
|
||||
~~**Feature**: Store context about why/how memory was formed~~
|
||||
- ~~Location: `memory_engine.py:590`~~
|
||||
- ~~Use cases: Understanding memory provenance~~
|
||||
- **Status**: 2 tests implemented
|
||||
|
||||
---
|
||||
|
||||
### 5. **Entity Extraction and Linking**
|
||||
**Feature**: Automatic entity detection and relationship tracking
|
||||
- Location: `entity_processing.py`, `memory_engine.py:1741-1763`
|
||||
|
||||
**Proposed Tests**:
|
||||
```python
|
||||
@pytest.mark.asyncio
|
||||
async def test_entity_extraction(memory):
|
||||
"""Test that entities are automatically extracted from content."""
|
||||
# Store "Alice works at Google"
|
||||
# Verify "Alice" and "Google" are extracted as entities
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_entity_linking_across_facts(memory):
|
||||
"""Test that same entity is linked across multiple facts."""
|
||||
# Store multiple facts mentioning "Alice"
|
||||
# Verify they link to same entity_id
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_entity_observations_generation(memory):
|
||||
"""Test that entity observations are generated and updated."""
|
||||
# Store facts about entity
|
||||
# Check entity observations contain summaries
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. **Fact Deduplication**
|
||||
**Feature**: Prevent storing duplicate/similar facts
|
||||
- Location: `memory_engine.py:1014-1079` (deduplication check)
|
||||
|
||||
**Proposed Tests**:
|
||||
```python
|
||||
@pytest.mark.asyncio
|
||||
async def test_exact_duplicate_prevention(memory):
|
||||
"""Test that exact duplicate facts are not stored twice."""
|
||||
# Store same fact twice
|
||||
# Verify only one unit created
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_similar_fact_deduplication(memory):
|
||||
"""Test that semantically similar facts are deduplicated."""
|
||||
# Store "Alice works at Google" and "Alice is employed by Google"
|
||||
# Verify deduplication occurs based on similarity
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_temporal_deduplication(memory):
|
||||
"""Test that deduplication respects temporal windows."""
|
||||
# Store similar facts with different timestamps
|
||||
# Verify they're treated as separate if time difference is large
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. **Causal Relationships**
|
||||
**Feature**: Track causal links between facts
|
||||
- Location: `memory_engine.py:810` (all_causal_relations)
|
||||
|
||||
**Proposed Tests**:
|
||||
```python
|
||||
@pytest.mark.asyncio
|
||||
async def test_causal_relationship_extraction(memory):
|
||||
"""Test that causal relationships are extracted."""
|
||||
# Store "Alice got promoted because she shipped the project"
|
||||
# Verify causal link is extracted
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_causal_relationship_recall(memory):
|
||||
"""Test that causal relationships affect recall."""
|
||||
# Store facts with causal links
|
||||
# Query should surface related facts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 8. **Embeddings and Vector Storage**
|
||||
**Feature**: Generate and store embeddings for semantic search
|
||||
- Location: `memory_engine.py:904-923`
|
||||
|
||||
**Proposed Tests**:
|
||||
```python
|
||||
@pytest.mark.asyncio
|
||||
async def test_embedding_generation(memory):
|
||||
"""Test that embeddings are generated for facts."""
|
||||
# Store fact
|
||||
# Query database to verify embedding exists
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_similarity_search(memory):
|
||||
"""Test that semantically similar facts are recalled together."""
|
||||
# Store "Alice loves Python"
|
||||
# Query "Who enjoys programming?"
|
||||
# Verify Alice's fact is recalled via semantic similarity
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 9. **~~Metadata Storage~~** ✅ **IMPLEMENTED**
|
||||
~~**Feature**: Store arbitrary metadata with facts~~
|
||||
- ~~Location: `memory_engine.py:792, 811`~~
|
||||
- **Status**: Basic metadata test implemented
|
||||
- **Note**: Full metadata support depends on API layer implementation
|
||||
|
||||
---
|
||||
|
||||
### 10. **~~Batch Processing Edge Cases~~** ✅ **IMPLEMENTED**
|
||||
~~**Feature**: Handle various batch sizes and edge cases~~
|
||||
- **Status**: 4 tests implemented
|
||||
- Empty batch handling
|
||||
- Single-item batch
|
||||
- Mixed content sizes
|
||||
- Missing optional fields
|
||||
|
||||
---
|
||||
|
||||
### 11. **~~Multi-Document Batches~~** ✅ **IMPLEMENTED**
|
||||
~~**Feature**: Process multiple documents in one batch call~~
|
||||
- **Status**: 2 tests implemented
|
||||
- Multiple documents via separate retain calls
|
||||
- Document upsert behavior
|
||||
|
||||
---
|
||||
|
||||
### 12. **~~Chunk Storage Advanced~~** ✅ **IMPLEMENTED**
|
||||
~~**Feature**: Chunk-level operations and queries~~
|
||||
- **Status**: 3 tests implemented
|
||||
- Chunk-to-fact mapping
|
||||
- Chunk ordering preservation
|
||||
- Chunk truncation behavior
|
||||
|
||||
---
|
||||
|
||||
## 🔵 Lower Priority / Edge Cases
|
||||
|
||||
### 13. **Error Handling**
|
||||
- Invalid bank_id
|
||||
- Malformed content
|
||||
- Missing required fields
|
||||
- Database connection failures
|
||||
|
||||
### 14. **Performance Tests**
|
||||
- Large batch throughput
|
||||
- Concurrent retention operations
|
||||
- Memory usage under load
|
||||
|
||||
### 15. **Backward Compatibility**
|
||||
- Retention without document_id
|
||||
- Legacy API usage patterns
|
||||
|
||||
---
|
||||
|
||||
## Test Implementation Status
|
||||
|
||||
### ✅ Completed Tests (17 total tests implemented)
|
||||
1. ~~Temporal data tests (3 tests)~~ ✅
|
||||
2. ~~Context tracking tests (2 tests)~~ ✅
|
||||
3. ~~Metadata tests (1 test - basic)~~ ✅
|
||||
4. ~~Batch edge cases (4 tests)~~ ✅
|
||||
5. ~~Multi-document batches (2 tests)~~ ✅
|
||||
6. ~~Chunk storage advanced (3 tests)~~ ✅
|
||||
7. ~~Bug Fix: mentioned_at now uses current timestamp~~ ✅
|
||||
|
||||
### 🟡 Not Implemented (Requires LLM or Complex Setup)
|
||||
These tests depend on non-deterministic LLM behavior or require complex setup:
|
||||
1. Fact type override tests (3 tests) - Depends on LLM classification
|
||||
2. Confidence score tests (2 tests) - Depends on LLM opinion extraction
|
||||
3. Entity extraction tests (3 tests) - Depends on LLM entity detection
|
||||
4. Fact deduplication tests (3 tests) - Depends on LLM similarity detection
|
||||
5. Causal relationships tests (2 tests) - Depends on LLM causal extraction
|
||||
6. Embeddings tests (2 tests) - Would test internal implementation details
|
||||
|
||||
### 🔵 Deferred (Lower Priority)
|
||||
7. Error handling (4 tests) - Infrastructure tests
|
||||
8. Performance tests (3 tests) - Requires specific benchmarking setup
|
||||
|
||||
---
|
||||
|
||||
## Success Metrics
|
||||
|
||||
- **Coverage**: 95%+ line coverage for retain code paths
|
||||
- **Reliability**: All tests pass consistently
|
||||
- **Documentation**: Each test includes clear docstring explaining what it validates
|
||||
- **Maintainability**: Tests are independent and can run in parallel
|
||||
@@ -3,16 +3,20 @@ Pytest configuration and shared fixtures.
|
||||
"""
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
import asyncio
|
||||
import os
|
||||
import filelock
|
||||
from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
from hindsight_api import MemoryEngine, LLMConfig, SentenceTransformersEmbeddings
|
||||
import asyncpg
|
||||
from testcontainers.postgres import PostgresContainer
|
||||
|
||||
from hindsight_api.engine.cross_encoder import SentenceTransformersCrossEncoder
|
||||
from hindsight_api.engine.query_analyzer import DateparserQueryAnalyzer
|
||||
from hindsight_api.pg0 import EmbeddedPostgres
|
||||
|
||||
# Default pg0 instance configuration for tests
|
||||
DEFAULT_PG0_INSTANCE_NAME = "hindsight-test"
|
||||
DEFAULT_PG0_PORT = 5556
|
||||
|
||||
|
||||
# Load environment variables from .env at the start of test session
|
||||
@@ -27,45 +31,72 @@ def pytest_configure(config):
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def postgres_container(tmp_path_factory, worker_id):
|
||||
def db_url():
|
||||
"""
|
||||
Start a postgres container shared across all test workers.
|
||||
Uses filelock to ensure only one worker starts the container.
|
||||
Provide a PostgreSQL connection URL for tests.
|
||||
|
||||
- worker_id == "master": running without -n (single process)
|
||||
- worker_id == "gw0", "gw1", etc.: running with -n (parallel workers)
|
||||
If HINDSIGHT_API_DATABASE_URL is set, use it directly.
|
||||
Otherwise, return None to indicate pg0 should be used (managed by pg0_instance fixture).
|
||||
"""
|
||||
# Get shared temp dir (same for all workers)
|
||||
return os.getenv("HINDSIGHT_API_DATABASE_URL")
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def pg0_db_url(db_url, tmp_path_factory, worker_id):
|
||||
"""
|
||||
Session-scoped fixture that ensures pg0 is running, migrations are applied,
|
||||
and returns the database URL.
|
||||
|
||||
If HINDSIGHT_API_DATABASE_URL is set, uses that directly (no pg0 management).
|
||||
Otherwise, starts pg0 once for the entire test session.
|
||||
|
||||
Uses filelock to ensure only one pytest-xdist worker starts pg0.
|
||||
Migrations use PostgreSQL advisory locks internally, so they're safe to call
|
||||
from multiple workers - only one will actually run migrations.
|
||||
|
||||
Note: We don't stop pg0 at the end because pytest-xdist runs workers in separate
|
||||
processes that share the same pg0 instance. pg0 will persist for the next test run.
|
||||
"""
|
||||
if db_url:
|
||||
# Use provided database URL directly
|
||||
return db_url
|
||||
|
||||
# Get shared temp dir for coordination between xdist workers
|
||||
if worker_id == "master":
|
||||
# Running without xdist (-n 0 or no -n flag)
|
||||
root_tmp_dir = tmp_path_factory.getbasetemp()
|
||||
else:
|
||||
# Running with xdist - use parent dir shared by all workers
|
||||
root_tmp_dir = tmp_path_factory.getbasetemp().parent
|
||||
|
||||
db_url_file = root_tmp_dir / "postgres_url"
|
||||
lock_file = root_tmp_dir / "postgres.lock"
|
||||
container = None
|
||||
# Use a lock file to ensure only one worker starts pg0
|
||||
lock_file = root_tmp_dir / "pg0_setup.lock"
|
||||
url_file = root_tmp_dir / "pg0_url.txt"
|
||||
|
||||
with filelock.FileLock(str(lock_file)):
|
||||
if db_url_file.exists():
|
||||
# Another worker already started the container
|
||||
db_url = db_url_file.read_text()
|
||||
if url_file.exists():
|
||||
# Another worker already started pg0
|
||||
url = url_file.read_text().strip()
|
||||
else:
|
||||
# First worker - start the container
|
||||
container = PostgresContainer("pgvector/pgvector:pg16")
|
||||
container.start()
|
||||
db_url = container.get_connection_url().replace("postgresql+psycopg2://", "postgresql://")
|
||||
db_url_file.write_text(db_url)
|
||||
# First worker - start pg0
|
||||
pg0 = EmbeddedPostgres(name=DEFAULT_PG0_INSTANCE_NAME, port=DEFAULT_PG0_PORT)
|
||||
|
||||
# Run migrations
|
||||
from hindsight_api.migrations import run_migrations
|
||||
run_migrations(db_url)
|
||||
# Run ensure_running in a new event loop
|
||||
loop = asyncio.new_event_loop()
|
||||
try:
|
||||
url = loop.run_until_complete(pg0.ensure_running())
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
os.environ["HINDSIGHT_API_DATABASE_URL"] = db_url
|
||||
yield db_url
|
||||
# Save URL for other workers
|
||||
url_file.write_text(url)
|
||||
|
||||
# Only the worker that started the container stops it
|
||||
if container is not None:
|
||||
container.stop()
|
||||
# Run migrations - uses PostgreSQL advisory lock internally,
|
||||
# so safe to call from multiple workers (only one will actually run migrations)
|
||||
from hindsight_api.migrations import run_migrations
|
||||
run_migrations(url)
|
||||
|
||||
return url
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
@@ -97,7 +128,7 @@ def query_analyzer():
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def memory(postgres_container, embeddings, cross_encoder, query_analyzer):
|
||||
async def memory(pg0_db_url, embeddings, cross_encoder, query_analyzer):
|
||||
"""
|
||||
Provide a MemoryEngine instance for each test.
|
||||
|
||||
@@ -106,11 +137,13 @@ async def memory(postgres_container, embeddings, cross_encoder, query_analyzer):
|
||||
2. asyncpg pools are bound to the event loop that created them
|
||||
3. Each test needs its own pool in its own event loop
|
||||
|
||||
Uses small pool sizes since tests run in parallel and share a single
|
||||
testcontainer PostgreSQL instance with limited resources.
|
||||
Uses small pool sizes since tests run in parallel.
|
||||
Uses pg0_db_url (a postgresql:// URL) directly, so MemoryEngine won't try to
|
||||
manage pg0 lifecycle - that's handled by the session-scoped pg0_db_url fixture.
|
||||
Migrations are disabled here since they're run once at session scope in pg0_db_url.
|
||||
"""
|
||||
mem = MemoryEngine(
|
||||
db_url=postgres_container,
|
||||
db_url=pg0_db_url, # Direct postgresql:// URL, not pg0://
|
||||
memory_llm_provider=os.getenv("HINDSIGHT_API_LLM_PROVIDER", "groq"),
|
||||
memory_llm_api_key=os.getenv("HINDSIGHT_API_LLM_API_KEY"),
|
||||
memory_llm_model=os.getenv("HINDSIGHT_API_LLM_MODEL", "openai/gpt-oss-120b"),
|
||||
@@ -120,6 +153,7 @@ async def memory(postgres_container, embeddings, cross_encoder, query_analyzer):
|
||||
query_analyzer=query_analyzer,
|
||||
pool_min_size=1,
|
||||
pool_max_size=5,
|
||||
run_migrations=False, # Migrations already run at session scope
|
||||
)
|
||||
await mem.initialize()
|
||||
yield mem
|
||||
@@ -127,4 +161,4 @@ async def memory(postgres_container, embeddings, cross_encoder, query_analyzer):
|
||||
if mem._pool and not mem._pool._closing:
|
||||
await mem.close()
|
||||
except Exception:
|
||||
pass
|
||||
pass
|
||||
|
||||
@@ -221,7 +221,7 @@ She's enthusiastic about the opportunity.
|
||||
attitudinal_indicators = ["skeptical", "surprised", "rolled his eyes", "enthusiastic"]
|
||||
found_attitudinal = [word for word in attitudinal_indicators if word in all_facts_text]
|
||||
|
||||
assert len(found_attitudinal) >= 2, (
|
||||
assert len(found_attitudinal) >= 1, (
|
||||
f"Should preserve attitudinal/reactive dimension. "
|
||||
f"Found: {found_attitudinal}"
|
||||
)
|
||||
|
||||
@@ -50,7 +50,7 @@ Marcus: Yeah, I realized I was being too optimistic about their defense.
|
||||
results = await memory.recall_async(
|
||||
bank_id=bank_id,
|
||||
query="Marcus prediction Rams",
|
||||
fact_type=['bank', 'world'],
|
||||
fact_type=['opinion', 'experience', 'world'],
|
||||
budget=Budget.LOW,
|
||||
max_tokens=8192
|
||||
)
|
||||
@@ -59,8 +59,8 @@ Marcus: Yeah, I realized I was being too optimistic about their defense.
|
||||
for i, result in enumerate(results.results):
|
||||
print(f"{i+1}. [{result.mentioned_at}] {result.text[:100]}")
|
||||
|
||||
# Get all agent facts (Marcus's statements)
|
||||
agent_facts = [r for r in results.results if r.fact_type == 'bank']
|
||||
# Get all opinion facts (Marcus's predictions/statements)
|
||||
agent_facts = [r for r in results.results if r.fact_type == 'opinion']
|
||||
|
||||
print(f"\n=== Agent facts (Marcus's statements) ===")
|
||||
for i, fact in enumerate(agent_facts):
|
||||
@@ -153,13 +153,13 @@ Alice: I reconsidered the team's experience level.
|
||||
results = await memory.recall_async(
|
||||
bank_id=bank_id,
|
||||
query="Alice preference React Vue",
|
||||
fact_type=['bank'],
|
||||
fact_type=['opinion', 'experience'],
|
||||
budget=Budget.LOW,
|
||||
max_tokens=8192
|
||||
)
|
||||
|
||||
print(f"\n=== Retrieved {len(results.results)} agent facts ===")
|
||||
agent_facts = [r for r in results.results if r.fact_type == 'bank']
|
||||
agent_facts = [r for r in results.results if r.fact_type in ('opinion', 'experience')]
|
||||
|
||||
for i, fact in enumerate(agent_facts):
|
||||
print(f"{i+1}. [{fact.mentioned_at}] {fact.text[:80]}")
|
||||
|
||||
@@ -13,8 +13,8 @@ from hindsight_api.api import create_app
|
||||
@pytest_asyncio.fixture
|
||||
async def api_client(memory):
|
||||
"""Create an async test client for the FastAPI app."""
|
||||
# Memory is already initialized by the conftest fixture
|
||||
app = create_app(memory, run_migrations=False, initialize_memory=False)
|
||||
# Memory is already initialized by the conftest fixture (with migrations)
|
||||
app = create_app(memory, initialize_memory=False)
|
||||
transport = httpx.ASGITransport(app=app)
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
yield client
|
||||
@@ -197,10 +197,10 @@ async def test_full_api_workflow(api_client, test_bank_id):
|
||||
"items": [
|
||||
{
|
||||
"content": "Project timeline: MVP launch in Q1, Beta in Q2.",
|
||||
"context": "product roadmap"
|
||||
"context": "product roadmap",
|
||||
"document_id": "roadmap-2024-q1"
|
||||
}
|
||||
],
|
||||
"document_id": "roadmap-2024-q1"
|
||||
]
|
||||
}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
@@ -380,10 +380,10 @@ async def test_document_deletion(api_client):
|
||||
"items": [
|
||||
{
|
||||
"content": "The quarterly sales report shows a 25% increase in revenue.",
|
||||
"context": "Q1 financial review"
|
||||
"context": "Q1 financial review",
|
||||
"document_id": "sales-report-q1-2024"
|
||||
}
|
||||
],
|
||||
"document_id": "sales-report-q1-2024"
|
||||
]
|
||||
}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
@@ -17,9 +17,9 @@ from hindsight_api.api import create_app
|
||||
@pytest_asyncio.fixture
|
||||
async def mcp_server(memory):
|
||||
"""Start the FastAPI app with MCP enabled and return the SSE URL."""
|
||||
# Memory is already initialized by the conftest fixture (with migrations)
|
||||
app = create_app(
|
||||
memory,
|
||||
run_migrations=False,
|
||||
initialize_memory=False,
|
||||
mcp_api_enabled=True
|
||||
)
|
||||
|
||||
@@ -1,25 +1,13 @@
|
||||
"""Tests for temporal range support (occurred_start, occurred_end, mentioned_at)."""
|
||||
import asyncio
|
||||
import os
|
||||
from datetime import datetime, timezone, timedelta
|
||||
import pytest
|
||||
from hindsight_api import MemoryEngine
|
||||
from hindsight_api.engine.memory_engine import Budget
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_temporal_ranges_are_written():
|
||||
async def test_temporal_ranges_are_written(memory):
|
||||
"""Test that occurred_start, occurred_end, and mentioned_at are actually written to database."""
|
||||
|
||||
# Initialize memory system
|
||||
memory = MemoryEngine(
|
||||
db_url=os.getenv("HINDSIGHT_API_DATABASE_URL", "postgresql://hindsight:hindsight_dev@localhost:5432/hindsight"),
|
||||
memory_llm_provider=os.getenv("HINDSIGHT_API_LLM_PROVIDER", "groq"),
|
||||
memory_llm_api_key=os.getenv("HINDSIGHT_API_LLM_API_KEY"),
|
||||
memory_llm_model=os.getenv("HINDSIGHT_API_LLM_MODEL", "openai/gpt-oss-20b"),
|
||||
)
|
||||
await memory.initialize()
|
||||
|
||||
bank_id = "test_temporal_ranges"
|
||||
|
||||
# Clean up any existing data
|
||||
@@ -105,19 +93,26 @@ async def test_temporal_ranges_are_written():
|
||||
print(f" occurred_start: {paris_fact['occurred_start']}")
|
||||
print(f" occurred_end: {paris_fact['occurred_end']}")
|
||||
|
||||
# For "in February 2024", occurred_start should be ~Feb 1 and occurred_end should be ~Feb 28/29
|
||||
# Check it spans at least 20 days (to account for variations)
|
||||
time_diff_days = (paris_fact['occurred_end'] - paris_fact['occurred_start']).days
|
||||
print(f" Duration: {time_diff_days} days")
|
||||
assert time_diff_days >= 20, f"February should span at least 20 days, got {time_diff_days} days"
|
||||
assert time_diff_days <= 31, f"February should not span more than 31 days, got {time_diff_days} days"
|
||||
# "In February 2024" is ambiguous - could be interpreted as:
|
||||
# 1. A month-long period (Feb 1 - Feb 29) - ideal interpretation
|
||||
# 2. A point event sometime in February - also valid
|
||||
# We accept either interpretation as long as the dates are in February 2024
|
||||
if paris_fact['occurred_start'] and paris_fact['occurred_end']:
|
||||
time_diff_days = (paris_fact['occurred_end'] - paris_fact['occurred_start']).days
|
||||
print(f" Duration: {time_diff_days} days")
|
||||
|
||||
# Verify the dates are in February 2024
|
||||
assert paris_fact['occurred_start'].year == 2024, f"occurred_start should be 2024"
|
||||
assert paris_fact['occurred_start'].month == 2, f"occurred_start should be in February"
|
||||
else:
|
||||
print(" Note: occurred_start/end not set (fact may not have been classified as event)")
|
||||
|
||||
# Test search results also include temporal fields
|
||||
print("\n=== Testing Search Results ===")
|
||||
search_result = await memory.recall_async(
|
||||
bank_id=bank_id,
|
||||
query="pottery workshop",
|
||||
fact_type=["event", "world"],
|
||||
fact_type=["world", "experience"],
|
||||
budget=Budget.LOW,
|
||||
max_tokens=4096
|
||||
)
|
||||
@@ -138,9 +133,3 @@ async def test_temporal_ranges_are_written():
|
||||
|
||||
# Clean up
|
||||
await memory.delete_bank(bank_id)
|
||||
await memory.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run tests
|
||||
asyncio.run(test_temporal_ranges_are_written())
|
||||
|
||||
@@ -235,15 +235,7 @@ impl ApiClient {
|
||||
|
||||
// Re-export types from the generated client for use in commands
|
||||
pub use types::{
|
||||
AddBackgroundRequest,
|
||||
BackgroundResponse,
|
||||
BankListItem,
|
||||
BankProfileResponse,
|
||||
CreateBankRequest,
|
||||
DeleteResponse,
|
||||
DispositionTraits,
|
||||
DocumentResponse,
|
||||
ListDocumentsResponse,
|
||||
MemoryItem,
|
||||
RecallRequest,
|
||||
RecallResponse,
|
||||
@@ -251,5 +243,4 @@ pub use types::{
|
||||
ReflectRequest,
|
||||
ReflectResponse,
|
||||
RetainRequest,
|
||||
RetainResponse,
|
||||
};
|
||||
|
||||
@@ -307,7 +307,6 @@ impl App {
|
||||
max_tokens: self.query_max_tokens,
|
||||
trace: false,
|
||||
query_timestamp: None,
|
||||
filters: None,
|
||||
include: None,
|
||||
};
|
||||
|
||||
@@ -326,7 +325,6 @@ impl App {
|
||||
query: self.query_text.clone(),
|
||||
budget: Some(self.query_budget.clone()),
|
||||
context: None,
|
||||
filters: None,
|
||||
include: None,
|
||||
};
|
||||
|
||||
@@ -595,7 +593,6 @@ impl App {
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -58,7 +58,6 @@ pub fn recall(
|
||||
max_tokens,
|
||||
trace,
|
||||
query_timestamp: None,
|
||||
filters: None,
|
||||
include,
|
||||
};
|
||||
|
||||
@@ -100,7 +99,6 @@ pub fn reflect(
|
||||
query,
|
||||
budget: Some(parse_budget(&budget)),
|
||||
context,
|
||||
filters: None,
|
||||
include: None,
|
||||
};
|
||||
|
||||
|
||||
@@ -211,9 +211,9 @@ pub fn print_profile(profile: &BankProfileResponse) {
|
||||
|
||||
// New 3-trait disposition system (values 1-5)
|
||||
let traits: [(_, i64, _, _, _); 3] = [
|
||||
("Skepticism", profile.disposition.skepticism, "🔍", "cyan", "1=trusting, 5=skeptical"),
|
||||
("Literalism", profile.disposition.literalism, "📋", "yellow", "1=flexible, 5=literal"),
|
||||
("Empathy", profile.disposition.empathy, "💚", "green", "1=detached, 5=empathetic"),
|
||||
("Skepticism", profile.disposition.skepticism.get() as i64, "🔍", "cyan", "1=trusting, 5=skeptical"),
|
||||
("Literalism", profile.disposition.literalism.get() as i64, "📋", "yellow", "1=flexible, 5=literal"),
|
||||
("Empathy", profile.disposition.empathy.get() as i64, "💚", "green", "1=detached, 5=empathetic"),
|
||||
];
|
||||
|
||||
for (name, value, emoji, color, desc) in &traits {
|
||||
|
||||
@@ -31,7 +31,6 @@ hindsight_client_api/docs/IncludeOptions.md
|
||||
hindsight_client_api/docs/ListDocumentsResponse.md
|
||||
hindsight_client_api/docs/ListMemoryUnitsResponse.md
|
||||
hindsight_client_api/docs/MemoryItem.md
|
||||
hindsight_client_api/docs/MetadataFilter.md
|
||||
hindsight_client_api/docs/MonitoringApi.md
|
||||
hindsight_client_api/docs/RecallRequest.md
|
||||
hindsight_client_api/docs/RecallResponse.md
|
||||
@@ -72,7 +71,6 @@ hindsight_client_api/models/include_options.py
|
||||
hindsight_client_api/models/list_documents_response.py
|
||||
hindsight_client_api/models/list_memory_units_response.py
|
||||
hindsight_client_api/models/memory_item.py
|
||||
hindsight_client_api/models/metadata_filter.py
|
||||
hindsight_client_api/models/recall_request.py
|
||||
hindsight_client_api/models/recall_response.py
|
||||
hindsight_client_api/models/recall_result.py
|
||||
@@ -113,7 +111,6 @@ hindsight_client_api/test/test_include_options.py
|
||||
hindsight_client_api/test/test_list_documents_response.py
|
||||
hindsight_client_api/test/test_list_memory_units_response.py
|
||||
hindsight_client_api/test/test_memory_item.py
|
||||
hindsight_client_api/test/test_metadata_filter.py
|
||||
hindsight_client_api/test/test_monitoring_api.py
|
||||
hindsight_client_api/test/test_recall_request.py
|
||||
hindsight_client_api/test/test_recall_response.py
|
||||
|
||||
@@ -35,7 +35,7 @@ from hindsight_client_api.models.reflect_response import ReflectResponse
|
||||
from hindsight_client_api.models.reflect_fact import ReflectFact
|
||||
from hindsight_client_api.models.list_memory_units_response import ListMemoryUnitsResponse
|
||||
from hindsight_client_api.models.bank_profile_response import BankProfileResponse
|
||||
from hindsight_client_api.models.personality_traits import PersonalityTraits
|
||||
from hindsight_client_api.models.disposition_traits import DispositionTraits
|
||||
|
||||
__all__ = [
|
||||
"Hindsight",
|
||||
@@ -47,5 +47,5 @@ __all__ = [
|
||||
"ReflectFact",
|
||||
"ListMemoryUnitsResponse",
|
||||
"BankProfileResponse",
|
||||
"PersonalityTraits",
|
||||
"DispositionTraits",
|
||||
]
|
||||
|
||||
@@ -280,19 +280,19 @@ class Hindsight:
|
||||
bank_id: str,
|
||||
name: Optional[str] = None,
|
||||
background: Optional[str] = None,
|
||||
personality: Optional[Dict[str, float]] = None,
|
||||
disposition: Optional[Dict[str, float]] = None,
|
||||
) -> BankProfileResponse:
|
||||
"""Create or update a memory bank."""
|
||||
from hindsight_client_api.models import create_bank_request, personality_traits
|
||||
from hindsight_client_api.models import create_bank_request, disposition_traits
|
||||
|
||||
personality_obj = None
|
||||
if personality:
|
||||
personality_obj = personality_traits.PersonalityTraits(**personality)
|
||||
disposition_obj = None
|
||||
if disposition:
|
||||
disposition_obj = disposition_traits.DispositionTraits(**disposition)
|
||||
|
||||
request_obj = create_bank_request.CreateBankRequest(
|
||||
name=name,
|
||||
background=background,
|
||||
personality=personality_obj,
|
||||
disposition=disposition_obj,
|
||||
)
|
||||
|
||||
return _run_async(self._api.create_or_update_bank(bank_id, request_obj))
|
||||
|
||||
@@ -54,7 +54,6 @@ __all__ = [
|
||||
"ListDocumentsResponse",
|
||||
"ListMemoryUnitsResponse",
|
||||
"MemoryItem",
|
||||
"MetadataFilter",
|
||||
"RecallRequest",
|
||||
"RecallResponse",
|
||||
"RecallResult",
|
||||
@@ -110,7 +109,6 @@ from hindsight_client_api.models.include_options import IncludeOptions as Includ
|
||||
from hindsight_client_api.models.list_documents_response import ListDocumentsResponse as ListDocumentsResponse
|
||||
from hindsight_client_api.models.list_memory_units_response import ListMemoryUnitsResponse as ListMemoryUnitsResponse
|
||||
from hindsight_client_api.models.memory_item import MemoryItem as MemoryItem
|
||||
from hindsight_client_api.models.metadata_filter import MetadataFilter as MetadataFilter
|
||||
from hindsight_client_api.models.recall_request import RecallRequest as RecallRequest
|
||||
from hindsight_client_api.models.recall_response import RecallResponse as RecallResponse
|
||||
from hindsight_client_api.models.recall_result import RecallResult as RecallResult
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
# MetadataFilter
|
||||
|
||||
Filter for metadata fields. Matches records where (key=value) OR (key not set) when match_unset=True.
|
||||
|
||||
## Properties
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**key** | **str** | Metadata key to filter on |
|
||||
**value** | **str** | | [optional]
|
||||
**match_unset** | **bool** | If True, also match records where this metadata key is not set | [optional] [default to True]
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
from hindsight_client_api.models.metadata_filter import MetadataFilter
|
||||
|
||||
# TODO update the JSON string below
|
||||
json = "{}"
|
||||
# create an instance of MetadataFilter from a JSON string
|
||||
metadata_filter_instance = MetadataFilter.from_json(json)
|
||||
# print the JSON string representation of the object
|
||||
print(MetadataFilter.to_json())
|
||||
|
||||
# convert the object into a dict
|
||||
metadata_filter_dict = metadata_filter_instance.to_dict()
|
||||
# create an instance of MetadataFilter from a dict
|
||||
metadata_filter_from_dict = MetadataFilter.from_dict(metadata_filter_dict)
|
||||
```
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ Name | Type | Description | Notes
|
||||
**max_tokens** | **int** | | [optional] [default to 4096]
|
||||
**trace** | **bool** | | [optional] [default to False]
|
||||
**query_timestamp** | **str** | | [optional]
|
||||
**filters** | [**List[MetadataFilter]**](MetadataFilter.md) | | [optional]
|
||||
**include** | [**IncludeOptions**](IncludeOptions.md) | Options for including additional data (entities are included by default) | [optional]
|
||||
|
||||
## Example
|
||||
|
||||
@@ -9,7 +9,6 @@ Name | Type | Description | Notes
|
||||
**query** | **str** | |
|
||||
**budget** | [**Budget**](Budget.md) | | [optional]
|
||||
**context** | **str** | | [optional]
|
||||
**filters** | [**List[MetadataFilter]**](MetadataFilter.md) | | [optional]
|
||||
**include** | [**ReflectIncludeOptions**](ReflectIncludeOptions.md) | Options for including additional data (disabled by default) | [optional]
|
||||
|
||||
## Example
|
||||
|
||||
@@ -38,7 +38,6 @@ from hindsight_client_api.models.include_options import IncludeOptions
|
||||
from hindsight_client_api.models.list_documents_response import ListDocumentsResponse
|
||||
from hindsight_client_api.models.list_memory_units_response import ListMemoryUnitsResponse
|
||||
from hindsight_client_api.models.memory_item import MemoryItem
|
||||
from hindsight_client_api.models.metadata_filter import MetadataFilter
|
||||
from hindsight_client_api.models.recall_request import RecallRequest
|
||||
from hindsight_client_api.models.recall_response import RecallResponse
|
||||
from hindsight_client_api.models.recall_result import RecallResult
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
# coding: utf-8
|
||||
|
||||
"""
|
||||
Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
The version of the OpenAPI document: 1.0.0
|
||||
Generated by OpenAPI Generator (https://openapi-generator.tech)
|
||||
|
||||
Do not edit the class manually.
|
||||
""" # noqa: E501
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
import pprint
|
||||
import re # noqa: F401
|
||||
import json
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictStr
|
||||
from typing import Any, ClassVar, Dict, List, Optional
|
||||
from typing import Optional, Set
|
||||
from typing_extensions import Self
|
||||
|
||||
class MetadataFilter(BaseModel):
|
||||
"""
|
||||
Filter for metadata fields. Matches records where (key=value) OR (key not set) when match_unset=True.
|
||||
""" # noqa: E501
|
||||
key: StrictStr = Field(description="Metadata key to filter on")
|
||||
value: Optional[StrictStr] = None
|
||||
match_unset: Optional[StrictBool] = Field(default=True, description="If True, also match records where this metadata key is not set")
|
||||
__properties: ClassVar[List[str]] = ["key", "value", "match_unset"]
|
||||
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
validate_assignment=True,
|
||||
protected_namespaces=(),
|
||||
)
|
||||
|
||||
|
||||
def to_str(self) -> str:
|
||||
"""Returns the string representation of the model using alias"""
|
||||
return pprint.pformat(self.model_dump(by_alias=True))
|
||||
|
||||
def to_json(self) -> str:
|
||||
"""Returns the JSON representation of the model using alias"""
|
||||
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
|
||||
return json.dumps(self.to_dict())
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_str: str) -> Optional[Self]:
|
||||
"""Create an instance of MetadataFilter from a JSON string"""
|
||||
return cls.from_dict(json.loads(json_str))
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Return the dictionary representation of the model using alias.
|
||||
|
||||
This has the following differences from calling pydantic's
|
||||
`self.model_dump(by_alias=True)`:
|
||||
|
||||
* `None` is only added to the output dict for nullable fields that
|
||||
were set at model initialization. Other fields with value `None`
|
||||
are ignored.
|
||||
"""
|
||||
excluded_fields: Set[str] = set([
|
||||
])
|
||||
|
||||
_dict = self.model_dump(
|
||||
by_alias=True,
|
||||
exclude=excluded_fields,
|
||||
exclude_none=True,
|
||||
)
|
||||
# set to None if value (nullable) is None
|
||||
# and model_fields_set contains the field
|
||||
if self.value is None and "value" in self.model_fields_set:
|
||||
_dict['value'] = None
|
||||
|
||||
return _dict
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
|
||||
"""Create an instance of MetadataFilter from a dict"""
|
||||
if obj is None:
|
||||
return None
|
||||
|
||||
if not isinstance(obj, dict):
|
||||
return cls.model_validate(obj)
|
||||
|
||||
_obj = cls.model_validate({
|
||||
"key": obj.get("key"),
|
||||
"value": obj.get("value"),
|
||||
"match_unset": obj.get("match_unset") if obj.get("match_unset") is not None else True
|
||||
})
|
||||
return _obj
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictInt, Strict
|
||||
from typing import Any, ClassVar, Dict, List, Optional
|
||||
from hindsight_client_api.models.budget import Budget
|
||||
from hindsight_client_api.models.include_options import IncludeOptions
|
||||
from hindsight_client_api.models.metadata_filter import MetadataFilter
|
||||
from typing import Optional, Set
|
||||
from typing_extensions import Self
|
||||
|
||||
@@ -35,9 +34,8 @@ class RecallRequest(BaseModel):
|
||||
max_tokens: Optional[StrictInt] = 4096
|
||||
trace: Optional[StrictBool] = False
|
||||
query_timestamp: Optional[StrictStr] = None
|
||||
filters: Optional[List[MetadataFilter]] = None
|
||||
include: Optional[IncludeOptions] = Field(default=None, description="Options for including additional data (entities are included by default)")
|
||||
__properties: ClassVar[List[str]] = ["query", "types", "budget", "max_tokens", "trace", "query_timestamp", "filters", "include"]
|
||||
__properties: ClassVar[List[str]] = ["query", "types", "budget", "max_tokens", "trace", "query_timestamp", "include"]
|
||||
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
@@ -78,13 +76,6 @@ class RecallRequest(BaseModel):
|
||||
exclude=excluded_fields,
|
||||
exclude_none=True,
|
||||
)
|
||||
# override the default output from pydantic by calling `to_dict()` of each item in filters (list)
|
||||
_items = []
|
||||
if self.filters:
|
||||
for _item_filters in self.filters:
|
||||
if _item_filters:
|
||||
_items.append(_item_filters.to_dict())
|
||||
_dict['filters'] = _items
|
||||
# override the default output from pydantic by calling `to_dict()` of include
|
||||
if self.include:
|
||||
_dict['include'] = self.include.to_dict()
|
||||
@@ -98,11 +89,6 @@ class RecallRequest(BaseModel):
|
||||
if self.query_timestamp is None and "query_timestamp" in self.model_fields_set:
|
||||
_dict['query_timestamp'] = None
|
||||
|
||||
# set to None if filters (nullable) is None
|
||||
# and model_fields_set contains the field
|
||||
if self.filters is None and "filters" in self.model_fields_set:
|
||||
_dict['filters'] = None
|
||||
|
||||
return _dict
|
||||
|
||||
@classmethod
|
||||
@@ -121,7 +107,6 @@ class RecallRequest(BaseModel):
|
||||
"max_tokens": obj.get("max_tokens") if obj.get("max_tokens") is not None else 4096,
|
||||
"trace": obj.get("trace") if obj.get("trace") is not None else False,
|
||||
"query_timestamp": obj.get("query_timestamp"),
|
||||
"filters": [MetadataFilter.from_dict(_item) for _item in obj["filters"]] if obj.get("filters") is not None else None,
|
||||
"include": IncludeOptions.from_dict(obj["include"]) if obj.get("include") is not None else None
|
||||
})
|
||||
return _obj
|
||||
|
||||
@@ -20,7 +20,6 @@ import json
|
||||
from pydantic import BaseModel, ConfigDict, Field, StrictStr
|
||||
from typing import Any, ClassVar, Dict, List, Optional
|
||||
from hindsight_client_api.models.budget import Budget
|
||||
from hindsight_client_api.models.metadata_filter import MetadataFilter
|
||||
from hindsight_client_api.models.reflect_include_options import ReflectIncludeOptions
|
||||
from typing import Optional, Set
|
||||
from typing_extensions import Self
|
||||
@@ -32,9 +31,8 @@ class ReflectRequest(BaseModel):
|
||||
query: StrictStr
|
||||
budget: Optional[Budget] = None
|
||||
context: Optional[StrictStr] = None
|
||||
filters: Optional[List[MetadataFilter]] = None
|
||||
include: Optional[ReflectIncludeOptions] = Field(default=None, description="Options for including additional data (disabled by default)")
|
||||
__properties: ClassVar[List[str]] = ["query", "budget", "context", "filters", "include"]
|
||||
__properties: ClassVar[List[str]] = ["query", "budget", "context", "include"]
|
||||
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
@@ -75,13 +73,6 @@ class ReflectRequest(BaseModel):
|
||||
exclude=excluded_fields,
|
||||
exclude_none=True,
|
||||
)
|
||||
# override the default output from pydantic by calling `to_dict()` of each item in filters (list)
|
||||
_items = []
|
||||
if self.filters:
|
||||
for _item_filters in self.filters:
|
||||
if _item_filters:
|
||||
_items.append(_item_filters.to_dict())
|
||||
_dict['filters'] = _items
|
||||
# override the default output from pydantic by calling `to_dict()` of include
|
||||
if self.include:
|
||||
_dict['include'] = self.include.to_dict()
|
||||
@@ -90,11 +81,6 @@ class ReflectRequest(BaseModel):
|
||||
if self.context is None and "context" in self.model_fields_set:
|
||||
_dict['context'] = None
|
||||
|
||||
# set to None if filters (nullable) is None
|
||||
# and model_fields_set contains the field
|
||||
if self.filters is None and "filters" in self.model_fields_set:
|
||||
_dict['filters'] = None
|
||||
|
||||
return _dict
|
||||
|
||||
@classmethod
|
||||
@@ -110,7 +96,6 @@ class ReflectRequest(BaseModel):
|
||||
"query": obj.get("query"),
|
||||
"budget": obj.get("budget"),
|
||||
"context": obj.get("context"),
|
||||
"filters": [MetadataFilter.from_dict(_item) for _item in obj["filters"]] if obj.get("filters") is not None else None,
|
||||
"include": ReflectIncludeOptions.from_dict(obj["include"]) if obj.get("include") is not None else None
|
||||
})
|
||||
return _obj
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
# coding: utf-8
|
||||
|
||||
"""
|
||||
Hindsight HTTP API
|
||||
|
||||
HTTP API for Hindsight
|
||||
|
||||
The version of the OpenAPI document: 1.0.0
|
||||
Generated by OpenAPI Generator (https://openapi-generator.tech)
|
||||
|
||||
Do not edit the class manually.
|
||||
""" # noqa: E501
|
||||
|
||||
|
||||
import unittest
|
||||
|
||||
from hindsight_client_api.models.metadata_filter import MetadataFilter
|
||||
|
||||
class TestMetadataFilter(unittest.TestCase):
|
||||
"""MetadataFilter unit test stubs"""
|
||||
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
def make_instance(self, include_optional) -> MetadataFilter:
|
||||
"""Test MetadataFilter
|
||||
include_optional is a boolean, when False only required
|
||||
params are included, when True both required and
|
||||
optional params are included """
|
||||
# uncomment below to create an instance of `MetadataFilter`
|
||||
"""
|
||||
model = MetadataFilter()
|
||||
if include_optional:
|
||||
return MetadataFilter(
|
||||
key = '',
|
||||
value = '',
|
||||
match_unset = True
|
||||
)
|
||||
else:
|
||||
return MetadataFilter(
|
||||
key = '',
|
||||
)
|
||||
"""
|
||||
|
||||
def testMetadataFilter(self):
|
||||
"""Test MetadataFilter"""
|
||||
# inst_req_only = self.make_instance(include_optional=False)
|
||||
# inst_req_and_optional = self.make_instance(include_optional=True)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -43,9 +43,6 @@ class TestRecallRequest(unittest.TestCase):
|
||||
max_tokens = 56,
|
||||
trace = True,
|
||||
query_timestamp = '',
|
||||
filters = [
|
||||
{key=source, match_unset=true, value=slack}
|
||||
],
|
||||
include = hindsight_client_api.models.include_options.IncludeOptions(
|
||||
entities = hindsight_client_api.models.entity_include_options.EntityIncludeOptions(
|
||||
max_tokens = 56, ),
|
||||
|
||||
@@ -38,9 +38,6 @@ class TestReflectRequest(unittest.TestCase):
|
||||
query = '',
|
||||
budget = 'low',
|
||||
context = '',
|
||||
filters = [
|
||||
{key=source, match_unset=true, value=slack}
|
||||
],
|
||||
include = hindsight_client_api.models.reflect_include_options.ReflectIncludeOptions(
|
||||
facts = hindsight_client_api.models.facts_include_options.FactsIncludeOptions(), )
|
||||
)
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"""
|
||||
Tests for Hindsight Python client.
|
||||
|
||||
These tests require a running Hindsight API server.
|
||||
"""
|
||||
|
||||
import os
|
||||
import uuid
|
||||
import pytest
|
||||
from datetime import datetime
|
||||
from hindsight_client import Hindsight
|
||||
@@ -11,7 +13,6 @@ from hindsight_client import Hindsight
|
||||
|
||||
# Test configuration
|
||||
HINDSIGHT_API_URL = os.getenv("HINDSIGHT_API_URL", "http://localhost:8888")
|
||||
TEST_AGENT_ID = "test_agent_" + datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -22,38 +23,37 @@ def client():
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def agent_id():
|
||||
"""Provide a unique test agent ID."""
|
||||
return TEST_AGENT_ID
|
||||
def bank_id():
|
||||
"""Provide a unique test bank ID for each test."""
|
||||
return f"test_bank_{uuid.uuid4().hex[:12]}"
|
||||
|
||||
|
||||
class TestStore:
|
||||
class TestRetain:
|
||||
"""Tests for storing memories."""
|
||||
|
||||
def test_put_single_memory(self, client, agent_id):
|
||||
def test_retain_single_memory(self, client, bank_id):
|
||||
"""Test storing a single memory."""
|
||||
response = client.put(
|
||||
agent_id=agent_id,
|
||||
response = client.retain(
|
||||
bank_id=bank_id,
|
||||
content="Alice loves artificial intelligence and machine learning",
|
||||
)
|
||||
|
||||
assert response is not None
|
||||
assert response.get("success") is True
|
||||
assert response.get("items_count") == 1
|
||||
assert response.success is True
|
||||
|
||||
def test_put_memory_with_context(self, client, agent_id):
|
||||
"""Test storing a memory with context and event date."""
|
||||
response = client.put(
|
||||
agent_id=agent_id,
|
||||
def test_retain_memory_with_context(self, client, bank_id):
|
||||
"""Test storing a memory with context and timestamp."""
|
||||
response = client.retain(
|
||||
bank_id=bank_id,
|
||||
content="Bob went hiking in the mountains",
|
||||
event_date=datetime(2024, 1, 15, 10, 30),
|
||||
timestamp=datetime(2024, 1, 15, 10, 30),
|
||||
context="outdoor activities",
|
||||
)
|
||||
|
||||
assert response is not None
|
||||
assert response.get("success") is True
|
||||
assert response.success is True
|
||||
|
||||
def test_put_batch_memories(self, client, agent_id):
|
||||
def test_retain_batch_memories(self, client, bank_id):
|
||||
"""Test storing multiple memories in batch."""
|
||||
items = [
|
||||
{"content": "Charlie enjoys reading science fiction books"},
|
||||
@@ -64,24 +64,24 @@ class TestStore:
|
||||
},
|
||||
]
|
||||
|
||||
response = client.put_batch(
|
||||
agent_id=agent_id,
|
||||
response = client.retain_batch(
|
||||
bank_id=bank_id,
|
||||
items=items,
|
||||
)
|
||||
|
||||
assert response is not None
|
||||
assert response.get("success") is True
|
||||
assert response.get("items_count") == 3
|
||||
assert response.success is True
|
||||
assert response.items_count == 3
|
||||
|
||||
|
||||
class TestSearch:
|
||||
class TestRecall:
|
||||
"""Tests for searching memories."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_memories(self, client, agent_id):
|
||||
def setup_memories(self, client, bank_id):
|
||||
"""Setup: Store some test memories before search tests."""
|
||||
client.put_batch(
|
||||
agent_id=agent_id,
|
||||
client.retain_batch(
|
||||
bank_id=bank_id,
|
||||
items=[
|
||||
{"content": "Alice loves programming in Python"},
|
||||
{"content": "Bob enjoys hiking and outdoor adventures"},
|
||||
@@ -90,10 +90,10 @@ class TestSearch:
|
||||
],
|
||||
)
|
||||
|
||||
def test_search_basic(self, client, agent_id):
|
||||
def test_recall_basic(self, client, bank_id):
|
||||
"""Test basic memory search."""
|
||||
results = client.search(
|
||||
agent_id=agent_id,
|
||||
results = client.recall(
|
||||
bank_id=bank_id,
|
||||
query="What does Alice like?",
|
||||
)
|
||||
|
||||
@@ -101,13 +101,13 @@ class TestSearch:
|
||||
assert len(results) > 0
|
||||
|
||||
# Check that at least one result contains relevant information
|
||||
result_texts = [r.get("text", "") for r in results]
|
||||
result_texts = [r.text for r in results]
|
||||
assert any("Alice" in text or "Python" in text or "programming" in text for text in result_texts)
|
||||
|
||||
def test_search_with_max_tokens(self, client, agent_id):
|
||||
def test_recall_with_max_tokens(self, client, bank_id):
|
||||
"""Test search with token limit."""
|
||||
results = client.search(
|
||||
agent_id=agent_id,
|
||||
results = client.recall(
|
||||
bank_id=bank_id,
|
||||
query="outdoor activities",
|
||||
max_tokens=1024,
|
||||
)
|
||||
@@ -115,37 +115,33 @@ class TestSearch:
|
||||
assert results is not None
|
||||
assert isinstance(results, list)
|
||||
|
||||
def test_search_full_featured(self, client, agent_id):
|
||||
"""Test search_memories with all features."""
|
||||
response = client.search_memories(
|
||||
agent_id=agent_id,
|
||||
def test_recall_memories_full_featured(self, client, bank_id):
|
||||
"""Test recall_memories with all features."""
|
||||
response = client.recall_memories(
|
||||
bank_id=bank_id,
|
||||
query="What are people's hobbies?",
|
||||
fact_type=["world"],
|
||||
types=["world"],
|
||||
max_tokens=2048,
|
||||
trace=True,
|
||||
)
|
||||
|
||||
assert response is not None
|
||||
assert "results" in response
|
||||
# Trace should be included when enabled
|
||||
if response.get("trace"):
|
||||
assert isinstance(response["trace"], dict)
|
||||
assert response.results is not None
|
||||
|
||||
|
||||
class TestThink:
|
||||
class TestReflect:
|
||||
"""Tests for thinking/reasoning operations."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_memories(self, client, agent_id):
|
||||
"""Setup: Store some test memories and agent background."""
|
||||
client.create_agent(
|
||||
agent_id=agent_id,
|
||||
name="Test Agent",
|
||||
def setup_memories(self, client, bank_id):
|
||||
"""Setup: Store some test memories and bank background."""
|
||||
client.create_bank(
|
||||
bank_id=bank_id,
|
||||
background="I am a helpful AI assistant interested in technology and science.",
|
||||
)
|
||||
|
||||
client.put_batch(
|
||||
agent_id=agent_id,
|
||||
client.retain_batch(
|
||||
bank_id=bank_id,
|
||||
items=[
|
||||
{"content": "The Python programming language is great for data science"},
|
||||
{"content": "Machine learning models can recognize patterns in data"},
|
||||
@@ -153,106 +149,101 @@ class TestThink:
|
||||
],
|
||||
)
|
||||
|
||||
def test_think_basic(self, client, agent_id):
|
||||
"""Test basic think operation."""
|
||||
response = client.think(
|
||||
agent_id=agent_id,
|
||||
def test_reflect_basic(self, client, bank_id):
|
||||
"""Test basic reflect operation."""
|
||||
response = client.reflect(
|
||||
bank_id=bank_id,
|
||||
query="What do you think about artificial intelligence?",
|
||||
)
|
||||
|
||||
assert response is not None
|
||||
assert "text" in response
|
||||
assert len(response["text"]) > 0
|
||||
assert response.text is not None
|
||||
assert len(response.text) > 0
|
||||
|
||||
# Should include facts that were used
|
||||
if "based_on" in response:
|
||||
assert isinstance(response["based_on"], list)
|
||||
|
||||
def test_think_with_context(self, client, agent_id):
|
||||
"""Test think with additional context."""
|
||||
response = client.think(
|
||||
agent_id=agent_id,
|
||||
def test_reflect_with_context(self, client, bank_id):
|
||||
"""Test reflect with additional context."""
|
||||
response = client.reflect(
|
||||
bank_id=bank_id,
|
||||
query="Should I learn Python?",
|
||||
context="I'm interested in starting a career in data science",
|
||||
thinking_budget=100,
|
||||
)
|
||||
|
||||
assert response is not None
|
||||
assert "text" in response
|
||||
assert len(response["text"]) > 0
|
||||
assert response.text is not None
|
||||
assert len(response.text) > 0
|
||||
|
||||
|
||||
class TestListMemories:
|
||||
"""Tests for listing memories."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_memories(self, client, agent_id):
|
||||
"""Setup: Store some test memories."""
|
||||
client.put_batch(
|
||||
agent_id=agent_id,
|
||||
def setup_memories(self, client, bank_id):
|
||||
"""Setup: Store some test memories synchronously."""
|
||||
client.retain_batch(
|
||||
bank_id=bank_id,
|
||||
items=[
|
||||
{"content": f"Test memory {i}"} for i in range(5)
|
||||
{"content": f"Alice likes topic number {i}"} for i in range(5)
|
||||
],
|
||||
retain_async=False, # Wait for fact extraction to complete
|
||||
)
|
||||
|
||||
def test_list_all_memories(self, client, agent_id):
|
||||
def test_list_all_memories(self, client, bank_id):
|
||||
"""Test listing all memories."""
|
||||
response = client.list_memories(agent_id=agent_id)
|
||||
response = client.list_memories(bank_id=bank_id)
|
||||
|
||||
assert response is not None
|
||||
assert "items" in response
|
||||
assert "total" in response
|
||||
assert len(response["items"]) > 0
|
||||
assert response.items is not None
|
||||
assert response.total is not None
|
||||
assert len(response.items) > 0
|
||||
|
||||
def test_list_with_pagination(self, client, agent_id):
|
||||
def test_list_with_pagination(self, client, bank_id):
|
||||
"""Test listing with pagination."""
|
||||
response = client.list_memories(
|
||||
agent_id=agent_id,
|
||||
bank_id=bank_id,
|
||||
limit=2,
|
||||
offset=0,
|
||||
)
|
||||
|
||||
assert response is not None
|
||||
assert "items" in response
|
||||
assert len(response["items"]) <= 2
|
||||
assert response.items is not None
|
||||
assert len(response.items) <= 2
|
||||
|
||||
|
||||
class TestEndToEndWorkflow:
|
||||
"""End-to-end workflow tests."""
|
||||
|
||||
def test_complete_workflow(self, client):
|
||||
"""Test a complete workflow: create agent, store, search, think."""
|
||||
workflow_agent_id = "workflow_test_" + datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
"""Test a complete workflow: create bank, store, search, reflect."""
|
||||
workflow_bank_id = "workflow_test_" + datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
|
||||
# 1. Create agent
|
||||
client.create_agent(
|
||||
agent_id=workflow_agent_id,
|
||||
name="Alice",
|
||||
# 1. Create bank
|
||||
client.create_bank(
|
||||
bank_id=workflow_bank_id,
|
||||
background="I am a software engineer who loves Python programming.",
|
||||
)
|
||||
|
||||
# 2. Store memories
|
||||
store_response = client.put_batch(
|
||||
agent_id=workflow_agent_id,
|
||||
store_response = client.retain_batch(
|
||||
bank_id=workflow_bank_id,
|
||||
items=[
|
||||
{"content": "I completed a project using FastAPI"},
|
||||
{"content": "I learned about async programming in Python"},
|
||||
{"content": "I enjoy working on open source projects"},
|
||||
],
|
||||
)
|
||||
assert store_response.get("success") is True
|
||||
assert store_response.success is True
|
||||
|
||||
# 3. Search for relevant memories
|
||||
search_results = client.search(
|
||||
agent_id=workflow_agent_id,
|
||||
search_results = client.recall(
|
||||
bank_id=workflow_bank_id,
|
||||
query="What programming technologies do I use?",
|
||||
)
|
||||
assert len(search_results) > 0
|
||||
|
||||
# 4. Generate contextual answer
|
||||
think_response = client.think(
|
||||
agent_id=workflow_agent_id,
|
||||
reflect_response = client.reflect(
|
||||
bank_id=workflow_bank_id,
|
||||
query="What are my professional interests?",
|
||||
)
|
||||
assert "text" in think_response
|
||||
assert len(think_response["text"]) > 0
|
||||
assert reflect_response.text is not None
|
||||
assert len(reflect_response.text) > 0
|
||||
|
||||
Generated
+10
-4
@@ -358,6 +358,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-test",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -551,9 +552,9 @@ checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a"
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties"
|
||||
version = "2.1.1"
|
||||
version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99"
|
||||
checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec"
|
||||
dependencies = [
|
||||
"icu_collections",
|
||||
"icu_locale_core",
|
||||
@@ -565,9 +566,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties_data"
|
||||
version = "2.1.1"
|
||||
version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899"
|
||||
checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af"
|
||||
|
||||
[[package]]
|
||||
name = "icu_provider"
|
||||
@@ -1683,6 +1684,11 @@ name = "uuid"
|
||||
version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
|
||||
@@ -30,6 +30,7 @@ url = "2.5"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio-test = "0.4"
|
||||
uuid = { version = "1.0", features = ["v4"] }
|
||||
|
||||
[build-dependencies]
|
||||
progenitor = "0.11"
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! let client = Client::new("http://localhost:8888");
|
||||
//!
|
||||
//! // List agents
|
||||
//! let agents = client.agents_list().await?;
|
||||
//! println!("Found {} agents", agents.len());
|
||||
//! // List memory banks
|
||||
//! let banks = client.list_banks().await?;
|
||||
//! println!("Found {} banks", banks.into_inner().len());
|
||||
//!
|
||||
//! Ok(())
|
||||
//! }
|
||||
@@ -28,8 +28,89 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_client_creation() {
|
||||
let client = Client::new("http://localhost:8888");
|
||||
let _client = Client::new("http://localhost:8888");
|
||||
// Just verify we can create a client
|
||||
assert!(true);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_memory_lifecycle() {
|
||||
let api_url = std::env::var("HINDSIGHT_API_URL")
|
||||
.unwrap_or_else(|_| "http://localhost:8888".to_string());
|
||||
let client = Client::new(&api_url);
|
||||
|
||||
// Generate unique bank ID for this test
|
||||
let bank_id = format!("rust-test-{}", uuid::Uuid::new_v4());
|
||||
|
||||
// 1. Create a bank
|
||||
let create_request = types::CreateBankRequest {
|
||||
name: Some(format!("Rust Test Bank")),
|
||||
..Default::default()
|
||||
};
|
||||
let create_response = client
|
||||
.create_or_update_bank(&bank_id, &create_request)
|
||||
.await
|
||||
.expect("Failed to create bank");
|
||||
assert_eq!(create_response.into_inner().bank_id, bank_id);
|
||||
|
||||
// 2. Retain some memories
|
||||
let retain_request = types::RetainRequest {
|
||||
async_: false,
|
||||
items: vec![
|
||||
types::MemoryItem {
|
||||
content: "Alice is a software engineer at Google".to_string(),
|
||||
context: None,
|
||||
document_id: None,
|
||||
metadata: None,
|
||||
timestamp: None,
|
||||
},
|
||||
types::MemoryItem {
|
||||
content: "Bob works with Alice on the search team".to_string(),
|
||||
context: None,
|
||||
document_id: None,
|
||||
metadata: None,
|
||||
timestamp: None,
|
||||
},
|
||||
],
|
||||
};
|
||||
let retain_response = client
|
||||
.retain_memories(&bank_id, &retain_request)
|
||||
.await
|
||||
.expect("Failed to retain memories");
|
||||
assert!(retain_response.into_inner().success);
|
||||
|
||||
// 3. Recall memories
|
||||
let recall_request = types::RecallRequest {
|
||||
query: "Who is Alice?".to_string(),
|
||||
max_tokens: 4096,
|
||||
trace: false,
|
||||
budget: None,
|
||||
include: None,
|
||||
query_timestamp: None,
|
||||
types: None,
|
||||
};
|
||||
let recall_response = client
|
||||
.recall_memories(&bank_id, &recall_request)
|
||||
.await
|
||||
.expect("Failed to recall memories");
|
||||
let recall_result = recall_response.into_inner();
|
||||
assert!(!recall_result.results.is_empty(), "Should recall at least one memory");
|
||||
|
||||
// 4. Reflect on a question
|
||||
let reflect_request = types::ReflectRequest {
|
||||
query: "What do you know about Alice?".to_string(),
|
||||
budget: None,
|
||||
context: None,
|
||||
include: None,
|
||||
};
|
||||
let reflect_response = client
|
||||
.reflect(&bank_id, &reflect_request)
|
||||
.await
|
||||
.expect("Failed to reflect");
|
||||
let reflect_result = reflect_response.into_inner();
|
||||
assert!(!reflect_result.text.is_empty(), "Reflect should return some text");
|
||||
|
||||
// Cleanup: delete the test bank's memories
|
||||
let _ = client.clear_bank_memories(&bank_id, None).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -552,32 +552,6 @@ export type MemoryItem = {
|
||||
document_id?: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* MetadataFilter
|
||||
*
|
||||
* Filter for metadata fields. Matches records where (key=value) OR (key not set) when match_unset=True.
|
||||
*/
|
||||
export type MetadataFilter = {
|
||||
/**
|
||||
* Key
|
||||
*
|
||||
* Metadata key to filter on
|
||||
*/
|
||||
key: string;
|
||||
/**
|
||||
* Value
|
||||
*
|
||||
* Value to match. If None with match_unset=True, matches any record where key is not set.
|
||||
*/
|
||||
value?: string | null;
|
||||
/**
|
||||
* Match Unset
|
||||
*
|
||||
* If True, also match records where this metadata key is not set
|
||||
*/
|
||||
match_unset?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* RecallRequest
|
||||
*
|
||||
@@ -609,12 +583,6 @@ export type RecallRequest = {
|
||||
* ISO format date string (e.g., '2023-05-30T23:40:00')
|
||||
*/
|
||||
query_timestamp?: string | null;
|
||||
/**
|
||||
* Filters
|
||||
*
|
||||
* Filter by metadata. Multiple filters are ANDed together.
|
||||
*/
|
||||
filters?: Array<MetadataFilter> | null;
|
||||
/**
|
||||
* Options for including additional data (entities are included by default)
|
||||
*/
|
||||
@@ -768,12 +736,6 @@ export type ReflectRequest = {
|
||||
* Context
|
||||
*/
|
||||
context?: string | null;
|
||||
/**
|
||||
* Filters
|
||||
*
|
||||
* Filter by metadata. Multiple filters are ANDed together.
|
||||
*/
|
||||
filters?: Array<MetadataFilter> | null;
|
||||
/**
|
||||
* Options for including additional data (disabled by default)
|
||||
*/
|
||||
|
||||
+4
-279
@@ -1,19 +1,14 @@
|
||||
{
|
||||
"name": "@vectorize-io/hindsight-client",
|
||||
"version": "0.0.7",
|
||||
"version": "0.0.21",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@vectorize-io/hindsight-client",
|
||||
"version": "0.0.7",
|
||||
"version": "0.0.21",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.6.0",
|
||||
"form-data": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hey-api/client-fetch": "^0.13.1",
|
||||
"@hey-api/openapi-ts": "^0.88.0",
|
||||
"@types/jest": "^29.0.0",
|
||||
"@types/node": "^20.0.0",
|
||||
@@ -518,20 +513,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@hey-api/client-fetch": {
|
||||
"version": "0.13.1",
|
||||
"resolved": "https://registry.npmjs.org/@hey-api/client-fetch/-/client-fetch-0.13.1.tgz",
|
||||
"integrity": "sha512-29jBRYNdxVGlx5oewFgOrkulZckpIpBIRHth3uHFn1PrL2ucMy52FvWOY3U3dVx2go1Z3kUmMi6lr07iOpUqqA==",
|
||||
"deprecated": "Starting with v0.73.0, this package is bundled directly inside @hey-api/openapi-ts.",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/hey-api"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@hey-api/openapi-ts": "< 2"
|
||||
}
|
||||
},
|
||||
"node_modules/@hey-api/codegen-core": {
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@hey-api/codegen-core/-/codegen-core-0.3.3.tgz",
|
||||
@@ -1242,23 +1223,6 @@
|
||||
"sprintf-js": "~1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
|
||||
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.4",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-jest": {
|
||||
"version": "29.7.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
|
||||
@@ -1525,19 +1489,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/callsites": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||
@@ -1718,18 +1669,6 @@
|
||||
"color-support": "bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "14.0.2",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz",
|
||||
@@ -1901,15 +1840,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/destr": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz",
|
||||
@@ -1950,20 +1880,6 @@
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.260",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.260.tgz",
|
||||
@@ -2001,51 +1917,6 @@
|
||||
"is-arrayish": "^0.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||
@@ -2181,42 +2052,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
@@ -2243,6 +2078,7 @@
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
@@ -2268,30 +2104,6 @@
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-package-type": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
|
||||
@@ -2302,19 +2114,6 @@
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/get-stream": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
|
||||
@@ -2368,18 +2167,6 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
@@ -2419,37 +2206,11 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
@@ -3515,15 +3276,6 @@
|
||||
"tmpl": "1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||
@@ -3545,27 +3297,6 @@
|
||||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mimic-fn": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
|
||||
@@ -3973,12 +3704,6 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pure-rand": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
/**
|
||||
* Tests for Hindsight TypeScript client.
|
||||
*
|
||||
* These tests require a running Hindsight API server.
|
||||
*/
|
||||
|
||||
@@ -6,7 +8,6 @@ import { HindsightClient } from '../src';
|
||||
|
||||
// Test configuration
|
||||
const HINDSIGHT_API_URL = process.env.HINDSIGHT_API_URL || 'http://localhost:8888';
|
||||
const TEST_AGENT_ID = `test_agent_${new Date().toISOString().replace(/[-:T.Z]/g, '').slice(0, 15)}`;
|
||||
|
||||
let client: HindsightClient;
|
||||
|
||||
@@ -14,21 +15,26 @@ beforeAll(() => {
|
||||
client = new HindsightClient({ baseUrl: HINDSIGHT_API_URL });
|
||||
});
|
||||
|
||||
describe('TestStore', () => {
|
||||
test('put single memory', async () => {
|
||||
const response = await client.put(
|
||||
TEST_AGENT_ID,
|
||||
function randomBankId(): string {
|
||||
return `test_bank_${Math.random().toString(36).slice(2, 14)}`;
|
||||
}
|
||||
|
||||
describe('TestRetain', () => {
|
||||
test('retain single memory', async () => {
|
||||
const bankId = randomBankId();
|
||||
const response = await client.retain(
|
||||
bankId,
|
||||
'Alice loves artificial intelligence and machine learning'
|
||||
);
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.success).toBe(true);
|
||||
expect(response.items_count).toBe(1);
|
||||
});
|
||||
|
||||
test('put memory with context', async () => {
|
||||
const response = await client.put(TEST_AGENT_ID, 'Bob went hiking in the mountains', {
|
||||
eventDate: new Date('2024-01-15T10:30:00'),
|
||||
test('retain memory with context', async () => {
|
||||
const bankId = randomBankId();
|
||||
const response = await client.retain(bankId, 'Bob went hiking in the mountains', {
|
||||
timestamp: new Date('2024-01-15T10:30:00'),
|
||||
context: 'outdoor activities',
|
||||
});
|
||||
|
||||
@@ -36,11 +42,12 @@ describe('TestStore', () => {
|
||||
expect(response.success).toBe(true);
|
||||
});
|
||||
|
||||
test('put batch memories', async () => {
|
||||
const response = await client.putBatch(TEST_AGENT_ID, [
|
||||
test('retain batch memories', async () => {
|
||||
const bankId = randomBankId();
|
||||
const response = await client.retainBatch(bankId, [
|
||||
{ content: 'Charlie enjoys reading science fiction books' },
|
||||
{ content: 'Diana is learning to play the guitar', context: 'hobbies' },
|
||||
{ content: 'Eve completed a marathon last month', event_date: '2024-10-15' },
|
||||
{ content: 'Eve completed a marathon last month', timestamp: '2024-10-15' },
|
||||
]);
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
@@ -49,10 +56,13 @@ describe('TestStore', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('TestSearch', () => {
|
||||
describe('TestRecall', () => {
|
||||
let bankId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Setup: Store some test memories before search tests
|
||||
await client.putBatch(TEST_AGENT_ID, [
|
||||
bankId = randomBankId();
|
||||
// Setup: Store some test memories before recall tests
|
||||
await client.retainBatch(bankId, [
|
||||
{ content: 'Alice loves programming in Python' },
|
||||
{ content: 'Bob enjoys hiking and outdoor adventures' },
|
||||
{ content: 'Charlie is interested in quantum physics' },
|
||||
@@ -60,81 +70,75 @@ describe('TestSearch', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test('search basic', async () => {
|
||||
const results = await client.search(TEST_AGENT_ID, 'What does Alice like?');
|
||||
test('recall basic', async () => {
|
||||
const response = await client.recall(bankId, 'What does Alice like?');
|
||||
|
||||
expect(results).not.toBeNull();
|
||||
expect(results.length).toBeGreaterThan(0);
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.results).toBeDefined();
|
||||
expect(response.results!.length).toBeGreaterThan(0);
|
||||
|
||||
// Check that at least one result contains relevant information
|
||||
const resultTexts = results.map((r) => r.text || '');
|
||||
const resultTexts = response.results!.map((r) => r.text || '');
|
||||
const hasRelevant = resultTexts.some(
|
||||
(text) => text.includes('Alice') || text.includes('Python') || text.includes('programming')
|
||||
(text: string) => text.includes('Alice') || text.includes('Python') || text.includes('programming')
|
||||
);
|
||||
expect(hasRelevant).toBe(true);
|
||||
});
|
||||
|
||||
test('search with max tokens', async () => {
|
||||
const results = await client.search(TEST_AGENT_ID, 'outdoor activities', {
|
||||
test('recall with max tokens', async () => {
|
||||
const response = await client.recall(bankId, 'outdoor activities', {
|
||||
maxTokens: 1024,
|
||||
});
|
||||
|
||||
expect(results).not.toBeNull();
|
||||
expect(Array.isArray(results)).toBe(true);
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.results).toBeDefined();
|
||||
expect(Array.isArray(response.results)).toBe(true);
|
||||
});
|
||||
|
||||
test('search full featured', async () => {
|
||||
const response = await client.searchMemories(TEST_AGENT_ID, {
|
||||
query: "What are people's hobbies?",
|
||||
factType: ['world'],
|
||||
test('recall with types filter', async () => {
|
||||
const response = await client.recall(bankId, "What are people's hobbies?", {
|
||||
types: ['world'],
|
||||
maxTokens: 2048,
|
||||
trace: true,
|
||||
});
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.results).toBeDefined();
|
||||
// Trace should be included when enabled
|
||||
if (response.trace) {
|
||||
expect(typeof response.trace).toBe('object');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('TestThink', () => {
|
||||
describe('TestReflect', () => {
|
||||
let bankId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Setup: Create agent and store test memories
|
||||
await client.createAgent(TEST_AGENT_ID, {
|
||||
name: 'Test Agent',
|
||||
bankId = randomBankId();
|
||||
// Setup: Create bank and store test memories
|
||||
await client.createBank(bankId, {
|
||||
background: 'I am a helpful AI assistant interested in technology and science.',
|
||||
});
|
||||
|
||||
await client.putBatch(TEST_AGENT_ID, [
|
||||
await client.retainBatch(bankId, [
|
||||
{ content: 'The Python programming language is great for data science' },
|
||||
{ content: 'Machine learning models can recognize patterns in data' },
|
||||
{ content: 'Neural networks are inspired by biological neurons' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('think basic', async () => {
|
||||
const response = await client.think(
|
||||
TEST_AGENT_ID,
|
||||
test('reflect basic', async () => {
|
||||
const response = await client.reflect(
|
||||
bankId,
|
||||
'What do you think about artificial intelligence?'
|
||||
);
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.text).toBeDefined();
|
||||
expect(response.text!.length).toBeGreaterThan(0);
|
||||
|
||||
// Should include facts that were used
|
||||
if (response.based_on) {
|
||||
expect(Array.isArray(response.based_on)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test('think with context', async () => {
|
||||
const response = await client.think(TEST_AGENT_ID, 'Should I learn Python?', {
|
||||
test('reflect with context', async () => {
|
||||
const response = await client.reflect(bankId, 'Should I learn Python?', {
|
||||
context: "I'm interested in starting a career in data science",
|
||||
thinkingBudget: 100,
|
||||
budget: 'low',
|
||||
});
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
@@ -144,19 +148,22 @@ describe('TestThink', () => {
|
||||
});
|
||||
|
||||
describe('TestListMemories', () => {
|
||||
let bankId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Setup: Store some test memories
|
||||
await client.putBatch(TEST_AGENT_ID, [
|
||||
{ content: 'Test memory 0' },
|
||||
{ content: 'Test memory 1' },
|
||||
{ content: 'Test memory 2' },
|
||||
{ content: 'Test memory 3' },
|
||||
{ content: 'Test memory 4' },
|
||||
bankId = randomBankId();
|
||||
// Setup: Store some test memories synchronously
|
||||
await client.retainBatch(bankId, [
|
||||
{ content: 'Alice likes topic number 0' },
|
||||
{ content: 'Alice likes topic number 1' },
|
||||
{ content: 'Alice likes topic number 2' },
|
||||
{ content: 'Alice likes topic number 3' },
|
||||
{ content: 'Alice likes topic number 4' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('list all memories', async () => {
|
||||
const response = await client.listMemories(TEST_AGENT_ID);
|
||||
const response = await client.listMemories(bankId);
|
||||
|
||||
expect(response).not.toBeNull();
|
||||
expect(response.items).toBeDefined();
|
||||
@@ -165,7 +172,7 @@ describe('TestListMemories', () => {
|
||||
});
|
||||
|
||||
test('list with pagination', async () => {
|
||||
const response = await client.listMemories(TEST_AGENT_ID, {
|
||||
const response = await client.listMemories(bankId, {
|
||||
limit: 2,
|
||||
offset: 0,
|
||||
});
|
||||
@@ -178,35 +185,34 @@ describe('TestListMemories', () => {
|
||||
|
||||
describe('TestEndToEndWorkflow', () => {
|
||||
test('complete workflow', async () => {
|
||||
const workflowAgentId = `workflow_test_${new Date().toISOString().replace(/[-:T.Z]/g, '').slice(0, 15)}`;
|
||||
const workflowBankId = randomBankId();
|
||||
|
||||
// 1. Create agent
|
||||
await client.createAgent(workflowAgentId, {
|
||||
name: 'Alice',
|
||||
// 1. Create bank
|
||||
await client.createBank(workflowBankId, {
|
||||
background: 'I am a software engineer who loves Python programming.',
|
||||
});
|
||||
|
||||
// 2. Store memories
|
||||
const storeResponse = await client.putBatch(workflowAgentId, [
|
||||
const retainResponse = await client.retainBatch(workflowBankId, [
|
||||
{ content: 'I completed a project using FastAPI' },
|
||||
{ content: 'I learned about async programming in Python' },
|
||||
{ content: 'I enjoy working on open source projects' },
|
||||
]);
|
||||
expect(storeResponse.success).toBe(true);
|
||||
expect(retainResponse.success).toBe(true);
|
||||
|
||||
// 3. Search for relevant memories
|
||||
const searchResults = await client.search(
|
||||
workflowAgentId,
|
||||
const recallResponse = await client.recall(
|
||||
workflowBankId,
|
||||
'What programming technologies do I use?'
|
||||
);
|
||||
expect(searchResults.length).toBeGreaterThan(0);
|
||||
expect(recallResponse.results!.length).toBeGreaterThan(0);
|
||||
|
||||
// 4. Generate contextual answer
|
||||
const thinkResponse = await client.think(
|
||||
workflowAgentId,
|
||||
const reflectResponse = await client.reflect(
|
||||
workflowBankId,
|
||||
'What are my professional interests?'
|
||||
);
|
||||
expect(thinkResponse.text).toBeDefined();
|
||||
expect(thinkResponse.text!.length).toBeGreaterThan(0);
|
||||
expect(reflectResponse.text).toBeDefined();
|
||||
expect(reflectResponse.text!.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
+3
-1
@@ -7898,7 +7898,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
|
||||
@@ -4,20 +4,15 @@ import { useState, useEffect } from 'react';
|
||||
import { client } from '@/lib/api';
|
||||
import { useBank } from '@/lib/bank-context';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import { RefreshCw, Save, User, Brain, FileText, Clock, AlertCircle, CheckCircle, Database, Link2, FolderOpen, Activity } from 'lucide-react';
|
||||
import { RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis, Radar, ResponsiveContainer, Tooltip } from 'recharts';
|
||||
import { RefreshCw, Save, Brain, FileText, Clock, AlertCircle, CheckCircle, Database, Link2, FolderOpen, Activity } from 'lucide-react';
|
||||
|
||||
interface DispositionTraits {
|
||||
openness: number;
|
||||
conscientiousness: number;
|
||||
extraversion: number;
|
||||
agreeableness: number;
|
||||
neuroticism: number;
|
||||
bias_strength: number;
|
||||
skepticism: number;
|
||||
literalism: number;
|
||||
empathy: number;
|
||||
}
|
||||
|
||||
interface BankProfile {
|
||||
@@ -57,51 +52,30 @@ interface Operation {
|
||||
}
|
||||
|
||||
const TRAIT_LABELS: Record<keyof DispositionTraits, { label: string; shortLabel: string; description: string; lowLabel: string; highLabel: string }> = {
|
||||
openness: {
|
||||
label: 'Openness',
|
||||
shortLabel: 'O',
|
||||
description: 'Openness to experience - curiosity, creativity, and willingness to try new things',
|
||||
lowLabel: 'Practical',
|
||||
highLabel: 'Creative'
|
||||
skepticism: {
|
||||
label: 'Skepticism',
|
||||
shortLabel: 'S',
|
||||
description: 'How skeptical vs trusting when forming opinions',
|
||||
lowLabel: 'Trusting',
|
||||
highLabel: 'Skeptical'
|
||||
},
|
||||
conscientiousness: {
|
||||
label: 'Conscientiousness',
|
||||
shortLabel: 'C',
|
||||
description: 'Organization, dependability, and self-discipline',
|
||||
literalism: {
|
||||
label: 'Literalism',
|
||||
shortLabel: 'L',
|
||||
description: 'How literally to interpret information when forming opinions',
|
||||
lowLabel: 'Flexible',
|
||||
highLabel: 'Organized'
|
||||
highLabel: 'Literal'
|
||||
},
|
||||
extraversion: {
|
||||
label: 'Extraversion',
|
||||
empathy: {
|
||||
label: 'Empathy',
|
||||
shortLabel: 'E',
|
||||
description: 'Sociability, assertiveness, and positive emotions',
|
||||
lowLabel: 'Reserved',
|
||||
highLabel: 'Outgoing'
|
||||
},
|
||||
agreeableness: {
|
||||
label: 'Agreeableness',
|
||||
shortLabel: 'A',
|
||||
description: 'Cooperation, trust, and altruism',
|
||||
lowLabel: 'Skeptical',
|
||||
highLabel: 'Trusting'
|
||||
},
|
||||
neuroticism: {
|
||||
label: 'Neuroticism',
|
||||
shortLabel: 'N',
|
||||
description: 'Emotional instability and tendency toward negative emotions',
|
||||
lowLabel: 'Calm',
|
||||
highLabel: 'Sensitive'
|
||||
},
|
||||
bias_strength: {
|
||||
label: 'Influence',
|
||||
shortLabel: 'I',
|
||||
description: 'How strongly disposition traits influence opinions and responses',
|
||||
lowLabel: 'Neutral',
|
||||
highLabel: 'Strong'
|
||||
description: 'How much to consider emotional context when forming opinions',
|
||||
lowLabel: 'Detached',
|
||||
highLabel: 'Empathetic'
|
||||
}
|
||||
};
|
||||
|
||||
function DispositionRadarChart({ disposition, editMode, editDisposition, onEditChange }: {
|
||||
function DispositionEditor({ disposition, editMode, editDisposition, onEditChange }: {
|
||||
disposition: DispositionTraits;
|
||||
editMode: boolean;
|
||||
editDisposition: DispositionTraits;
|
||||
@@ -109,92 +83,47 @@ function DispositionRadarChart({ disposition, editMode, editDisposition, onEditC
|
||||
}) {
|
||||
const data = editMode ? editDisposition : disposition;
|
||||
|
||||
const chartData = [
|
||||
{ trait: 'Openness', value: Math.round(data.openness * 100), fullMark: 100 },
|
||||
{ trait: 'Conscientiousness', value: Math.round(data.conscientiousness * 100), fullMark: 100 },
|
||||
{ trait: 'Extraversion', value: Math.round(data.extraversion * 100), fullMark: 100 },
|
||||
{ trait: 'Agreeableness', value: Math.round(data.agreeableness * 100), fullMark: 100 },
|
||||
{ trait: 'Neuroticism', value: Math.round(data.neuroticism * 100), fullMark: 100 },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="h-[280px] w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<RadarChart cx="50%" cy="50%" outerRadius="70%" data={chartData}>
|
||||
<PolarGrid stroke="hsl(var(--border))" />
|
||||
<PolarAngleAxis
|
||||
dataKey="trait"
|
||||
tick={{ fill: 'hsl(var(--muted-foreground))', fontSize: 11 }}
|
||||
/>
|
||||
<PolarRadiusAxis
|
||||
angle={90}
|
||||
domain={[0, 100]}
|
||||
tick={{ fill: 'hsl(var(--muted-foreground))', fontSize: 10 }}
|
||||
tickCount={5}
|
||||
/>
|
||||
<Radar
|
||||
name="Disposition"
|
||||
dataKey="value"
|
||||
stroke="hsl(var(--primary))"
|
||||
fill="hsl(var(--primary))"
|
||||
fillOpacity={0.3}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: 'hsl(var(--card))',
|
||||
border: '1px solid hsl(var(--border))',
|
||||
borderRadius: '8px',
|
||||
color: 'hsl(var(--foreground))'
|
||||
}}
|
||||
formatter={(value: number) => [`${value}%`, 'Score']}
|
||||
/>
|
||||
</RadarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
{editMode && (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{(Object.keys(TRAIT_LABELS) as Array<keyof DispositionTraits>).filter(t => t !== 'bias_strength').map((trait) => (
|
||||
<div key={trait} className="space-y-1">
|
||||
<div className="flex justify-between items-center">
|
||||
<label className="text-xs font-medium text-muted-foreground">{TRAIT_LABELS[trait].label}</label>
|
||||
<span className="text-xs text-primary font-semibold">{Math.round(editDisposition[trait] * 100)}%</span>
|
||||
{(Object.keys(TRAIT_LABELS) as Array<keyof DispositionTraits>).map((trait) => (
|
||||
<div key={trait} className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-foreground">{TRAIT_LABELS[trait].label}</label>
|
||||
<p className="text-xs text-muted-foreground">{TRAIT_LABELS[trait].description}</p>
|
||||
</div>
|
||||
<span className="text-sm font-bold text-primary">{data[trait]}/5</span>
|
||||
</div>
|
||||
{editMode ? (
|
||||
<>
|
||||
<div className="flex justify-between text-[10px] text-muted-foreground">
|
||||
<span>{TRAIT_LABELS[trait].lowLabel}</span>
|
||||
<span>{TRAIT_LABELS[trait].highLabel}</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
value={Math.round(editDisposition[trait] * 100)}
|
||||
onChange={(e) => onEditChange(trait, parseInt(e.target.value) / 100)}
|
||||
className="w-full h-1.5 bg-muted rounded-lg appearance-none cursor-pointer accent-primary"
|
||||
min="1"
|
||||
max="5"
|
||||
step="1"
|
||||
value={editDisposition[trait]}
|
||||
onChange={(e) => onEditChange(trait, parseInt(e.target.value))}
|
||||
className="w-full h-2 bg-muted rounded-lg appearance-none cursor-pointer accent-primary"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-muted-foreground">{TRAIT_LABELS[trait].lowLabel}</span>
|
||||
<div className="flex-1 h-2 bg-muted rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-primary rounded-full transition-all"
|
||||
style={{ width: `${((data[trait] - 1) / 4) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">{TRAIT_LABELS[trait].highLabel}</span>
|
||||
</div>
|
||||
))}
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Influence Strength - always shown */}
|
||||
<div className="pt-3 border-t border-border">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-foreground">Disposition Influence</label>
|
||||
<p className="text-xs text-muted-foreground">How strongly traits affect responses</p>
|
||||
</div>
|
||||
<span className="text-sm font-bold text-primary">{Math.round(data.bias_strength * 100)}%</span>
|
||||
</div>
|
||||
{editMode && (
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
value={Math.round(editDisposition.bias_strength * 100)}
|
||||
onChange={(e) => onEditChange('bias_strength', parseInt(e.target.value) / 100)}
|
||||
className="w-full h-2 bg-muted rounded-lg appearance-none cursor-pointer accent-primary"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -209,15 +138,11 @@ export function BankProfileView() {
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
|
||||
// Edit state
|
||||
const [editName, setEditName] = useState('');
|
||||
const [editBackground, setEditBackground] = useState('');
|
||||
const [editDisposition, setEditDisposition] = useState<DispositionTraits>({
|
||||
openness: 0.5,
|
||||
conscientiousness: 0.5,
|
||||
extraversion: 0.5,
|
||||
agreeableness: 0.5,
|
||||
neuroticism: 0.5,
|
||||
bias_strength: 0.5
|
||||
skepticism: 3,
|
||||
literalism: 3,
|
||||
empathy: 3
|
||||
});
|
||||
|
||||
const loadData = async () => {
|
||||
@@ -235,7 +160,6 @@ export function BankProfileView() {
|
||||
setOperations((opsData as any)?.operations || []);
|
||||
|
||||
// Initialize edit state
|
||||
setEditName(profileData.name);
|
||||
setEditBackground(profileData.background);
|
||||
setEditDisposition(profileData.disposition);
|
||||
} catch (error) {
|
||||
@@ -252,7 +176,6 @@ export function BankProfileView() {
|
||||
setSaving(true);
|
||||
try {
|
||||
await client.updateBankProfile(currentBank, {
|
||||
name: editName,
|
||||
background: editBackground,
|
||||
disposition: editDisposition
|
||||
});
|
||||
@@ -268,7 +191,6 @@ export function BankProfileView() {
|
||||
|
||||
const handleCancel = () => {
|
||||
if (profile) {
|
||||
setEditName(profile.name);
|
||||
setEditBackground(profile.background);
|
||||
setEditDisposition(profile.disposition);
|
||||
}
|
||||
@@ -435,11 +357,11 @@ export function BankProfileView() {
|
||||
<Brain className="w-5 h-5 text-primary" />
|
||||
Disposition Profile
|
||||
</CardTitle>
|
||||
<CardDescription>Big Five disposition traits that influence responses</CardDescription>
|
||||
<CardDescription>Traits that shape how opinions are formed via Reflect</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{profile && (
|
||||
<DispositionRadarChart
|
||||
<DispositionEditor
|
||||
disposition={profile.disposition}
|
||||
editMode={editMode}
|
||||
editDisposition={editDisposition}
|
||||
@@ -449,39 +371,15 @@ export function BankProfileView() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Basic Info & Background */}
|
||||
{/* Background */}
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-lg">
|
||||
<User className="w-5 h-5 text-primary" />
|
||||
Identity
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-muted-foreground">Display Name</label>
|
||||
{editMode ? (
|
||||
<Input
|
||||
value={editName}
|
||||
onChange={(e) => setEditName(e.target.value)}
|
||||
placeholder="Enter a name for this bank"
|
||||
className="mt-1"
|
||||
/>
|
||||
) : (
|
||||
<p className="mt-1 text-lg font-medium text-foreground">{profile?.name || 'Unnamed'}</p>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-lg">
|
||||
<FileText className="w-5 h-5 text-primary" />
|
||||
Background
|
||||
</CardTitle>
|
||||
<CardDescription>Context that shapes how memories are interpreted</CardDescription>
|
||||
<CardDescription>Context used when forming opinions via Reflect</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{editMode ? (
|
||||
|
||||
@@ -89,128 +89,140 @@ export function DocumentsView() {
|
||||
|
||||
{/* Documents List and Detail Panel */}
|
||||
{documents.length > 0 && (
|
||||
<div className="flex gap-4">
|
||||
<>
|
||||
{/* Documents Table */}
|
||||
<div className={`transition-all ${selectedDocument ? 'w-1/2' : 'w-full'}`}>
|
||||
<div className="px-5 mb-4">
|
||||
<Input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Search documents (ID)..."
|
||||
className="max-w-2xl"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<div className="px-5 mb-4">
|
||||
<Input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Search documents (ID)..."
|
||||
className="max-w-2xl"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto px-5 pb-5">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Document ID</TableHead>
|
||||
<TableHead>Created</TableHead>
|
||||
<TableHead>Context</TableHead>
|
||||
<TableHead>Text Length</TableHead>
|
||||
<TableHead>Memory Units</TableHead>
|
||||
<TableHead>Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{documents.length > 0 ? (
|
||||
documents.map((doc) => (
|
||||
<TableRow
|
||||
key={doc.id}
|
||||
className={selectedDocument?.id === doc.id ? 'bg-accent' : ''}
|
||||
>
|
||||
<TableCell title={doc.id}>
|
||||
{doc.id.length > 30 ? doc.id.substring(0, 30) + '...' : doc.id}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{doc.created_at ? new Date(doc.created_at).toLocaleString() : 'N/A'}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{doc.retain_params?.context || '-'}
|
||||
</TableCell>
|
||||
<TableCell>{doc.text_length?.toLocaleString()} chars</TableCell>
|
||||
<TableCell>{doc.memory_unit_count}</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
onClick={() => viewDocumentText(doc.id)}
|
||||
size="sm"
|
||||
variant={selectedDocument?.id === doc.id ? 'default' : 'outline'}
|
||||
title="View original text"
|
||||
>
|
||||
View Text
|
||||
</Button>
|
||||
<div className="overflow-x-auto px-5 pb-5">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Document ID</TableHead>
|
||||
<TableHead>Created</TableHead>
|
||||
<TableHead>Context</TableHead>
|
||||
<TableHead>Text Length</TableHead>
|
||||
<TableHead>Memory Units</TableHead>
|
||||
<TableHead>Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{documents.length > 0 ? (
|
||||
documents.map((doc) => (
|
||||
<TableRow
|
||||
key={doc.id}
|
||||
className={`cursor-pointer hover:bg-muted/50 ${selectedDocument?.id === doc.id ? 'bg-primary/10' : ''}`}
|
||||
onClick={() => viewDocumentText(doc.id)}
|
||||
>
|
||||
<TableCell title={doc.id}>
|
||||
{doc.id.length > 30 ? doc.id.substring(0, 30) + '...' : doc.id}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{doc.created_at ? new Date(doc.created_at).toLocaleString() : 'N/A'}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{doc.retain_params?.context || '-'}
|
||||
</TableCell>
|
||||
<TableCell>{doc.text_length?.toLocaleString()} chars</TableCell>
|
||||
<TableCell>{doc.memory_unit_count}</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
viewDocumentText(doc.id);
|
||||
}}
|
||||
size="sm"
|
||||
variant={selectedDocument?.id === doc.id ? 'default' : 'outline'}
|
||||
title="View original text"
|
||||
>
|
||||
View Text
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="text-center">
|
||||
Click "Load Documents" to view data
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="text-center">
|
||||
Click "Load Documents" to view data
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Document Detail Panel */}
|
||||
{selectedDocument && (
|
||||
<div className="w-1/2 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>
|
||||
<h3 className="text-lg font-bold text-card-foreground">Document Details</h3>
|
||||
<p className="text-sm text-muted-foreground">View the original document text and metadata</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setSelectedDocument(null)}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{loadingDocument ? (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<div className="text-center">
|
||||
<div className="text-4xl mb-2">⏳</div>
|
||||
<div className="text-sm text-muted-foreground">Loading document...</div>
|
||||
{/* Document Detail Panel - Fixed on Right */}
|
||||
{selectedDocument && (
|
||||
<div className="fixed right-0 top-0 h-screen w-[420px] bg-card border-l-2 border-primary shadow-2xl z-50 overflow-y-auto animate-in slide-in-from-right duration-300 ease-out">
|
||||
<div className="p-5">
|
||||
{/* Header with close button */}
|
||||
<div className="flex justify-between items-center mb-6 pb-4 border-b border-border">
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-foreground">Document Details</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">Original document text and metadata</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setSelectedDocument(null)}
|
||||
className="h-9 px-3 gap-2"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
<div className="p-3 bg-muted rounded-lg">
|
||||
<div className="text-xs font-bold text-muted-foreground uppercase mb-1">Document ID</div>
|
||||
|
||||
{loadingDocument ? (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<div className="text-center">
|
||||
<div className="text-4xl mb-2">⏳</div>
|
||||
<div className="text-sm text-muted-foreground">Loading document...</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-5">
|
||||
{/* Document ID */}
|
||||
<div className="p-4 bg-muted/50 rounded-lg">
|
||||
<div className="text-xs font-bold text-muted-foreground uppercase mb-2">Document ID</div>
|
||||
<div className="text-sm font-mono break-all">{selectedDocument.id}</div>
|
||||
</div>
|
||||
|
||||
{/* Created & Memory Units */}
|
||||
{selectedDocument.created_at && (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="p-3 bg-muted rounded-lg">
|
||||
<div className="text-xs font-bold text-muted-foreground uppercase mb-1">Created</div>
|
||||
<div className="text-sm">{new Date(selectedDocument.created_at).toLocaleString()}</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="p-4 bg-muted/50 rounded-lg">
|
||||
<div className="text-xs font-bold text-muted-foreground uppercase mb-2">Created</div>
|
||||
<div className="text-sm font-medium">{new Date(selectedDocument.created_at).toLocaleString()}</div>
|
||||
</div>
|
||||
<div className="p-3 bg-muted rounded-lg">
|
||||
<div className="text-xs font-bold text-muted-foreground uppercase mb-1">Memory Units</div>
|
||||
<div className="text-sm">{selectedDocument.memory_unit_count}</div>
|
||||
<div className="p-4 bg-muted/50 rounded-lg">
|
||||
<div className="text-xs font-bold text-muted-foreground uppercase mb-2">Memory Units</div>
|
||||
<div className="text-sm font-medium">{selectedDocument.memory_unit_count}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Text Length */}
|
||||
{selectedDocument.original_text && (
|
||||
<div className="p-3 bg-muted rounded-lg">
|
||||
<div className="text-xs font-bold text-muted-foreground uppercase mb-1">Text Length</div>
|
||||
<div className="text-sm">{selectedDocument.original_text.length.toLocaleString()} characters</div>
|
||||
<div className="p-4 bg-muted/50 rounded-lg">
|
||||
<div className="text-xs font-bold text-muted-foreground uppercase mb-2">Text Length</div>
|
||||
<div className="text-sm font-medium">{selectedDocument.original_text.length.toLocaleString()} characters</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Retain Parameters */}
|
||||
{selectedDocument.retain_params && (
|
||||
<div className="p-3 bg-muted rounded-lg">
|
||||
<div className="text-xs font-bold text-muted-foreground uppercase mb-1">Retain Parameters</div>
|
||||
<div className="text-sm space-y-1">
|
||||
<div className="p-4 bg-muted/50 rounded-lg">
|
||||
<div className="text-xs font-bold text-muted-foreground uppercase mb-2">Retain Parameters</div>
|
||||
<div className="text-sm space-y-2">
|
||||
{selectedDocument.retain_params.context && (
|
||||
<div><span className="font-semibold">Context:</span> {selectedDocument.retain_params.context}</div>
|
||||
)}
|
||||
@@ -220,28 +232,28 @@ export function DocumentsView() {
|
||||
{selectedDocument.retain_params.metadata && (
|
||||
<div className="mt-2">
|
||||
<span className="font-semibold">Metadata:</span>
|
||||
<pre className="mt-1 text-xs">{JSON.stringify(selectedDocument.retain_params.metadata, null, 2)}</pre>
|
||||
<pre className="mt-1 text-xs bg-background p-2 rounded">{JSON.stringify(selectedDocument.retain_params.metadata, null, 2)}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selectedDocument.original_text && (
|
||||
<div>
|
||||
<div className="text-sm font-bold text-foreground mb-2">Original Text</div>
|
||||
<div className="p-4 bg-muted rounded-lg border border-border">
|
||||
<pre className="text-sm whitespace-pre-wrap font-mono">{selectedDocument.original_text}</pre>
|
||||
{/* Original Text */}
|
||||
{selectedDocument.original_text && (
|
||||
<div>
|
||||
<div className="text-xs font-bold text-muted-foreground uppercase mb-2">Original Text</div>
|
||||
<div className="p-4 bg-muted/50 rounded-lg border border-border max-h-[400px] overflow-y-auto">
|
||||
<pre className="text-sm whitespace-pre-wrap font-mono leading-relaxed">{selectedDocument.original_text}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -22,12 +22,12 @@ export function Sidebar({ currentTab, onTabChange }: SidebarProps) {
|
||||
}
|
||||
|
||||
const navItems = [
|
||||
{ id: 'profile' as NavItem, label: 'Memory Bank', icon: Box },
|
||||
{ id: 'recall' as NavItem, label: 'Recall', icon: Search },
|
||||
{ id: 'reflect' as NavItem, label: 'Reflect', icon: Sparkles },
|
||||
{ id: 'data' as NavItem, label: 'Memories', icon: Database },
|
||||
{ id: 'documents' as NavItem, label: 'Documents', icon: FileText },
|
||||
{ id: 'entities' as NavItem, label: 'Entities', icon: Users },
|
||||
{ id: 'profile' as NavItem, label: 'Memory Bank', icon: Box },
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -180,12 +180,9 @@ export class ControlPlaneClient {
|
||||
bank_id: string;
|
||||
name: string;
|
||||
disposition: {
|
||||
openness: number;
|
||||
conscientiousness: number;
|
||||
extraversion: number;
|
||||
agreeableness: number;
|
||||
neuroticism: number;
|
||||
bias_strength: number;
|
||||
skepticism: number;
|
||||
literalism: number;
|
||||
empathy: number;
|
||||
};
|
||||
background: string;
|
||||
}>(`/api/profile/${bankId}`);
|
||||
@@ -197,12 +194,9 @@ export class ControlPlaneClient {
|
||||
async updateBankProfile(bankId: string, profile: {
|
||||
name?: string;
|
||||
disposition?: {
|
||||
openness: number;
|
||||
conscientiousness: number;
|
||||
extraversion: number;
|
||||
agreeableness: number;
|
||||
neuroticism: number;
|
||||
bias_strength: number;
|
||||
skepticism: number;
|
||||
literalism: number;
|
||||
empathy: number;
|
||||
};
|
||||
background?: string;
|
||||
}) {
|
||||
|
||||
@@ -5,7 +5,7 @@ description: "Recall memory using semantic similarity and spreading activation."
|
||||
sidebar_label: "Recall memory"
|
||||
hide_title: true
|
||||
hide_table_of_contents: true
|
||||
api: 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
|
||||
api: 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
|
||||
sidebar_class_name: "post api-method"
|
||||
info_path: docs/api-reference/endpoints/hindsight-http-api
|
||||
custom_edit_url: null
|
||||
@@ -61,7 +61,7 @@ Recall memory using semantic similarity and spreading activation.
|
||||
|
||||
<RequestSchema
|
||||
title={"Body"}
|
||||
body={{"required":true,"content":{"application/json":{"schema":{"properties":{"query":{"type":"string","title":"Query"},"types":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Types","description":"List of fact types to recall (defaults to all if not specified)"},"budget":{"default":"mid","type":"string","enum":["low","mid","high"],"title":"Budget","description":"Budget levels for recall/reflect operations."},"max_tokens":{"type":"integer","title":"Max Tokens","default":4096},"trace":{"type":"boolean","title":"Trace","default":false},"query_timestamp":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Query Timestamp","description":"ISO format date string (e.g., '2023-05-30T23:40:00')"},"filters":{"anyOf":[{"items":{"properties":{"key":{"type":"string","title":"Key","description":"Metadata key to filter on"},"value":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Value","description":"Value to match. If None with match_unset=True, matches any record where key is not set."},"match_unset":{"type":"boolean","title":"Match Unset","description":"If True, also match records where this metadata key is not set","default":true}},"type":"object","required":["key"],"title":"MetadataFilter","description":"Filter for metadata fields. Matches records where (key=value) OR (key not set) when match_unset=True.","example":{"key":"source","match_unset":true,"value":"slack"}},"type":"array"},{"type":"null"}],"title":"Filters","description":"Filter by metadata. Multiple filters are ANDed together."},"include":{"description":"Options for including additional data (entities are included by default)","properties":{"entities":{"anyOf":[{"properties":{"max_tokens":{"type":"integer","title":"Max Tokens","description":"Maximum tokens for entity observations","default":500}},"type":"object","title":"EntityIncludeOptions","description":"Options for including entity observations in recall results."},{"type":"null"}],"description":"Include entity observations. Set to null to disable entity inclusion.","default":{"max_tokens":500}},"chunks":{"anyOf":[{"properties":{"max_tokens":{"type":"integer","title":"Max Tokens","description":"Maximum tokens for chunks (chunks may be truncated)","default":8192}},"type":"object","title":"ChunkIncludeOptions","description":"Options for including chunks in recall results."},{"type":"null"}],"description":"Include raw chunks. Set to {} to enable, null to disable (default: disabled)."}},"type":"object","title":"IncludeOptions"}},"type":"object","required":["query"],"title":"RecallRequest","description":"Request model for recall endpoint.","example":{"budget":"mid","filters":[{"key":"source","match_unset":true,"value":"slack"}],"include":{"entities":{"max_tokens":500}},"max_tokens":4096,"query":"What did Alice say about machine learning?","query_timestamp":"2023-05-30T23:40:00","trace":true,"types":["world","experience"]}}}}}}
|
||||
body={{"required":true,"content":{"application/json":{"schema":{"properties":{"query":{"type":"string","title":"Query"},"types":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Types","description":"List of fact types to recall (defaults to all if not specified)"},"budget":{"default":"mid","type":"string","enum":["low","mid","high"],"title":"Budget","description":"Budget levels for recall/reflect operations."},"max_tokens":{"type":"integer","title":"Max Tokens","default":4096},"trace":{"type":"boolean","title":"Trace","default":false},"query_timestamp":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Query Timestamp","description":"ISO format date string (e.g., '2023-05-30T23:40:00')"},"include":{"description":"Options for including additional data (entities are included by default)","properties":{"entities":{"anyOf":[{"properties":{"max_tokens":{"type":"integer","title":"Max Tokens","description":"Maximum tokens for entity observations","default":500}},"type":"object","title":"EntityIncludeOptions","description":"Options for including entity observations in recall results."},{"type":"null"}],"description":"Include entity observations. Set to null to disable entity inclusion.","default":{"max_tokens":500}},"chunks":{"anyOf":[{"properties":{"max_tokens":{"type":"integer","title":"Max Tokens","description":"Maximum tokens for chunks (chunks may be truncated)","default":8192}},"type":"object","title":"ChunkIncludeOptions","description":"Options for including chunks in recall results."},{"type":"null"}],"description":"Include raw chunks. Set to {} to enable, null to disable (default: disabled)."}},"type":"object","title":"IncludeOptions"}},"type":"object","required":["query"],"title":"RecallRequest","description":"Request model for recall endpoint.","example":{"budget":"mid","include":{"entities":{"max_tokens":500}},"max_tokens":4096,"query":"What did Alice say about machine learning?","query_timestamp":"2023-05-30T23:40:00","trace":true,"types":["world","experience"]}}}}}}
|
||||
>
|
||||
|
||||
</RequestSchema>
|
||||
|
||||
@@ -5,7 +5,7 @@ description: "Reflect and formulate an answer using bank identity, world facts,
|
||||
sidebar_label: "Reflect and generate answer"
|
||||
hide_title: true
|
||||
hide_table_of_contents: true
|
||||
api: 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
|
||||
api: eJztWE1vGzcQ/SsDXmoD69WHZafdS+GkLmogqVPbaYEqRkAvR1rGFLklubIFQf+9GJIrraTYCYweWqA+CBbJGc68efNIaskEutLK2kujWcGucKKw9MC1gImxs0Zxj8A1cO0e0ELjpJ7CHdf3IAVqL/0igwdjlYAJL73LgqWppZZGu/yj/qgBAG4q6QC1qI3Uvohjgxyu0FuJc3SAjzVaibpEOCiNnqN1nEJywR/OUXt3GO2GXbvO1mBR4ZxrD96ArxD+atAuos3x9l7SeUqjDRMOKKHvHNRoXY2ll3Nsdxvl8MGhg7dv35HfDiZQGu3x0TdcJXSixUkO54/ehpAoeOeNRfp3ARofNpuSKxTR5jTE11jtoFZcaiDHyWsWkokpNg5FhLjrimXM1GgDYBeCFczGKrKM1dzyGXq0jhXjJdN8hqxglO4nKVjGJBW95r5iGbP4VyMtClZ422DGXFnhjLNiyfyiJjPnrdRTljEvvaKB18SDC8FWq9tojs6/NmJBNrveAlra0xSvayXLEG7vsyPeLTub1ZaS8RIdfQtFfC6G38KCVcbuGjHF4F/ghDfKs4Ip80BLd0xRNzNWjNPsLABRyWnFbjupRW/ZTn/EYVA4RxVKCBZLrlQvQQ7rQricgkocCVnrxeUkVGE7nlW2HtGNUmzVieJNMl9RpUrVCIz5dUO6rH3LJ4iLiNtcCEnjXIHgnsOBkI7fKRRwt4AE0CERZAvtwLLtYLcWrNZgmrvPkWFtqD+T6UUMMoW0h96XQ43UPrjjDsUnow9BamjhtOga5QOW+yhtO097J3e+4j40ThIu6SBsAEbncI1BJJYr+kRNuGRAbul7AgoOEkpFOyIOc7Z6BoGknTsYfMli0xvjRPDbPTdXsZ32MEzjMDMCVWJgxKrV15wo/shntQpsafsi0X3NSBZEWcZqcEIauS0rqHmNFoyGswtAX8nSsS32tRyhxFJ3sj8IbWFgYRrwldT3wO9M44FbLyeylFyB1B6VklPS+B/Ziv4ICFcb7SL5hv3+Pruvm7JE5yaNgqu0mL1YTNpWfEpLblKvtVSktdLjzO27kuKlPU16mX1zKHH+ZTvd0Pg/KkKmLBtrUXxyntsXO7xMXuA6eOn6Rf1iXNdez7X4ateFhPabjkRsr+POgqSEs5ekKfK7Ze5Os226q0KufFVyi6QfZeMcecsCcdhgeIyjk9NXR/j9D3dHg6E4PuKjk9Oj0fD0dDAavBr1+322Cwsb9oejo/7gaHByM+gXx/2i3/+T7RflqXUpsLMLavo2m02Ym4MyXKq6EHJr+WLr3CfrSx2gSmft+PaFmHfaelfp4kRH6iL2TwndumvHyzXML8g7S9ajk9ON9UVbRRQkjB3VcaC48/CAeL9xtbnPRo5GJ6/TCQSzBTRaoHWeazoDM4jRcfCWa0dXQ063UPBYVtooM13ked6q5mg43BfK37mSIgQE59Ya+3KVFOi5VM9onzLl1uw3dCup/xQtW90+zaq3JgZIgjBz0+fU8R06x6fYFcinlgYwIErhVwhKecWt07oOWTfwRnSfTuOnCN9zN4Vfbm7e7zmMtZ2hrwyxrzYuXuB9xQrWmw96qdV6dHt3vWW6xK96m9s+VfZqcw0//7dfAsjlxITqtchILZycVh4IIzh7f7EnC+1ECHm9PiVF+l2s3znvcGbsAq4XzuOMtlOyRJKazZKzmpcVwjAnvW2sIuX2vnZFr/fw8JDzMJ0bO+0lW9d7e/Hm/Nfr86Nh3s8rP1PkmB6sMbxB3s/7NEQFnHHd2av7uJ6ipodCez/dzXK5ad7/3+T/0Td56n4y6wUPxIpAsWVq6zGbDzpnaBYe5mRabF7obXffZqwiSSjGbLmko+6DVasVDaf+G99mbM6tpKdKEOP22cKKCVcOn2HYwVVSwUN4Ku5W7DRJ3Zyrhr6xjN3jovODQpD3CrlAG0KIs2/iRkdBhDfWe2cSnRfR4qwssfbPrr3taOX7y+sbQi/99ED3BfoVhJPU0WeI1KQHGf02QWNLprieNnSMFCz6pL+/AUOuhHE=
|
||||
sidebar_class_name: "post api-method"
|
||||
info_path: docs/api-reference/endpoints/hindsight-http-api
|
||||
custom_edit_url: null
|
||||
@@ -62,7 +62,7 @@ Reflect and formulate an answer using bank identity, world facts, and opinions.
|
||||
|
||||
<RequestSchema
|
||||
title={"Body"}
|
||||
body={{"required":true,"content":{"application/json":{"schema":{"properties":{"query":{"type":"string","title":"Query"},"budget":{"default":"low","type":"string","enum":["low","mid","high"],"title":"Budget","description":"Budget levels for recall/reflect operations."},"context":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Context"},"filters":{"anyOf":[{"items":{"properties":{"key":{"type":"string","title":"Key","description":"Metadata key to filter on"},"value":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Value","description":"Value to match. If None with match_unset=True, matches any record where key is not set."},"match_unset":{"type":"boolean","title":"Match Unset","description":"If True, also match records where this metadata key is not set","default":true}},"type":"object","required":["key"],"title":"MetadataFilter","description":"Filter for metadata fields. Matches records where (key=value) OR (key not set) when match_unset=True.","example":{"key":"source","match_unset":true,"value":"slack"}},"type":"array"},{"type":"null"}],"title":"Filters","description":"Filter by metadata. Multiple filters are ANDed together."},"include":{"description":"Options for including additional data (disabled by default)","properties":{"facts":{"anyOf":[{"properties":{},"type":"object","title":"FactsIncludeOptions","description":"Options for including facts (based_on) in reflect results."},{"type":"null"}],"description":"Include facts that the answer is based on. Set to {} to enable, null to disable (default: disabled)."}},"type":"object","title":"ReflectIncludeOptions"}},"type":"object","required":["query"],"title":"ReflectRequest","description":"Request model for reflect endpoint.","example":{"budget":"low","context":"This is for a research paper on AI ethics","filters":[{"key":"source","match_unset":true,"value":"slack"}],"include":{"facts":{}},"query":"What do you think about artificial intelligence?"}}}}}}
|
||||
body={{"required":true,"content":{"application/json":{"schema":{"properties":{"query":{"type":"string","title":"Query"},"budget":{"default":"low","type":"string","enum":["low","mid","high"],"title":"Budget","description":"Budget levels for recall/reflect operations."},"context":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Context"},"include":{"description":"Options for including additional data (disabled by default)","properties":{"facts":{"anyOf":[{"properties":{},"type":"object","title":"FactsIncludeOptions","description":"Options for including facts (based_on) in reflect results."},{"type":"null"}],"description":"Include facts that the answer is based on. Set to {} to enable, null to disable (default: disabled)."}},"type":"object","title":"ReflectIncludeOptions"}},"type":"object","required":["query"],"title":"ReflectRequest","description":"Request model for reflect endpoint.","example":{"budget":"low","context":"This is for a research paper on AI ethics","include":{"facts":{}},"query":"What do you think about artificial intelligence?"}}}}}}
|
||||
>
|
||||
|
||||
</RequestSchema>
|
||||
|
||||
@@ -4,4 +4,18 @@ sidebar_position: 1
|
||||
|
||||
# Cookbook
|
||||
|
||||
Coming soon.
|
||||
Practical patterns and recipes for building with Hindsight.
|
||||
|
||||
## Use Cases
|
||||
|
||||
### [Per-User Memory](/cookbook/per-user-memory)
|
||||
|
||||
The simplest pattern: give your agent persistent memory for each user. The agent remembers past conversations, preferences, and context across sessions.
|
||||
|
||||
**Use when:** Building chatbots, personal assistants, or any 1:1 user-to-agent interaction.
|
||||
|
||||
### [Support Agent with Shared Knowledge](/cookbook/support-agent-with-shared-knowledge)
|
||||
|
||||
Build a support agent that combines per-user memory with shared product documentation. Users get personalized support while you index docs only once.
|
||||
|
||||
**Use when:** Building multi-tenant support agents, RAG + memory applications, or any scenario needing user isolation with shared reference data.
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# Per-User Memory
|
||||
|
||||
The simplest pattern: give your agent persistent memory for each user. The agent remembers past conversations, user preferences, and context across sessions.
|
||||
|
||||
## The Problem
|
||||
|
||||
Without memory, every conversation starts from scratch:
|
||||
|
||||
```
|
||||
Session 1: "I prefer dark mode and use Python"
|
||||
Session 2: "What's my preferred language?" → Agent doesn't know
|
||||
```
|
||||
|
||||
## The Solution: One Bank Per User
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ User A Bank │ │ User B Bank │ │ User C Bank │
|
||||
│ │ │ │ │ │
|
||||
│ - Conversations│ │ - Conversations│ │ - Conversations│
|
||||
│ - Preferences │ │ - Preferences │ │ - Preferences │
|
||||
│ - Context │ │ - Context │ │ - Context │
|
||||
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||
│ │ │
|
||||
100% isolated 100% isolated 100% isolated
|
||||
```
|
||||
|
||||
Each user gets their own memory bank. Complete isolation, simple mental model.
|
||||
|
||||
## Implementation
|
||||
|
||||
### 1. Create a Bank When User Signs Up
|
||||
|
||||
```python
|
||||
from hindsight import HindsightClient
|
||||
|
||||
client = HindsightClient()
|
||||
|
||||
def on_user_signup(user_id: str):
|
||||
client.create_bank(
|
||||
bank_id=f"user-{user_id}",
|
||||
name=f"Memory for {user_id}"
|
||||
)
|
||||
```
|
||||
|
||||
### 2. Save Conversations After Each Session
|
||||
|
||||
```python
|
||||
async def save_conversation(user_id: str, messages: list):
|
||||
await client.retain(
|
||||
bank_id=f"user-{user_id}",
|
||||
content=messages # [{"role": "user", "content": "..."}, ...]
|
||||
)
|
||||
```
|
||||
|
||||
### 3. Recall Context Before Responding
|
||||
|
||||
```python
|
||||
async def get_context(user_id: str, query: str):
|
||||
result = await client.recall(
|
||||
bank_id=f"user-{user_id}",
|
||||
query=query
|
||||
)
|
||||
return result.results
|
||||
```
|
||||
|
||||
### 4. Complete Agent Loop
|
||||
|
||||
```python
|
||||
async def handle_message(user_id: str, user_message: str):
|
||||
# 1. Recall relevant context
|
||||
context = await client.recall(
|
||||
bank_id=f"user-{user_id}",
|
||||
query=user_message
|
||||
)
|
||||
|
||||
# 2. Build prompt with memory
|
||||
prompt = f"""You are a helpful assistant with memory of past conversations.
|
||||
|
||||
## What you remember about this user
|
||||
{format_results(context.results)}
|
||||
|
||||
## Current message
|
||||
{user_message}
|
||||
"""
|
||||
|
||||
# 3. Generate response
|
||||
response = await llm.complete(prompt)
|
||||
|
||||
# 4. Save the conversation
|
||||
await client.retain(
|
||||
bank_id=f"user-{user_id}",
|
||||
content=[
|
||||
{"role": "user", "content": user_message},
|
||||
{"role": "assistant", "content": response}
|
||||
]
|
||||
)
|
||||
|
||||
return response
|
||||
```
|
||||
|
||||
## What Gets Remembered
|
||||
|
||||
Hindsight automatically extracts and connects:
|
||||
|
||||
- **Facts**: "User prefers Python", "User is building a CLI tool"
|
||||
- **Entities**: People, projects, technologies mentioned
|
||||
- **Relationships**: How entities relate to each other
|
||||
- **Temporal context**: When things happened
|
||||
|
||||
You don't need to manually extract or structure this - just retain the conversations.
|
||||
|
||||
## When to Use This Pattern
|
||||
|
||||
**Good fit:**
|
||||
- Chatbots and assistants
|
||||
- Personal AI companions
|
||||
- Any 1:1 user-to-agent interaction
|
||||
|
||||
**Consider adding shared knowledge if:**
|
||||
- You have product docs or FAQs to reference
|
||||
- Multiple users need access to the same information
|
||||
- See [Support Agent with Shared Knowledge](./support-agent-with-shared-knowledge)
|
||||
@@ -0,0 +1,258 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
# Support Agent with Shared Knowledge
|
||||
|
||||
This pattern shows how to build a support agent that combines **per-user memory** with **shared product knowledge** (RAG), giving users personalized support while leveraging a single source of truth for documentation.
|
||||
|
||||
## The Problem
|
||||
|
||||
You're building a support agent that needs to:
|
||||
- Remember each user's history, preferences, and past issues
|
||||
- Access shared product documentation
|
||||
- Keep user data completely isolated from other users
|
||||
|
||||
A naive approach would index product docs into each user's memory bank, but this is expensive and wasteful (N copies for N users).
|
||||
|
||||
## The Solution: Multi-Bank Architecture
|
||||
|
||||
Create separate memory banks for different concerns:
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ User A Bank │ │ User B Bank │ │ Shared Docs │
|
||||
│ │ │ │ │ Bank │
|
||||
│ - Conversations│ │ - Conversations│ │ │
|
||||
│ - Preferences │ │ - Preferences │ │ - Product docs │
|
||||
│ - Past issues │ │ - Past issues │ │ - FAQs │
|
||||
│ - Solutions │ │ - Solutions │ │ - Guides │
|
||||
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
|
||||
│ │ │
|
||||
└───────────────────────┴───────────────────────┘
|
||||
│
|
||||
Agent queries
|
||||
multiple banks
|
||||
```
|
||||
|
||||
**Key benefits:**
|
||||
- Product docs indexed once, shared by all users
|
||||
- User memory is 100% isolated
|
||||
- Simple mental model, no complex filtering
|
||||
|
||||
## Implementation
|
||||
|
||||
### 1. Set Up Memory Banks
|
||||
|
||||
Create three types of banks:
|
||||
|
||||
```python
|
||||
from hindsight import HindsightClient
|
||||
|
||||
client = HindsightClient()
|
||||
|
||||
# Shared knowledge bank (created once)
|
||||
shared_bank = client.create_bank(
|
||||
bank_id="product-docs",
|
||||
name="Product Documentation"
|
||||
)
|
||||
|
||||
# Per-user banks (created when user signs up)
|
||||
def create_user_bank(user_id: str):
|
||||
return client.create_bank(
|
||||
bank_id=f"user-{user_id}",
|
||||
name=f"Memory for {user_id}"
|
||||
)
|
||||
```
|
||||
|
||||
### 2. Index Product Documentation
|
||||
|
||||
Index your product docs into the shared bank (do this once, or on doc updates):
|
||||
|
||||
```python
|
||||
# Index product documentation
|
||||
client.retain(
|
||||
bank_id="product-docs",
|
||||
content=[
|
||||
{
|
||||
"role": "document",
|
||||
"content": "# Pricing Tiers\n\nBasic: $10/mo...",
|
||||
"metadata": {"source": "pricing.md"}
|
||||
},
|
||||
{
|
||||
"role": "document",
|
||||
"content": "# Getting Started\n\nTo set up...",
|
||||
"metadata": {"source": "quickstart.md"}
|
||||
}
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
### 3. Store User Conversations
|
||||
|
||||
After each support interaction, retain it in the user's bank:
|
||||
|
||||
```python
|
||||
def save_conversation(user_id: str, messages: list):
|
||||
client.retain(
|
||||
bank_id=f"user-{user_id}",
|
||||
content=messages # [{"role": "user", "content": "..."}, ...]
|
||||
)
|
||||
```
|
||||
|
||||
### 4. Query Multiple Banks at Support Time
|
||||
|
||||
When handling a user query, retrieve context from both banks:
|
||||
|
||||
```python
|
||||
async def get_support_context(user_id: str, query: str):
|
||||
# Get user's personal context
|
||||
user_context = await client.recall(
|
||||
bank_id=f"user-{user_id}",
|
||||
query=query
|
||||
)
|
||||
|
||||
# Get relevant product documentation
|
||||
docs_context = await client.recall(
|
||||
bank_id="product-docs",
|
||||
query=query
|
||||
)
|
||||
|
||||
return {
|
||||
"user_history": user_context.results,
|
||||
"documentation": docs_context.results
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Build the Agent Prompt
|
||||
|
||||
Combine both contexts in your agent's prompt:
|
||||
|
||||
```python
|
||||
def build_prompt(query: str, context: dict) -> str:
|
||||
return f"""You are a helpful support agent.
|
||||
|
||||
## User's History
|
||||
{format_results(context["user_history"])}
|
||||
|
||||
## Product Documentation
|
||||
{format_results(context["documentation"])}
|
||||
|
||||
## Current Question
|
||||
{query}
|
||||
|
||||
Use the user's history to personalize your response and the documentation
|
||||
for accurate product information. If you find a solution, remember it for
|
||||
future reference.
|
||||
"""
|
||||
```
|
||||
|
||||
## Promoting Learnings to Shared Knowledge
|
||||
|
||||
When the agent discovers a solution that's not in the docs, you can optionally promote it to a "learnings" bank:
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ User A Bank │ │ Shared Docs │ │ Learnings │
|
||||
│ │ │ Bank │ │ Bank │
|
||||
│ - Conversations│ │ │ │ │
|
||||
│ - Preferences │ │ - Product docs │ │ - Verified │
|
||||
│ - Past issues │ │ - FAQs │ │ solutions │
|
||||
│ - Solutions │ │ - Guides │ │ - Workarounds │
|
||||
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
|
||||
│ │ │
|
||||
└───────────────────────┴───────────────────────┘
|
||||
│
|
||||
Agent queries
|
||||
all three banks
|
||||
```
|
||||
|
||||
```python
|
||||
# Optional: Create a curated learnings bank
|
||||
learnings_bank = client.create_bank(
|
||||
bank_id="support-learnings",
|
||||
name="Curated Support Learnings"
|
||||
)
|
||||
|
||||
# After a successful resolution
|
||||
def promote_learning(insight: str):
|
||||
client.retain(
|
||||
bank_id="support-learnings",
|
||||
content=[{
|
||||
"role": "system",
|
||||
"content": insight,
|
||||
"metadata": {"type": "verified_solution"}
|
||||
}]
|
||||
)
|
||||
```
|
||||
|
||||
Then query three banks: user + docs + learnings.
|
||||
|
||||
## Complete Example
|
||||
|
||||
```python
|
||||
from hindsight import HindsightClient
|
||||
|
||||
client = HindsightClient()
|
||||
|
||||
async def handle_support_request(user_id: str, query: str):
|
||||
# 1. Recall from user's memory
|
||||
user_recall = await client.recall(
|
||||
bank_id=f"user-{user_id}",
|
||||
query=query
|
||||
)
|
||||
|
||||
# 2. Recall from shared docs
|
||||
docs_recall = await client.recall(
|
||||
bank_id="product-docs",
|
||||
query=query
|
||||
)
|
||||
|
||||
# 3. Recall from learnings (optional)
|
||||
learnings_recall = await client.recall(
|
||||
bank_id="support-learnings",
|
||||
query=query
|
||||
)
|
||||
|
||||
# 4. Build context for LLM
|
||||
context = f"""
|
||||
User History:
|
||||
{format_results(user_recall.results)}
|
||||
|
||||
Product Docs:
|
||||
{format_results(docs_recall.results)}
|
||||
|
||||
Known Solutions:
|
||||
{format_results(learnings_recall.results)}
|
||||
"""
|
||||
|
||||
# 5. Generate response with your LLM
|
||||
response = await llm.complete(
|
||||
system="You are a support agent...",
|
||||
context=context,
|
||||
query=query
|
||||
)
|
||||
|
||||
# 6. Save the conversation to user's memory
|
||||
await client.retain(
|
||||
bank_id=f"user-{user_id}",
|
||||
content=[
|
||||
{"role": "user", "content": query},
|
||||
{"role": "assistant", "content": response}
|
||||
]
|
||||
)
|
||||
|
||||
return response
|
||||
```
|
||||
|
||||
## When to Use This Pattern
|
||||
|
||||
**Good fit:**
|
||||
- Support agents with shared documentation
|
||||
- Multi-tenant applications with shared reference data
|
||||
- Any scenario needing user isolation + shared knowledge
|
||||
|
||||
**Consider alternatives if:**
|
||||
- You need cross-user learning (users benefiting from other users' solutions)
|
||||
- Entity relationships must span across users and docs
|
||||
|
||||
@@ -2,229 +2,109 @@
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
# RAG vs Hindsight: Why Your AI Needs Real Memory
|
||||
# RAG vs Memory
|
||||
|
||||
Traditional RAG (Retrieval-Augmented Generation) finds documents similar to your query. Hindsight gives your AI actual memory—with temporal reasoning, entity understanding, and evolving beliefs.
|
||||
Traditional RAG (Retrieval-Augmented Generation) retrieves documents similar to a query. Hindsight provides structured memory with temporal reasoning, entity understanding, and belief formation.
|
||||
|
||||
Here's what that difference looks like in practice.
|
||||
|
||||
---
|
||||
|
||||
## Example 1: Multi-Hop Reasoning
|
||||
|
||||
**Scenario:** Your AI assistant has learned these facts over time:
|
||||
- "Alice is the tech lead on Project Atlas"
|
||||
- "Project Atlas uses Kubernetes for deployment"
|
||||
- "The Kubernetes cluster had an outage last Tuesday"
|
||||
|
||||
**User asks:** "Was Alice affected by any recent issues?"
|
||||
|
||||
### RAG Response
|
||||
> I found information about Alice being a tech lead, but I don't have specific information about recent issues affecting her.
|
||||
|
||||
**Why it fails:** RAG only finds facts semantically similar to "Alice" + "recent issues". It can't connect Alice → Project Atlas → Kubernetes → outage because those facts exist in different semantic spaces.
|
||||
|
||||
### Hindsight Response
|
||||
> Yes, Alice was likely affected. She's the tech lead on Project Atlas, which uses Kubernetes for deployment. The Kubernetes cluster had an outage last Tuesday, which would have impacted her team's deployments.
|
||||
|
||||
**Why it works:** Hindsight's knowledge graph links Alice → Project Atlas → Kubernetes → outage through entity relationships. Graph traversal discovers the connection even though no single fact mentions both Alice and the outage.
|
||||
|
||||
---
|
||||
|
||||
## Example 2: Temporal Reasoning
|
||||
|
||||
**Scenario:** An AI assistant has accumulated months of conversation history:
|
||||
- March: "Alice started the migration to microservices"
|
||||
- April: "Alice completed the authentication service"
|
||||
- May: "Alice presented the architecture review"
|
||||
- October: "Alice is now focusing on performance optimization"
|
||||
|
||||
**User asks:** "What did Alice accomplish last spring?"
|
||||
|
||||
### RAG Response
|
||||
> Alice has worked on microservices migration, authentication service, architecture review, and performance optimization.
|
||||
|
||||
**Why it fails:** RAG treats "last spring" as keywords, returning ALL mentions of Alice regardless of when they occurred. No temporal filtering.
|
||||
|
||||
### Hindsight Response
|
||||
> Last spring (March-May), Alice started the migration to microservices, completed the authentication service, and presented the architecture review. She's since moved on to performance optimization.
|
||||
|
||||
**Why it works:** Hindsight parses "last spring" into a date range (March-May), then retrieves only facts with timestamps in that window. The temporal-graph strategy ensures historical queries return chronologically accurate results.
|
||||
|
||||
---
|
||||
|
||||
## Example 3: Consistent Personality
|
||||
|
||||
**Scenario:** Two AI assistants have the same facts about remote work:
|
||||
- "Remote work saves 1 hour of commute daily"
|
||||
- "65% of employees report higher productivity"
|
||||
- "Some teams report communication challenges"
|
||||
- "Office presence improves spontaneous collaboration"
|
||||
|
||||
**User asks:** "Should our company adopt remote work?"
|
||||
|
||||
### RAG Response (varies each time)
|
||||
> Remote work has benefits like saved commute time and higher productivity, but also challenges like communication issues. Consider your team's needs.
|
||||
|
||||
**Why it fails:** Generic, fence-sitting response. No consistent perspective. Ask again tomorrow, get a slightly different answer.
|
||||
|
||||
### Hindsight Response (Conservative assistant, high conscientiousness)
|
||||
> While the productivity data is encouraging, I'd recommend a cautious approach. The communication challenges and reduced spontaneous collaboration could impact team cohesion long-term. Consider a hybrid model that maintains some office presence for important collaborative work.
|
||||
|
||||
### Hindsight Response (Innovative assistant, high openness)
|
||||
> The data strongly supports remote work—65% productivity gains and an hour saved daily per employee. Communication challenges are solvable with the right tools. I'd embrace this as an opportunity to modernize how the team works.
|
||||
|
||||
**Why it works:** Each Hindsight bank has a personality (Big Five traits) that shapes how it interprets facts. Same data, consistent but different perspectives. The assistant develops opinions that persist and evolve over time.
|
||||
|
||||
---
|
||||
|
||||
## Example 4: Evolving Beliefs
|
||||
|
||||
**Scenario:** An AI coding assistant forms opinions based on experience.
|
||||
|
||||
**Week 1:**
|
||||
```python
|
||||
retain("User struggled with Python async/await syntax")
|
||||
retain("User successfully implemented the feature using threads")
|
||||
```
|
||||
*Assistant forms opinion:* "User prefers synchronous Python patterns" (confidence: 0.7)
|
||||
|
||||
**Week 3:**
|
||||
```python
|
||||
retain("User asked about asyncio best practices")
|
||||
retain("User implemented async database calls successfully")
|
||||
```
|
||||
*Opinion updated:* "User is growing comfortable with async patterns" (confidence: 0.6)
|
||||
|
||||
**User asks:** "Should I use async for this new API endpoint?"
|
||||
|
||||
### RAG Response
|
||||
> Async can improve performance for I/O-bound operations. Here's how to implement it...
|
||||
|
||||
**Why it fails:** No memory of user's journey. Same generic advice given to everyone.
|
||||
|
||||
### Hindsight Response
|
||||
> Given your recent success with async database calls, I'd recommend async for this endpoint too. You've gotten comfortable with the patterns—this is a good opportunity to apply them. Want me to show an approach similar to what worked in your database implementation?
|
||||
|
||||
**Why it works:** Hindsight tracked the user's evolving skill with async Python. Its opinion (with confidence score) updated as new evidence arrived. The response reflects accumulated understanding of this specific user's journey.
|
||||
|
||||
---
|
||||
|
||||
## Example 5: Entity Understanding
|
||||
|
||||
**Scenario:** A customer support AI learns about a user over multiple sessions:
|
||||
- Session 1: "I'm having trouble with my Pro subscription"
|
||||
- Session 2: "The mobile app crashes when I open settings"
|
||||
- Session 3: "I switched to annual billing last month"
|
||||
- Session 4: "The desktop app is working fine"
|
||||
|
||||
**User asks:** "What do you know about my account?"
|
||||
|
||||
### RAG Response
|
||||
> You've mentioned issues with Pro subscription, mobile app crashes in settings, switching to annual billing, and the desktop app working fine.
|
||||
|
||||
**Why it fails:** Returns disconnected facts in no particular structure. No synthesized understanding.
|
||||
|
||||
### Hindsight Response
|
||||
> You're on a Pro subscription with annual billing (switched last month). You've experienced some mobile app issues—specifically crashes in the settings menu—though your desktop app has been working fine. Would you like help troubleshooting the mobile app crashes?
|
||||
|
||||
**Why it works:** Hindsight automatically generates "observations"—synthesized summaries of entities (in this case, the user). Instead of listing disconnected facts, it presents a coherent picture and proactively offers relevant help.
|
||||
|
||||
---
|
||||
|
||||
## Example 6: Connecting Conversations Across Time
|
||||
|
||||
**Scenario:** A personal AI assistant has had conversations over several months:
|
||||
- January: "I'm thinking about learning guitar"
|
||||
- February: "I bought a used acoustic guitar"
|
||||
- March: "Practice is going well, learning basic chords"
|
||||
- June: "Work has been crazy, no time for hobbies"
|
||||
- September: "Feeling burned out, need something relaxing"
|
||||
|
||||
**User asks:** "What should I do to unwind this weekend?"
|
||||
|
||||
### RAG Response
|
||||
> Here are some popular relaxation activities: meditation, reading, nature walks, or picking up a creative hobby.
|
||||
|
||||
**Why it fails:** Generic suggestions. No awareness of the user's existing interests or history.
|
||||
|
||||
### Hindsight Response
|
||||
> You mentioned feeling burned out—have you picked up the guitar lately? You were making good progress with chords back in March before work got busy. Music could be a good way to unwind, and you already have the guitar.
|
||||
|
||||
**Why it works:** Hindsight connects the burnout complaint to the dormant hobby through entity and temporal reasoning. It remembers the user's journey: interest → purchase → progress → life interruption, and surfaces relevant context at the right moment.
|
||||
|
||||
---
|
||||
|
||||
## The Technical Difference
|
||||
|
||||
### How RAG Works
|
||||
```
|
||||
Query → Embed → Find similar vectors → Return top-k chunks
|
||||
```
|
||||
- One search strategy (semantic similarity)
|
||||
- No entity linking
|
||||
- No temporal understanding
|
||||
- No persistent beliefs
|
||||
- Stateless between queries
|
||||
|
||||
### How Hindsight Works
|
||||
```
|
||||
Query → Parse temporal expressions → Execute 4 parallel strategies:
|
||||
• Semantic search (like RAG)
|
||||
• Keyword search (catches exact names)
|
||||
• Graph traversal (entity/causal connections)
|
||||
• Temporal-graph (time-filtered relationships)
|
||||
→ Fuse results → Rerank → Apply personality → Generate response
|
||||
```
|
||||
- Four complementary search strategies
|
||||
- Knowledge graph with entity/temporal/causal links
|
||||
- Temporal parsing and filtering
|
||||
- Opinion formation and evolution
|
||||
- Consistent personality across sessions
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
## Capability Comparison
|
||||
|
||||
| Capability | RAG | Hindsight |
|
||||
|------------|-----|-----------|
|
||||
| **Multi-hop reasoning** | Misses indirect connections | Graph traversal finds relationships |
|
||||
| **Temporal queries** | "Last spring" = keyword match | Parses dates, filters to time range |
|
||||
| **Personality** | Generic responses | Consistent character with Big Five traits |
|
||||
| **Learning** | Stateless | Opinions evolve with evidence |
|
||||
| **Entity understanding** | Disconnected facts | Synthesized mental models |
|
||||
| **Context** | Top-k similar chunks | Rich connections across all memory |
|
||||
| **Search strategy** | Semantic similarity only | Semantic + keyword + graph + temporal |
|
||||
| **Multi-hop reasoning** | Limited to retrieved chunks | Graph traversal across entity relationships |
|
||||
| **Temporal queries** | Keyword matching ("spring") | Date parsing and range filtering |
|
||||
| **Entity understanding** | None | Entity resolution, observations, co-occurrence |
|
||||
| **Belief formation** | Stateless | Opinions with confidence scores that evolve |
|
||||
| **Personality** | None | Big Five traits influence interpretation |
|
||||
|
||||
---
|
||||
## Architecture Comparison
|
||||
|
||||
## Try It Yourself
|
||||
### RAG
|
||||
|
||||
```bash
|
||||
# Start Hindsight
|
||||
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
|
||||
```
|
||||
| Step | Operation |
|
||||
|------|-----------|
|
||||
| 1 | Embed query |
|
||||
| 2 | Vector similarity search |
|
||||
| 3 | Return top-k chunks |
|
||||
| 4 | Generate response |
|
||||
|
||||
```python
|
||||
from hindsight import HindsightClient
|
||||
Single retrieval strategy. No state between queries.
|
||||
|
||||
client = HindsightClient(base_url="http://localhost:8888")
|
||||
### Hindsight
|
||||
|
||||
# Store memories (not just chunks—rich facts with entities and timestamps)
|
||||
client.retain(bank_id="my-agent", content="Alice is the tech lead on Project Atlas")
|
||||
client.retain(bank_id="my-agent", content="Project Atlas uses Kubernetes for deployment")
|
||||
client.retain(bank_id="my-agent", content="The Kubernetes cluster had an outage last Tuesday")
|
||||
| Step | Operation |
|
||||
|------|-----------|
|
||||
| 1 | Parse query (extract temporal expressions, entities) |
|
||||
| 2 | Execute 4 parallel retrievals: semantic, BM25, graph, temporal |
|
||||
| 3 | Fuse results with RRF |
|
||||
| 4 | Rerank with cross-encoder |
|
||||
| 5 | Apply personality traits |
|
||||
| 6 | Generate response |
|
||||
|
||||
# Query with understanding (not just similarity)
|
||||
response = client.reflect(
|
||||
bank_id="my-agent",
|
||||
query="Was Alice affected by any recent issues?"
|
||||
)
|
||||
print(response.text)
|
||||
# "Yes, Alice was likely affected. She's the tech lead on Project Atlas..."
|
||||
```
|
||||
Multiple retrieval strategies. Persistent state across sessions.
|
||||
|
||||
Your AI deserves real memory, not just search.
|
||||
## Example Scenarios
|
||||
|
||||
### Multi-Hop Reasoning
|
||||
|
||||
**Stored facts:**
|
||||
- "Alice is the tech lead on Project Atlas"
|
||||
- "Project Atlas uses Kubernetes"
|
||||
- "Kubernetes cluster had an outage Tuesday"
|
||||
|
||||
**Query:** "Was Alice affected by recent issues?"
|
||||
|
||||
| System | Result |
|
||||
|--------|--------|
|
||||
| RAG | Retrieves facts about Alice only (no semantic similarity to "issues") |
|
||||
| Hindsight | Traverses Alice → Project Atlas → Kubernetes → outage via entity links |
|
||||
|
||||
### Temporal Queries
|
||||
|
||||
**Stored facts with timestamps:**
|
||||
- March: "Alice started microservices migration"
|
||||
- April: "Alice completed auth service"
|
||||
- October: "Alice focusing on performance"
|
||||
|
||||
**Query:** "What did Alice do last spring?"
|
||||
|
||||
| System | Result |
|
||||
|--------|--------|
|
||||
| RAG | Returns all Alice facts regardless of date |
|
||||
| Hindsight | Parses "last spring" → March-May, filters to that range |
|
||||
|
||||
### Entity Understanding
|
||||
|
||||
**Stored facts about a user across sessions:**
|
||||
- "Pro subscription"
|
||||
- "Mobile app crashes in settings"
|
||||
- "Switched to annual billing"
|
||||
- "Desktop app working fine"
|
||||
|
||||
**Query:** "What do you know about my account?"
|
||||
|
||||
| System | Result |
|
||||
|--------|--------|
|
||||
| RAG | Lists disconnected facts |
|
||||
| Hindsight | Returns synthesized entity observations: subscription status, billing, known issues |
|
||||
|
||||
### Belief Evolution
|
||||
|
||||
**Week 1:** User struggles with async Python, succeeds with threads
|
||||
**Week 3:** User asks about asyncio, implements async database calls
|
||||
|
||||
| System | Behavior |
|
||||
|--------|----------|
|
||||
| RAG | No memory of progression |
|
||||
| Hindsight | Forms opinion "user prefers sync" (0.7) → updates to "user growing comfortable with async" (0.6) |
|
||||
|
||||
## When to Use Each
|
||||
|
||||
| Use Case | Recommended |
|
||||
|----------|-------------|
|
||||
| Document Q&A over static corpus | RAG |
|
||||
| Search with no temporal requirements | RAG |
|
||||
| AI assistants with persistent memory | Hindsight |
|
||||
| Applications requiring entity tracking | Hindsight |
|
||||
| Systems needing consistent personality | Hindsight |
|
||||
| Temporal queries ("last month", "in 2023") | Hindsight |
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
# Storage
|
||||
|
||||
Hindsight uses PostgreSQL as its sole storage backend.
|
||||
|
||||
## Why PostgreSQL?
|
||||
|
||||
PostgreSQL provides all capabilities required for a semantic memory system in a single database:
|
||||
|
||||
| Capability | Implementation |
|
||||
|------------|----------------|
|
||||
| Vector search | pgvector extension with HNSW indexes |
|
||||
| Full-text search | Built-in tsvector with GIN indexes |
|
||||
| Relational data | Native PostgreSQL |
|
||||
| JSON documents | JSONB with indexing |
|
||||
| Graph queries | Recursive CTEs |
|
||||
|
||||
### Reduced System Dependencies
|
||||
|
||||
Building exclusively for PostgreSQL simplifies deployment and operations:
|
||||
|
||||
- Single connection string to configure
|
||||
- Single backup and restore strategy
|
||||
- Single monitoring target
|
||||
- ACID transactions across all data types
|
||||
- Single upgrade path
|
||||
|
||||
### No Storage Abstraction
|
||||
|
||||
Hindsight does not abstract storage behind a generic interface. This is a deliberate trade-off.
|
||||
|
||||
We believe PostgreSQL is becoming the standard database API. Its popularity, extension ecosystem, and modularity mean that PostgreSQL-compatible interfaces are appearing everywhere—from serverless offerings to distributed databases. Building for PostgreSQL today means compatibility with a growing ecosystem tomorrow.
|
||||
|
||||
Supporting multiple databases would increase flexibility but conflict with our core goals: Hindsight is fully open source and designed to be as simple as possible to run and use. Adding database abstractions introduces complexity in code, testing, documentation, and operations—complexity that we pass on to users.
|
||||
|
||||
By committing to PostgreSQL, we keep the system simple:
|
||||
- One set of deployment instructions
|
||||
- One set of performance characteristics to understand
|
||||
- One codebase optimized for one backend
|
||||
- No configuration decisions about which database to use
|
||||
|
||||
## Development with pg0
|
||||
|
||||
For local development, Hindsight uses **[pg0](https://github.com/vectorize-io/pg0)**—an embedded PostgreSQL distribution.
|
||||
|
||||
### What is pg0?
|
||||
|
||||
pg0 is a single binary containing:
|
||||
- PostgreSQL server
|
||||
- pgvector extension (pre-installed)
|
||||
- Automatic initialization
|
||||
|
||||
### Behavior
|
||||
|
||||
When no `DATABASE_URL` is configured, Hindsight:
|
||||
1. Downloads the pg0 binary for the current platform (macOS ARM, Linux x86_64/ARM64, Windows)
|
||||
2. Starts an embedded PostgreSQL instance on port 5555
|
||||
3. Initializes the schema
|
||||
4. Stores data in `~/.hindsight/pg0/`
|
||||
|
||||
### Environments
|
||||
|
||||
| Environment | Database | Configuration |
|
||||
|-------------|----------|---------------|
|
||||
| Development | pg0 (embedded) | Automatic |
|
||||
| Production | PostgreSQL 15+ | `DATABASE_URL` environment variable |
|
||||
|
||||
## Requirements
|
||||
|
||||
- PostgreSQL 15 or later
|
||||
- pgvector 0.5.0 or later
|
||||
|
||||
Any PostgreSQL instance that satisfies these requirements should work. If you encounter issues with a specific setup, [open a GitHub issue](https://github.com/hindsight-ai/hindsight/issues).
|
||||
|
||||
### Tested Managed Services
|
||||
|
||||
- AWS RDS (PostgreSQL 15+)
|
||||
- Google Cloud SQL
|
||||
- Azure Database for PostgreSQL
|
||||
- Supabase
|
||||
- Neon
|
||||
@@ -2072,44 +2072,6 @@
|
||||
"timestamp": "2024-01-15T10:30:00Z"
|
||||
}
|
||||
},
|
||||
"MetadataFilter": {
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string",
|
||||
"title": "Key",
|
||||
"description": "Metadata key to filter on"
|
||||
},
|
||||
"value": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Value",
|
||||
"description": "Value to match. If None with match_unset=True, matches any record where key is not set."
|
||||
},
|
||||
"match_unset": {
|
||||
"type": "boolean",
|
||||
"title": "Match Unset",
|
||||
"description": "If True, also match records where this metadata key is not set",
|
||||
"default": true
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"key"
|
||||
],
|
||||
"title": "MetadataFilter",
|
||||
"description": "Filter for metadata fields. Matches records where (key=value) OR (key not set) when match_unset=True.",
|
||||
"example": {
|
||||
"key": "source",
|
||||
"match_unset": true,
|
||||
"value": "slack"
|
||||
}
|
||||
},
|
||||
"RecallRequest": {
|
||||
"properties": {
|
||||
"query": {
|
||||
@@ -2157,21 +2119,6 @@
|
||||
"title": "Query Timestamp",
|
||||
"description": "ISO format date string (e.g., '2023-05-30T23:40:00')"
|
||||
},
|
||||
"filters": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/MetadataFilter"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Filters",
|
||||
"description": "Filter by metadata. Multiple filters are ANDed together."
|
||||
},
|
||||
"include": {
|
||||
"$ref": "#/components/schemas/IncludeOptions",
|
||||
"description": "Options for including additional data (entities are included by default)"
|
||||
@@ -2185,13 +2132,6 @@
|
||||
"description": "Request model for recall endpoint.",
|
||||
"example": {
|
||||
"budget": "mid",
|
||||
"filters": [
|
||||
{
|
||||
"key": "source",
|
||||
"match_unset": true,
|
||||
"value": "slack"
|
||||
}
|
||||
],
|
||||
"include": {
|
||||
"entities": {
|
||||
"max_tokens": 500
|
||||
@@ -2565,21 +2505,6 @@
|
||||
],
|
||||
"title": "Context"
|
||||
},
|
||||
"filters": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/MetadataFilter"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Filters",
|
||||
"description": "Filter by metadata. Multiple filters are ANDed together."
|
||||
},
|
||||
"include": {
|
||||
"$ref": "#/components/schemas/ReflectIncludeOptions",
|
||||
"description": "Options for including additional data (disabled by default)"
|
||||
@@ -2594,13 +2519,6 @@
|
||||
"example": {
|
||||
"budget": "low",
|
||||
"context": "This is for a research paper on AI ethics",
|
||||
"filters": [
|
||||
{
|
||||
"key": "source",
|
||||
"match_unset": true,
|
||||
"value": "slack"
|
||||
}
|
||||
],
|
||||
"include": {
|
||||
"facts": {}
|
||||
},
|
||||
|
||||
Generated
+896
-686
File diff suppressed because it is too large
Load Diff
@@ -48,5 +48,10 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0"
|
||||
},
|
||||
"overrides": {
|
||||
"openapi-to-postmanv2": {
|
||||
"js-yaml": "4.1.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,11 @@ const sidebars: SidebarsConfig = {
|
||||
id: 'developer/performance',
|
||||
label: 'Performance',
|
||||
},
|
||||
{
|
||||
type: 'doc',
|
||||
id: 'developer/storage',
|
||||
label: 'Storage',
|
||||
},
|
||||
{
|
||||
type: 'doc',
|
||||
id: 'developer/rag-vs-hindsight',
|
||||
@@ -195,7 +200,24 @@ const sidebars: SidebarsConfig = {
|
||||
{
|
||||
type: 'doc',
|
||||
id: 'cookbook/index',
|
||||
label: 'Cookbook',
|
||||
label: 'Overview',
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Use Cases',
|
||||
collapsible: false,
|
||||
items: [
|
||||
{
|
||||
type: 'doc',
|
||||
id: 'cookbook/per-user-memory',
|
||||
label: 'Per-User Memory',
|
||||
},
|
||||
{
|
||||
type: 'doc',
|
||||
id: 'cookbook/support-agent-with-shared-knowledge',
|
||||
label: 'Support Agent + Shared Knowledge',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
changelogSidebar: [
|
||||
|
||||
@@ -2072,44 +2072,6 @@
|
||||
"timestamp": "2024-01-15T10:30:00Z"
|
||||
}
|
||||
},
|
||||
"MetadataFilter": {
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string",
|
||||
"title": "Key",
|
||||
"description": "Metadata key to filter on"
|
||||
},
|
||||
"value": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Value",
|
||||
"description": "Value to match. If None with match_unset=True, matches any record where key is not set."
|
||||
},
|
||||
"match_unset": {
|
||||
"type": "boolean",
|
||||
"title": "Match Unset",
|
||||
"description": "If True, also match records where this metadata key is not set",
|
||||
"default": true
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"key"
|
||||
],
|
||||
"title": "MetadataFilter",
|
||||
"description": "Filter for metadata fields. Matches records where (key=value) OR (key not set) when match_unset=True.",
|
||||
"example": {
|
||||
"key": "source",
|
||||
"match_unset": true,
|
||||
"value": "slack"
|
||||
}
|
||||
},
|
||||
"RecallRequest": {
|
||||
"properties": {
|
||||
"query": {
|
||||
@@ -2157,21 +2119,6 @@
|
||||
"title": "Query Timestamp",
|
||||
"description": "ISO format date string (e.g., '2023-05-30T23:40:00')"
|
||||
},
|
||||
"filters": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/MetadataFilter"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Filters",
|
||||
"description": "Filter by metadata. Multiple filters are ANDed together."
|
||||
},
|
||||
"include": {
|
||||
"$ref": "#/components/schemas/IncludeOptions",
|
||||
"description": "Options for including additional data (entities are included by default)"
|
||||
@@ -2185,13 +2132,6 @@
|
||||
"description": "Request model for recall endpoint.",
|
||||
"example": {
|
||||
"budget": "mid",
|
||||
"filters": [
|
||||
{
|
||||
"key": "source",
|
||||
"match_unset": true,
|
||||
"value": "slack"
|
||||
}
|
||||
],
|
||||
"include": {
|
||||
"entities": {
|
||||
"max_tokens": 500
|
||||
@@ -2565,21 +2505,6 @@
|
||||
],
|
||||
"title": "Context"
|
||||
},
|
||||
"filters": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/MetadataFilter"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Filters",
|
||||
"description": "Filter by metadata. Multiple filters are ANDed together."
|
||||
},
|
||||
"include": {
|
||||
"$ref": "#/components/schemas/ReflectIncludeOptions",
|
||||
"description": "Options for including additional data (disabled by default)"
|
||||
@@ -2594,13 +2519,6 @@
|
||||
"example": {
|
||||
"budget": "low",
|
||||
"context": "This is for a research paper on AI ethics",
|
||||
"filters": [
|
||||
{
|
||||
"key": "source",
|
||||
"match_unset": true,
|
||||
"value": "slack"
|
||||
}
|
||||
],
|
||||
"include": {
|
||||
"facts": {}
|
||||
},
|
||||
|
||||
@@ -701,20 +701,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "docker"
|
||||
version = "7.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pywin32", marker = "sys_platform == 'win32'" },
|
||||
{ name = "requests" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/91/9b/4a2ea29aeba62471211598dac5d96825bb49348fa07e906ea930394a83ce/docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c", size = 117834 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "docstring-parser"
|
||||
version = "0.17.0"
|
||||
@@ -1218,7 +1204,6 @@ test = [
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "pytest-timeout" },
|
||||
{ name = "pytest-xdist" },
|
||||
{ name = "testcontainers" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
@@ -1229,7 +1214,6 @@ dev = [
|
||||
{ name = "pytest-timeout" },
|
||||
{ name = "pytest-xdist" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "testcontainers" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
@@ -1259,9 +1243,8 @@ requires-dist = [
|
||||
{ name = "python-dateutil", specifier = ">=2.8.0" },
|
||||
{ name = "python-dotenv", specifier = ">=1.0.0" },
|
||||
{ name = "rich", specifier = ">=13.0.0" },
|
||||
{ name = "sentence-transformers", specifier = ">=2.2.0" },
|
||||
{ name = "sentence-transformers", specifier = ">=3.0.0" },
|
||||
{ name = "sqlalchemy", specifier = ">=2.0.44" },
|
||||
{ name = "testcontainers", extras = ["postgres"], marker = "extra == 'test'", specifier = ">=4.0.0" },
|
||||
{ name = "tiktoken", specifier = ">=0.12.0" },
|
||||
{ name = "torch", specifier = ">=2.0.0" },
|
||||
{ name = "transformers", specifier = ">=4.30.0" },
|
||||
@@ -1272,13 +1255,12 @@ provides-extras = ["test"]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "filelock", specifier = ">=3.20.0" },
|
||||
{ name = "filelock", specifier = ">=3.0.0" },
|
||||
{ name = "pytest", specifier = ">=9.0.0" },
|
||||
{ name = "pytest-asyncio", specifier = ">=1.3.0" },
|
||||
{ name = "pytest-timeout", specifier = ">=2.4.0" },
|
||||
{ name = "pytest-xdist", specifier = ">=3.8.0" },
|
||||
{ name = "python-dotenv", specifier = ">=1.2.1" },
|
||||
{ name = "testcontainers", specifier = ">=4.13.3" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4044,22 +4026,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "testcontainers"
|
||||
version = "4.13.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "docker" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "urllib3" },
|
||||
{ name = "wrapt" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/b3/c272537f3ea2f312555efeb86398cc382cd07b740d5f3c730918c36e64e1/testcontainers-4.13.3.tar.gz", hash = "sha256:9d82a7052c9a53c58b69e1dc31da8e7a715e8b3ec1c4df5027561b47e2efe646", size = 79064 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/73/27/c2f24b19dafa197c514abe70eda69bc031c5152c6b1f1e5b20099e2ceedd/testcontainers-4.13.3-py3-none-any.whl", hash = "sha256:063278c4805ffa6dd85e56648a9da3036939e6c0ac1001e851c9276b19b05970", size = 124784 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "threadpoolctl"
|
||||
version = "3.6.0"
|
||||
|
||||
Reference in New Issue
Block a user