Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4092efdfa9 | ||
|
|
a7c094d436 |
@@ -2,9 +2,10 @@
|
||||
|
||||
import uuid
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
import httpx
|
||||
|
||||
from hindsight_api.api import create_app
|
||||
from hindsight_api.engine.memory_engine import MemoryEngine
|
||||
|
||||
@@ -446,3 +447,141 @@ class TestReflectUsesMentalModels:
|
||||
|
||||
# Cleanup
|
||||
await memory.delete_bank(bank_id, request_context=request_context)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reflect_on_empty_bank_completes_without_hanging(self, memory: MemoryEngine, request_context):
|
||||
"""Test that reflect completes when called on an empty bank without any documents retained.
|
||||
|
||||
This verifies that the agent doesn't loop indefinitely when there are no memories available.
|
||||
|
||||
Expected behavior:
|
||||
- The agent should try to search for information (recall/search_observations)
|
||||
- When no results are found, it should provide a response indicating no information is available
|
||||
- It should complete within the max_iterations limit without hanging
|
||||
"""
|
||||
bank_id = f"test-reflect-empty-{uuid.uuid4().hex[:8]}"
|
||||
|
||||
# Create the bank but DO NOT retain any documents
|
||||
await memory.get_bank_profile(bank_id=bank_id, request_context=request_context)
|
||||
|
||||
# Run reflect on the empty bank with a short timeout to detect hanging
|
||||
# Use a low budget to reduce max_iterations
|
||||
from hindsight_api.engine.memory_engine import Budget
|
||||
|
||||
result = await memory.reflect_async(
|
||||
bank_id=bank_id,
|
||||
query="What is the user's favorite color?",
|
||||
request_context=request_context,
|
||||
budget=Budget.LOW, # This should set max_iterations lower
|
||||
)
|
||||
|
||||
# Verify the result is returned (not hanging)
|
||||
assert result is not None, "Reflect should return a result even on empty bank"
|
||||
assert result.text, "Reflect should return some text response"
|
||||
|
||||
# Verify tool trace shows the agent tried to search
|
||||
tool_names = [tc.tool for tc in result.tool_trace]
|
||||
assert any(
|
||||
tool in tool_names for tool in ["recall", "search_observations", "search_mental_models"]
|
||||
), f"Agent should have tried to search for information. Tool calls: {tool_names}"
|
||||
|
||||
# Verify it completed successfully
|
||||
# The agent should find no results and provide a response anyway
|
||||
# We can infer from llm_trace that it completed (multiple LLM calls indicates iterations)
|
||||
assert len(result.llm_trace) > 0, "Should have made at least one LLM call"
|
||||
assert len(result.llm_trace) <= 15, (
|
||||
f"Made {len(result.llm_trace)} LLM calls, which suggests excessive looping. "
|
||||
f"Tool trace: {[tc.tool for tc in result.tool_trace]}"
|
||||
)
|
||||
|
||||
# The response should indicate no information is available or provide a generic answer
|
||||
# (not testing specific content since LLM behavior varies)
|
||||
|
||||
# Cleanup
|
||||
await memory.delete_bank(bank_id, request_context=request_context)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reflect_with_structured_output_handles_schema_mismatch_gracefully(
|
||||
self, memory: MemoryEngine, request_context
|
||||
):
|
||||
"""Test that reflect completes gracefully when structured output fails validation.
|
||||
|
||||
This verifies that if the LLM provides structured output that doesn't match
|
||||
the expected schema, or if structured output generation fails for any reason,
|
||||
the reflect operation still completes without looping.
|
||||
|
||||
Expected behavior:
|
||||
- The reflect operation should complete successfully
|
||||
- The text answer should be returned
|
||||
- structured_output should be None (or potentially populated if LLM succeeds)
|
||||
- Should not loop or hang
|
||||
"""
|
||||
bank_id = f"test-reflect-structured-{uuid.uuid4().hex[:8]}"
|
||||
|
||||
# Create the bank and add some content
|
||||
await memory.get_bank_profile(bank_id=bank_id, request_context=request_context)
|
||||
|
||||
# Retain a simple document
|
||||
await memory.retain_async(
|
||||
bank_id=bank_id,
|
||||
content="The user's favorite color is blue. They also like green.",
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
# Define a response schema that the LLM might have trouble with
|
||||
# or that might not match the actual answer
|
||||
response_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"favorite_color": {"type": "string", "description": "The user's favorite color"},
|
||||
"secondary_colors": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Other colors the user likes",
|
||||
},
|
||||
"confidence": {
|
||||
"type": "number",
|
||||
"description": "Confidence level from 0 to 1",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
},
|
||||
},
|
||||
"required": ["favorite_color"],
|
||||
}
|
||||
|
||||
# Run reflect with structured output
|
||||
from hindsight_api.engine.memory_engine import Budget
|
||||
|
||||
result = await memory.reflect_async(
|
||||
bank_id=bank_id,
|
||||
query="What is the user's favorite color?",
|
||||
request_context=request_context,
|
||||
response_schema=response_schema,
|
||||
budget=Budget.LOW,
|
||||
)
|
||||
|
||||
# Verify the operation completed successfully
|
||||
assert result is not None, "Reflect should return a result"
|
||||
assert result.text, "Reflect should return a text answer"
|
||||
|
||||
# structured_output might be None if extraction failed, or a dict if it succeeded
|
||||
# Both are acceptable - the key is that the operation completed
|
||||
assert result.structured_output is None or isinstance(
|
||||
result.structured_output, dict
|
||||
), "structured_output should be None or a dict"
|
||||
|
||||
# Verify it didn't loop excessively
|
||||
assert len(result.llm_trace) > 0, "Should have made at least one LLM call"
|
||||
assert len(result.llm_trace) <= 20, (
|
||||
f"Made {len(result.llm_trace)} LLM calls, which suggests excessive looping. "
|
||||
f"Tool trace: {[tc.tool for tc in result.tool_trace]}"
|
||||
)
|
||||
|
||||
# If structured output succeeded, verify it has the expected structure
|
||||
if result.structured_output:
|
||||
assert "favorite_color" in result.structured_output, (
|
||||
"If structured_output is returned, it should have the required fields"
|
||||
)
|
||||
|
||||
# Cleanup
|
||||
await memory.delete_bank(bank_id, request_context=request_context)
|
||||
|
||||
@@ -20,10 +20,24 @@ The API service handles all memory operations (retain, recall, reflect).
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `HINDSIGHT_API_DATABASE_URL` | PostgreSQL connection string | `pg0` (embedded) |
|
||||
| `HINDSIGHT_API_DATABASE_SCHEMA` | PostgreSQL schema name for tables | `public` |
|
||||
| `HINDSIGHT_API_RUN_MIGRATIONS_ON_STARTUP` | Run database migrations on API startup | `true` |
|
||||
|
||||
If not provided, the server uses embedded `pg0` — convenient for development but not recommended for production.
|
||||
|
||||
The `DATABASE_SCHEMA` setting allows you to use a custom PostgreSQL schema instead of the default `public` schema. This is useful for:
|
||||
- Multi-database setups where you want Hindsight tables in a dedicated schema
|
||||
- Hosting platforms (e.g., Supabase) where `public` schema is reserved or shared
|
||||
- Organizational preferences for schema naming conventions
|
||||
|
||||
```bash
|
||||
# Example: Using a custom schema
|
||||
export HINDSIGHT_API_DATABASE_URL=postgresql://user:pass@host:5432/dbname
|
||||
export HINDSIGHT_API_DATABASE_SCHEMA=hindsight
|
||||
```
|
||||
|
||||
Migrations will automatically create the schema if it doesn't exist and create all tables in the configured schema.
|
||||
|
||||
### Database Connection Pool
|
||||
|
||||
| Variable | Description | Default |
|
||||
@@ -364,6 +378,7 @@ Observations are consolidated knowledge synthesized from facts.
|
||||
|----------|-------------|---------|
|
||||
| `HINDSIGHT_API_ENABLE_OBSERVATIONS` | Enable observation consolidation | `true` |
|
||||
| `HINDSIGHT_API_CONSOLIDATION_BATCH_SIZE` | Memories to load per batch (internal optimization) | `50` |
|
||||
| `HINDSIGHT_API_CONSOLIDATION_MAX_TOKENS` | Max tokens for recall when finding related observations during consolidation | `1024` |
|
||||
| `HINDSIGHT_API_RETAIN_OBSERVATIONS_ASYNC` | Run observation generation asynchronously (after retain completes) | `false` |
|
||||
|
||||
### Reflect
|
||||
@@ -439,6 +454,7 @@ export HINDSIGHT_CP_DATAPLANE_API_URL=http://api.example.com:8888
|
||||
```bash
|
||||
# API Service
|
||||
HINDSIGHT_API_DATABASE_URL=postgresql://hindsight:hindsight_dev@localhost:5432/hindsight
|
||||
# HINDSIGHT_API_DATABASE_SCHEMA=public # optional, defaults to 'public'
|
||||
HINDSIGHT_API_LLM_PROVIDER=groq
|
||||
HINDSIGHT_API_LLM_API_KEY=gsk_xxxxxxxxxxxx
|
||||
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Bump documentation version for Docusaurus
|
||||
# Usage: ./scripts/bump-docs-version.sh <version>
|
||||
# Example: ./scripts/bump-docs-version.sh 0.4
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
DOCS_DIR="$ROOT_DIR/hindsight-docs"
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: $0 <version>"
|
||||
echo "Example: $0 0.4"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION="$1"
|
||||
|
||||
# Validate version format (minor version only: X.Y)
|
||||
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "Error: Version must be in format X.Y (e.g., 0.4)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Creating docs version $VERSION..."
|
||||
|
||||
# Create the version snapshot
|
||||
cd "$DOCS_DIR"
|
||||
npx docusaurus docs:version "$VERSION"
|
||||
|
||||
echo ""
|
||||
echo "Done! Version $VERSION created."
|
||||
echo ""
|
||||
echo "Files created/modified:"
|
||||
echo " - versioned_docs/version-$VERSION/"
|
||||
echo " - versioned_sidebars/version-$VERSION-sidebars.json"
|
||||
echo " - versions.json (automatically read by docusaurus.config.ts)"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Review the changes"
|
||||
echo " 2. Test with: cd hindsight-docs && npm run build"
|
||||
echo " 3. Commit the versioned docs"
|
||||
+31
-1
@@ -144,6 +144,19 @@ else
|
||||
print_warn "File $TYPESCRIPT_CLIENT_PKG not found, skipping"
|
||||
fi
|
||||
|
||||
# Update documentation version (creates new version or syncs to existing)
|
||||
print_info "Updating documentation for version $VERSION..."
|
||||
if [ -f "scripts/update-docs-version.sh" ]; then
|
||||
./scripts/update-docs-version.sh "$VERSION" 2>&1 | grep -E "✓|IMPORTANT|Error" || true
|
||||
if [ ${PIPESTATUS[0]} -eq 0 ]; then
|
||||
print_info "✓ Documentation updated"
|
||||
else
|
||||
print_warn "Failed to update documentation, but continuing..."
|
||||
fi
|
||||
else
|
||||
print_warn "update-docs-version.sh not found, skipping docs update"
|
||||
fi
|
||||
|
||||
# Show changes
|
||||
print_info "Changes to be committed:"
|
||||
git diff
|
||||
@@ -161,7 +174,13 @@ fi
|
||||
# Commit changes
|
||||
print_info "Committing version changes..."
|
||||
git add -A
|
||||
git commit --no-verify -m "Release v$VERSION
|
||||
|
||||
# Extract major.minor and patch for commit message
|
||||
MAJOR_MINOR=$(echo "$VERSION" | sed -E 's/^([0-9]+\.[0-9]+)\.[0-9]+$/\1/')
|
||||
PATCH_VERSION=$(echo "$VERSION" | sed -E 's/^[0-9]+\.[0-9]+\.([0-9]+)$/\1/')
|
||||
|
||||
# Build commit message
|
||||
COMMIT_MSG="Release v$VERSION
|
||||
|
||||
- Update version to $VERSION in all components
|
||||
- Python packages: hindsight-api, hindsight-dev, hindsight-all, hindsight-litellm, hindsight-embed
|
||||
@@ -171,6 +190,17 @@ git commit --no-verify -m "Release v$VERSION
|
||||
- Control Plane: hindsight-control-plane
|
||||
- Helm chart"
|
||||
|
||||
# Add docs update note
|
||||
if [ "$PATCH_VERSION" != "0" ]; then
|
||||
COMMIT_MSG="$COMMIT_MSG
|
||||
- Sync documentation to version-$MAJOR_MINOR"
|
||||
else
|
||||
COMMIT_MSG="$COMMIT_MSG
|
||||
- Create documentation version-$MAJOR_MINOR"
|
||||
fi
|
||||
|
||||
git commit --no-verify -m "$COMMIT_MSG"
|
||||
|
||||
# Create tag
|
||||
print_info "Creating tag v$VERSION..."
|
||||
git tag -a "v$VERSION" -m "Release v$VERSION"
|
||||
|
||||
Executable
+113
@@ -0,0 +1,113 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Unified docs versioning script for Docusaurus
|
||||
# Automatically handles both patch releases (sync) and minor/major releases (create new version)
|
||||
#
|
||||
# Usage: ./scripts/update-docs-version.sh <version>
|
||||
# Examples:
|
||||
# ./scripts/update-docs-version.sh 0.4.2 # Patch: syncs docs/ to existing version-0.4/
|
||||
# ./scripts/update-docs-version.sh 0.5.0 # Minor: creates new version-0.5/ snapshot
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
DOCS_DIR="$ROOT_DIR/hindsight-docs"
|
||||
|
||||
# Colors for output
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
print_info() {
|
||||
echo -e "${GREEN}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
print_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
}
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: $0 <version>"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 0.4.2 # Patch release: syncs docs/ to version-0.4/"
|
||||
echo " $0 0.5.0 # Minor release: creates new version-0.5/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION="$1"
|
||||
|
||||
# Validate version format (semantic versioning)
|
||||
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "Error: Version must be in X.Y.Z format (e.g., 0.4.2 or 0.5.0)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract major.minor and patch version
|
||||
MAJOR_MINOR=$(echo "$VERSION" | sed -E 's/^([0-9]+\.[0-9]+)\.[0-9]+$/\1/')
|
||||
PATCH_VERSION=$(echo "$VERSION" | sed -E 's/^[0-9]+\.[0-9]+\.([0-9]+)$/\1/')
|
||||
|
||||
SOURCE_DIR="$DOCS_DIR/docs"
|
||||
TARGET_DIR="$DOCS_DIR/versioned_docs/version-${MAJOR_MINOR}"
|
||||
VERSIONS_FILE="$DOCS_DIR/versions.json"
|
||||
|
||||
# Determine action based on patch version
|
||||
if [ "$PATCH_VERSION" != "0" ]; then
|
||||
#
|
||||
# PATCH RELEASE: Sync docs to existing version
|
||||
#
|
||||
print_info "Detected PATCH release ($VERSION)"
|
||||
print_info "Syncing docs/ → versioned_docs/version-${MAJOR_MINOR}/"
|
||||
|
||||
if [ ! -d "$TARGET_DIR" ]; then
|
||||
echo "Error: Target version directory does not exist: $TARGET_DIR"
|
||||
echo ""
|
||||
echo "Available versions:"
|
||||
[ -f "$VERSIONS_FILE" ] && cat "$VERSIONS_FILE" || echo " (No versions.json found)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Use rsync to sync, preserving structure and deleting removed files
|
||||
rsync -av --delete \
|
||||
--exclude='*.swp' \
|
||||
--exclude='.DS_Store' \
|
||||
"$SOURCE_DIR/" "$TARGET_DIR/"
|
||||
|
||||
echo ""
|
||||
print_info "✓ Synced docs/ to version-${MAJOR_MINOR}"
|
||||
print_info "✓ Files updated in: $TARGET_DIR"
|
||||
|
||||
else
|
||||
#
|
||||
# MINOR/MAJOR RELEASE: Create new version snapshot
|
||||
#
|
||||
print_info "Detected MINOR/MAJOR release ($VERSION)"
|
||||
print_info "Creating new docs version: version-${MAJOR_MINOR}"
|
||||
|
||||
# Check if version already exists
|
||||
if [ -d "$TARGET_DIR" ]; then
|
||||
echo "Error: Version $MAJOR_MINOR already exists in $TARGET_DIR"
|
||||
echo "If you want to update it, use a patch version (e.g., ${MAJOR_MINOR}.1)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create the version snapshot using Docusaurus
|
||||
cd "$DOCS_DIR"
|
||||
npx docusaurus docs:version "$MAJOR_MINOR"
|
||||
|
||||
echo ""
|
||||
print_info "✓ Created docs version-${MAJOR_MINOR}"
|
||||
print_info "Files created:"
|
||||
print_info " - versioned_docs/version-${MAJOR_MINOR}/"
|
||||
print_info " - versioned_sidebars/version-${MAJOR_MINOR}-sidebars.json"
|
||||
print_info " - versions.json (updated)"
|
||||
echo ""
|
||||
print_warn "IMPORTANT: Future docs changes will go to docs/ (next version)"
|
||||
print_warn " To update ${MAJOR_MINOR} docs, use patch releases (e.g., ${MAJOR_MINOR}.1)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
print_info "Next steps:"
|
||||
echo " 1. Review changes: git diff $DOCS_DIR"
|
||||
echo " 2. Test build: cd hindsight-docs && npm run build"
|
||||
echo " 3. Changes will be committed automatically by release.sh"
|
||||
@@ -1295,7 +1295,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "hindsight-all"
|
||||
version = "0.4.0"
|
||||
version = "0.4.1"
|
||||
source = { editable = "hindsight" }
|
||||
dependencies = [
|
||||
{ name = "hindsight-api" },
|
||||
@@ -1319,7 +1319,7 @@ provides-extras = ["test"]
|
||||
|
||||
[[package]]
|
||||
name = "hindsight-api"
|
||||
version = "0.4.0"
|
||||
version = "0.4.1"
|
||||
source = { editable = "hindsight-api" }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
@@ -1447,7 +1447,7 @@ dev = [
|
||||
|
||||
[[package]]
|
||||
name = "hindsight-client"
|
||||
version = "0.4.0"
|
||||
version = "0.4.1"
|
||||
source = { editable = "hindsight-clients/python" }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
@@ -1481,7 +1481,7 @@ provides-extras = ["test"]
|
||||
|
||||
[[package]]
|
||||
name = "hindsight-dev"
|
||||
version = "0.4.0"
|
||||
version = "0.4.1"
|
||||
source = { editable = "hindsight-dev" }
|
||||
dependencies = [
|
||||
{ name = "hindsight-api" },
|
||||
@@ -1527,7 +1527,7 @@ dev = [
|
||||
|
||||
[[package]]
|
||||
name = "hindsight-embed"
|
||||
version = "0.4.0"
|
||||
version = "0.4.1"
|
||||
source = { editable = "hindsight-embed" }
|
||||
dependencies = [
|
||||
{ name = "httpx" },
|
||||
|
||||
Reference in New Issue
Block a user