* feat: introduce hindsight-api-slim and hindsight-all-slim packages Closes #552 - Move all source code from hindsight-api/ to new hindsight-api-slim/ - hindsight-api-slim has heavy ML deps (torch, sentence-transformers, transformers, einops, flashrank, mlx, mlx-lm, safetensors) and pg0-embedded as optional extras: [local-ml], [embedded-db], [all] - hindsight-api becomes a zero-code meta-package depending on hindsight-api-slim[all] for full backward compatibility - Add hindsight-all-slim meta-package: hindsight-api-slim + client + embed - hindsight-all updated to depend on hindsight-api-slim[all] - pg0.py: lazy-import pg0 with clear ImportError pointing to [embedded-db] - Dockerfile: replace sed hack with proper uv sync --extra flags - Update release.yml, test.yml, lint.sh, release.sh, CLAUDE.md and all path references throughout the repo * refactor: rename hindsight/ directory to hindsight-all/ * docs: document hindsight-api-slim and hindsight-all-slim package variants Add package variants table and extras explanation to installation.md * docs: remove emojis from installation.md, use professional tone * docs: link Docker slim variant to pip package variants section * docs: consolidate Docker image variants into single table * ci: fix working-directory paths after package restructure - Replace all hindsight-api → hindsight-api-slim in test.yml - Replace hindsight → hindsight-all in test.yml - Add --extra embedded-db to test-embed API install step * ci: add local-ml and embedded-db extras to API sync steps These extras were previously implicit in the old hindsight-api package (which bundled everything). Now that hindsight-api-slim uses optional extras, we must explicitly request local-ml and embedded-db in CI. * ci: add API install step with embedded-db to test-embed smoke test The smoke test starts hindsight-api as a daemon, which requires pg0-embedded. Add a dedicated install step for hindsight-api-slim with embedded-db extra so the daemon can start successfully. * ci: remove --no-install-project when using optional extras When --no-install-project is combined with --extra, the optional deps are not installed because extras require the project to be active. Remove --no-install-project from steps that need local-ml or embedded-db. * ci: fix ordering of uv sync steps to preserve optional extras When uv sync runs for a different workspace member, it removes optional extras installed for other members. Fix by always running extra-requiring API sync last, after other workspace member syncs. Also remove --no-install-project from embedded-db sync in test-embed, as --no-install-project prevents optional extras from being active. * ci: add local-ml extra to test-embed API install for smoke test The smoke test starts the full API server which needs sentence-transformers for local embeddings (default provider). Add local-ml extra to the install. * ci: simplify extras with --all-extras and add slim pip smoke test - Replace explicit --extra local-ml --extra embedded-db with --all-extras for cleaner, more maintainable sync steps - Add test-pip-slim job: tests hindsight-api-slim[embedded-db] without local ML models, using Cohere for embeddings/reranking (mirrors Docker slim smoke test approach) * ci: simplify slim smoke test to health check only (mirrors Docker test)
273 lines
9.3 KiB
Python
273 lines
9.3 KiB
Python
"""
|
|
Comprehensive tracing span verification tests.
|
|
|
|
Verifies that all memory engine operations create correct parent and child spans
|
|
with proper attributes and hierarchy.
|
|
"""
|
|
|
|
from datetime import datetime, timezone
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.skip(reason="Background consolidation causes StopIteration - need to investigate separately")
|
|
@patch("hindsight_api.tracing._tracing_enabled", True)
|
|
@patch("hindsight_api.tracing._tracer")
|
|
async def test_recall_span_hierarchy(mock_tracer, memory, request_context):
|
|
"""Test that recall creates proper parent and child spans."""
|
|
# Setup mock spans
|
|
mock_recall_span = MagicMock()
|
|
mock_recall_span.__enter__ = MagicMock(return_value=mock_recall_span)
|
|
mock_recall_span.__exit__ = MagicMock(return_value=False)
|
|
|
|
mock_embedding_span = MagicMock()
|
|
mock_retrieval_span = MagicMock()
|
|
mock_fusion_span = MagicMock()
|
|
mock_rerank_span = MagicMock()
|
|
|
|
# Mock tracer to return spans in sequence
|
|
mock_tracer.start_as_current_span.side_effect = [mock_recall_span]
|
|
mock_tracer.start_span.side_effect = [
|
|
mock_embedding_span,
|
|
mock_retrieval_span,
|
|
mock_fusion_span,
|
|
mock_rerank_span,
|
|
]
|
|
|
|
bank_id = f"test-recall-{datetime.now(timezone.utc).timestamp()}"
|
|
|
|
try:
|
|
# Add some memories first
|
|
await memory.retain_async(
|
|
bank_id=bank_id,
|
|
content="Paris is the capital of France",
|
|
request_context=request_context,
|
|
)
|
|
|
|
# Wait a bit for any background tasks to settle
|
|
import asyncio
|
|
await asyncio.sleep(0.5)
|
|
|
|
# Reset mocks after retain
|
|
mock_tracer.reset_mock()
|
|
mock_recall_span.reset_mock()
|
|
|
|
# Execute recall
|
|
await memory.recall_async(
|
|
bank_id=bank_id,
|
|
query="What is the capital of France?",
|
|
request_context=request_context,
|
|
)
|
|
|
|
# Verify parent span was created with start_as_current_span
|
|
assert mock_tracer.start_as_current_span.called
|
|
parent_call = mock_tracer.start_as_current_span.call_args
|
|
assert parent_call[0][0] == "hindsight.recall"
|
|
|
|
# Verify parent span attributes were set
|
|
recall_attrs = {call[0][0]: call[0][1] for call in mock_recall_span.set_attribute.call_args_list}
|
|
assert "hindsight.bank_id" in recall_attrs
|
|
assert recall_attrs["hindsight.bank_id"] == bank_id
|
|
assert "hindsight.query" in recall_attrs
|
|
assert "hindsight.fact_types" in recall_attrs
|
|
assert "hindsight.thinking_budget" in recall_attrs
|
|
assert "hindsight.max_tokens" in recall_attrs
|
|
|
|
# Verify child spans were created (if tracing is enabled)
|
|
if mock_tracer.start_span.called:
|
|
child_spans = [call[0][0] for call in mock_tracer.start_span.call_args_list]
|
|
assert "hindsight.recall_embedding" in child_spans
|
|
assert "hindsight.recall_retrieval" in child_spans
|
|
assert "hindsight.recall_fusion" in child_spans
|
|
assert "hindsight.recall_rerank" in child_spans
|
|
|
|
finally:
|
|
# Cleanup
|
|
await memory.delete_bank(bank_id, request_context=request_context)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_mental_model_refresh_span_exists(memory, request_context):
|
|
"""Test that mental model refresh functionality exists (span creation tested via unit tests)."""
|
|
# This test verifies that refresh_mental_model method exists and can be called
|
|
# The actual span creation is tested in unit tests with proper mocking
|
|
bank_id = f"test-mmr-{datetime.now(timezone.utc).timestamp()}"
|
|
|
|
try:
|
|
# Just verify the method exists - it will return None if no mental model found
|
|
result = await memory.refresh_mental_model(
|
|
bank_id=bank_id,
|
|
mental_model_id="non-existent-id",
|
|
request_context=request_context,
|
|
)
|
|
# Result will be None since mental model doesn't exist
|
|
assert result is None
|
|
|
|
finally:
|
|
# Cleanup
|
|
try:
|
|
await memory.delete_bank(bank_id, request_context=request_context)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_consolidation_child_spans(memory, request_context):
|
|
"""Test that consolidation creates child spans for its operations."""
|
|
bank_id = f"test-cons-child-{datetime.now(timezone.utc).timestamp()}"
|
|
|
|
try:
|
|
# Add memories to consolidate
|
|
await memory.retain_async(
|
|
bank_id=bank_id,
|
|
content="The Eiffel Tower is in Paris",
|
|
request_context=request_context,
|
|
)
|
|
|
|
await memory.retain_async(
|
|
bank_id=bank_id,
|
|
content="Paris is the capital of France",
|
|
request_context=request_context,
|
|
)
|
|
|
|
# Run consolidation (this will create parent + child spans)
|
|
await memory.run_consolidation(
|
|
bank_id=bank_id,
|
|
request_context=request_context,
|
|
)
|
|
|
|
# Note: We can't easily verify the child spans without mocking the tracer,
|
|
# but we can verify that consolidation completes successfully
|
|
# The actual span creation is tested in unit tests
|
|
|
|
finally:
|
|
# Cleanup
|
|
await memory.delete_bank(bank_id, request_context=request_context)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_reflect_tool_call_spans(memory, request_context):
|
|
"""Test that reflect creates tool call spans (not reflect_generation)."""
|
|
bank_id = f"test-reflect-tools-{datetime.now(timezone.utc).timestamp()}"
|
|
|
|
try:
|
|
# Add some memories
|
|
await memory.retain_async(
|
|
bank_id=bank_id,
|
|
content="Machine learning is a subset of AI",
|
|
request_context=request_context,
|
|
)
|
|
|
|
# Execute reflect (will create reflect_tool_call spans)
|
|
result = await memory.reflect_async(
|
|
bank_id=bank_id,
|
|
query="What is machine learning?",
|
|
request_context=request_context,
|
|
)
|
|
|
|
# Verify reflect completed successfully
|
|
assert result.text
|
|
assert len(result.text) > 0
|
|
|
|
# The span names are verified via unit tests with mocked tracers
|
|
# This integration test ensures the operation completes successfully
|
|
|
|
finally:
|
|
# Cleanup
|
|
await memory.delete_bank(bank_id, request_context=request_context)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_all_operations_create_spans(memory, request_context):
|
|
"""Comprehensive test that all operations create their respective spans."""
|
|
bank_id = f"test-all-ops-{datetime.now(timezone.utc).timestamp()}"
|
|
|
|
try:
|
|
# 1. Retain operation
|
|
await memory.retain_async(
|
|
bank_id=bank_id,
|
|
content="Test memory for comprehensive span test",
|
|
request_context=request_context,
|
|
)
|
|
|
|
# 2. Recall operation
|
|
await memory.recall_async(
|
|
bank_id=bank_id,
|
|
query="test memory",
|
|
request_context=request_context,
|
|
)
|
|
|
|
# 3. Reflect operation
|
|
await memory.reflect_async(
|
|
bank_id=bank_id,
|
|
query="What can you tell me about the test?",
|
|
request_context=request_context,
|
|
)
|
|
|
|
# 4. Consolidation operation
|
|
await memory.run_consolidation(
|
|
bank_id=bank_id,
|
|
request_context=request_context,
|
|
)
|
|
|
|
# All operations completed successfully
|
|
# Span hierarchy verification is done in unit tests with mocked tracers
|
|
|
|
finally:
|
|
# Cleanup
|
|
await memory.delete_bank(bank_id, request_context=request_context)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("hindsight_api.tracing._tracing_enabled", True)
|
|
@patch("hindsight_api.tracing._tracer")
|
|
async def test_recall_span_attributes(mock_tracer, memory, request_context):
|
|
"""Verify that recall spans have all required attributes."""
|
|
# Setup mock span
|
|
mock_span = MagicMock()
|
|
mock_span.__enter__ = MagicMock(return_value=mock_span)
|
|
mock_span.__exit__ = MagicMock(return_value=False)
|
|
mock_tracer.start_as_current_span.return_value = mock_span
|
|
|
|
bank_id = f"test-attrs-{datetime.now(timezone.utc).timestamp()}"
|
|
|
|
try:
|
|
# Add memory
|
|
await memory.retain_async(
|
|
bank_id=bank_id,
|
|
content="Test content for attributes",
|
|
request_context=request_context,
|
|
)
|
|
|
|
# Reset mock
|
|
mock_span.reset_mock()
|
|
|
|
# Execute recall with specific parameters
|
|
await memory.recall_async(
|
|
bank_id=bank_id,
|
|
query="test query for attributes",
|
|
fact_type=["world", "experience"],
|
|
max_tokens=2048,
|
|
request_context=request_context,
|
|
)
|
|
|
|
# Collect all attributes set on the span
|
|
attrs = {call[0][0]: call[0][1] for call in mock_span.set_attribute.call_args_list}
|
|
|
|
# Verify required attributes
|
|
assert "hindsight.bank_id" in attrs
|
|
assert "hindsight.query" in attrs
|
|
assert "hindsight.fact_types" in attrs
|
|
assert "hindsight.max_tokens" in attrs
|
|
assert "hindsight.thinking_budget" in attrs
|
|
|
|
# Verify attribute values
|
|
assert attrs["hindsight.bank_id"] == bank_id
|
|
assert "test query" in attrs["hindsight.query"]
|
|
assert attrs["hindsight.max_tokens"] == 2048
|
|
|
|
finally:
|
|
# Cleanup
|
|
await memory.delete_bank(bank_id, request_context=request_context)
|