* 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)
103 lines
3.4 KiB
Python
103 lines
3.4 KiB
Python
"""
|
|
Test reflect endpoint with empty based_on (no memories scenario).
|
|
|
|
This test verifies that the API returns the correct based_on format:
|
|
- v0.3.0 (old): returned based_on as list []
|
|
- v0.4.0+ (current): returns based_on as object {"memories": [], "mental_models": [], "directives": []}
|
|
"""
|
|
|
|
import pytest
|
|
import pytest_asyncio
|
|
import httpx
|
|
from hindsight_api.api import create_app
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def api_client(memory):
|
|
"""Create an async test client for the FastAPI app."""
|
|
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
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_reflect_with_no_memories_empty_bank(api_client):
|
|
"""Test reflect on an empty bank (no memories) with include.facts enabled."""
|
|
bank_id = "test_empty_bank"
|
|
|
|
# Reflect on empty bank with facts requested
|
|
response = await api_client.post(
|
|
f"/v1/default/banks/{bank_id}/reflect",
|
|
json={
|
|
"query": "What do you know about machine learning?",
|
|
"budget": "low",
|
|
"include": {
|
|
"facts": {} # Request facts but bank is empty
|
|
}
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
|
|
# DEBUG: Print what the API actually returned
|
|
import json
|
|
print("\n" + "="*80)
|
|
print("API Response:")
|
|
print(json.dumps(data, indent=2))
|
|
print("="*80 + "\n")
|
|
|
|
# Verify response structure
|
|
assert "text" in data
|
|
assert "based_on" in data
|
|
|
|
# The API should return based_on as either:
|
|
# 1. null/None (if include.facts not set)
|
|
# 2. {"memories": [], "mental_models": [], "directives": []} (if include.facts set but empty)
|
|
# It should NEVER return based_on: []
|
|
|
|
based_on = data.get("based_on")
|
|
if based_on is not None:
|
|
assert isinstance(based_on, dict), f"based_on should be dict or null, got {type(based_on)}: {based_on}"
|
|
assert not isinstance(based_on, list), f"based_on should NEVER be a list! Got: {based_on}"
|
|
assert "memories" in based_on
|
|
assert "mental_models" in based_on
|
|
assert "directives" in based_on
|
|
# All should be empty lists
|
|
assert based_on["memories"] == []
|
|
assert based_on["mental_models"] == []
|
|
assert based_on["directives"] == []
|
|
|
|
# Verify the structure is parseable as proper types
|
|
assert isinstance(data["text"], str)
|
|
if based_on is not None:
|
|
# Verify it's the v0.4.0+ format (object with arrays)
|
|
assert isinstance(based_on["memories"], list)
|
|
assert isinstance(based_on["mental_models"], list)
|
|
assert isinstance(based_on["directives"], list)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_reflect_without_include_facts(api_client):
|
|
"""Test reflect without requesting facts (based_on should be None)."""
|
|
bank_id = "test_no_facts"
|
|
|
|
response = await api_client.post(
|
|
f"/v1/default/banks/{bank_id}/reflect",
|
|
json={
|
|
"query": "Hello world",
|
|
"budget": "low"
|
|
# No include.facts
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
|
|
# When include.facts is not set, based_on should not be in response (or be null)
|
|
based_on = data.get("based_on")
|
|
assert based_on is None, f"based_on should be None when not requested, got {type(based_on)}: {based_on}"
|
|
|
|
# Verify structure
|
|
assert isinstance(data["text"], str)
|