Compare commits

...
Author SHA1 Message Date
Nicolò Boschi 4092efdfa9 tests: add more tests about reflect 2026-01-29 17:00:20 +01:00
Nicolò Boschi a7c094d436 fix(doc): improve docs versioning and release (#231)
* fix(doc): improve docs versioning and release

* fix(doc): improve docs versioning and release
2026-01-29 14:45:57 +01:00
6 changed files with 305 additions and 50 deletions
+140 -1
View File
@@ -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
-43
View File
@@ -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
View File
@@ -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"
+113
View File
@@ -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"
Generated
+5 -5
View File
@@ -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" },