* chore: run benchmarks with reflect mode * chore: run benchmarks with reflect mode * fixes * new mm * bunch of fixes * initial commit * fixes * fixes * fixes * fix: sometimes memories gets extracted in the wrong language
359 lines
13 KiB
Python
359 lines
13 KiB
Python
"""Tests for reflections, mental models, and learnings functionality."""
|
|
|
|
import uuid
|
|
|
|
import pytest
|
|
import pytest_asyncio
|
|
import httpx
|
|
from hindsight_api.api import create_app
|
|
from hindsight_api.engine.memory_engine import MemoryEngine
|
|
|
|
|
|
@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.fixture
|
|
def test_bank_id():
|
|
"""Provide a unique bank ID for this test run."""
|
|
return f"test_reflections_{uuid.uuid4().hex[:8]}"
|
|
|
|
|
|
class TestReflectionsCRUD:
|
|
"""Test reflections CRUD operations via memory engine."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_and_get_reflection(self, memory: MemoryEngine, request_context):
|
|
"""Test creating and retrieving a reflection."""
|
|
bank_id = f"test-reflection-{uuid.uuid4().hex[:8]}"
|
|
|
|
# Create the bank first
|
|
await memory.get_bank_profile(bank_id=bank_id, request_context=request_context)
|
|
|
|
# Create a reflection
|
|
reflection = await memory.create_reflection(
|
|
bank_id=bank_id,
|
|
name="Team Preferences",
|
|
source_query="What are the team's communication preferences?",
|
|
content="The team prefers async communication via Slack",
|
|
tags=["team"],
|
|
request_context=request_context,
|
|
)
|
|
|
|
assert reflection["name"] == "Team Preferences"
|
|
assert reflection["source_query"] == "What are the team's communication preferences?"
|
|
assert reflection["content"] == "The team prefers async communication via Slack"
|
|
assert reflection["tags"] == ["team"]
|
|
assert "id" in reflection
|
|
|
|
# Get the reflection
|
|
fetched = await memory.get_reflection(
|
|
bank_id=bank_id,
|
|
reflection_id=reflection["id"],
|
|
request_context=request_context,
|
|
)
|
|
|
|
assert fetched["id"] == reflection["id"]
|
|
assert fetched["name"] == "Team Preferences"
|
|
|
|
# Cleanup
|
|
await memory.delete_bank(bank_id, request_context=request_context)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_reflections(self, memory: MemoryEngine, request_context):
|
|
"""Test listing reflections with filters."""
|
|
bank_id = f"test-reflection-list-{uuid.uuid4().hex[:8]}"
|
|
|
|
# Create the bank first
|
|
await memory.get_bank_profile(bank_id=bank_id, request_context=request_context)
|
|
|
|
# Create multiple reflections
|
|
await memory.create_reflection(
|
|
bank_id=bank_id,
|
|
name="Reflection 1",
|
|
source_query="Query 1",
|
|
content="Content 1",
|
|
tags=["tag1"],
|
|
request_context=request_context,
|
|
)
|
|
await memory.create_reflection(
|
|
bank_id=bank_id,
|
|
name="Reflection 2",
|
|
source_query="Query 2",
|
|
content="Content 2",
|
|
tags=["tag2"],
|
|
request_context=request_context,
|
|
)
|
|
|
|
# List all
|
|
all_reflections = await memory.list_reflections(
|
|
bank_id=bank_id,
|
|
request_context=request_context,
|
|
)
|
|
assert len(all_reflections) == 2
|
|
|
|
# List with tag filter
|
|
tag1_reflections = await memory.list_reflections(
|
|
bank_id=bank_id,
|
|
tags=["tag1"],
|
|
request_context=request_context,
|
|
)
|
|
assert len(tag1_reflections) == 1
|
|
|
|
# Cleanup
|
|
await memory.delete_bank(bank_id, request_context=request_context)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_reflection(self, memory: MemoryEngine, request_context):
|
|
"""Test updating a reflection."""
|
|
bank_id = f"test-reflection-update-{uuid.uuid4().hex[:8]}"
|
|
|
|
# Create the bank first
|
|
await memory.get_bank_profile(bank_id=bank_id, request_context=request_context)
|
|
|
|
# Create a reflection
|
|
reflection = await memory.create_reflection(
|
|
bank_id=bank_id,
|
|
name="Original Name",
|
|
source_query="Original Query",
|
|
content="Original Content",
|
|
request_context=request_context,
|
|
)
|
|
|
|
# Update the reflection
|
|
updated = await memory.update_reflection(
|
|
bank_id=bank_id,
|
|
reflection_id=reflection["id"],
|
|
name="Updated Name",
|
|
content="Updated Content",
|
|
request_context=request_context,
|
|
)
|
|
|
|
assert updated["name"] == "Updated Name"
|
|
assert updated["content"] == "Updated Content"
|
|
|
|
# Cleanup
|
|
await memory.delete_bank(bank_id, request_context=request_context)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_reflection(self, memory: MemoryEngine, request_context):
|
|
"""Test deleting a reflection."""
|
|
bank_id = f"test-reflection-delete-{uuid.uuid4().hex[:8]}"
|
|
|
|
# Create the bank first
|
|
await memory.get_bank_profile(bank_id=bank_id, request_context=request_context)
|
|
|
|
# Create a reflection
|
|
reflection = await memory.create_reflection(
|
|
bank_id=bank_id,
|
|
name="To Delete",
|
|
source_query="Query",
|
|
content="Content",
|
|
request_context=request_context,
|
|
)
|
|
|
|
# Delete the reflection
|
|
await memory.delete_reflection(
|
|
bank_id=bank_id,
|
|
reflection_id=reflection["id"],
|
|
request_context=request_context,
|
|
)
|
|
|
|
# Verify deletion - should return None
|
|
fetched = await memory.get_reflection(
|
|
bank_id=bank_id,
|
|
reflection_id=reflection["id"],
|
|
request_context=request_context,
|
|
)
|
|
assert fetched is None
|
|
|
|
# Cleanup
|
|
await memory.delete_bank(bank_id, request_context=request_context)
|
|
|
|
|
|
class TestMentalModelsAPI:
|
|
"""Test mental models API endpoints.
|
|
|
|
NOTE: Mental models are now stored in memory_units with fact_type='mental_model'
|
|
and accessed via recall with fact_type=["mental_model"]. The old /mental-models
|
|
endpoint was removed. These tests are skipped.
|
|
"""
|
|
|
|
@pytest.mark.skip(reason="Mental models endpoint removed - use recall with fact_type=['mental_model']")
|
|
@pytest.mark.asyncio
|
|
async def test_list_mental_models_empty(self, api_client, test_bank_id):
|
|
"""Test listing mental models when none exist."""
|
|
pass
|
|
|
|
@pytest.mark.skip(reason="Mental models endpoint removed - use recall with fact_type=['mental_model']")
|
|
@pytest.mark.asyncio
|
|
async def test_get_mental_model_not_found(self, api_client, test_bank_id):
|
|
"""Test getting a non-existent mental model."""
|
|
pass
|
|
|
|
|
|
class TestReflectionsAPI:
|
|
"""Test reflections API endpoints."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_reflections_api_crud(self, api_client, test_bank_id):
|
|
"""Test full CRUD cycle through API."""
|
|
import asyncio
|
|
|
|
# Create bank first via profile endpoint
|
|
await api_client.get(f"/v1/default/banks/{test_bank_id}/profile")
|
|
|
|
# Create a reflection (async operation)
|
|
response = await api_client.post(
|
|
f"/v1/default/banks/{test_bank_id}/reflections",
|
|
json={
|
|
"name": "API Test Reflection",
|
|
"source_query": "What is the API test about?",
|
|
"content": "This is an API test reflection",
|
|
"tags": ["api-test"],
|
|
},
|
|
)
|
|
assert response.status_code == 200
|
|
create_result = response.json()
|
|
assert "operation_id" in create_result
|
|
operation_id = create_result["operation_id"]
|
|
|
|
# Wait for the async operation to complete
|
|
for _ in range(30): # Wait up to 30 seconds
|
|
response = await api_client.get(f"/v1/default/banks/{test_bank_id}/operations/{operation_id}")
|
|
if response.status_code == 200:
|
|
op_status = response.json()
|
|
if op_status.get("status") == "completed":
|
|
break
|
|
await asyncio.sleep(1)
|
|
|
|
# List reflections to get the created reflection
|
|
response = await api_client.get(f"/v1/default/banks/{test_bank_id}/reflections")
|
|
assert response.status_code == 200
|
|
reflections = response.json()["items"]
|
|
assert len(reflections) >= 1
|
|
|
|
# Find our reflection
|
|
reflection = next((r for r in reflections if r["name"] == "API Test Reflection"), None)
|
|
assert reflection is not None, f"Reflection not found. Items: {reflections}"
|
|
reflection_id = reflection["id"]
|
|
|
|
# Get the reflection
|
|
response = await api_client.get(f"/v1/default/banks/{test_bank_id}/reflections/{reflection_id}")
|
|
assert response.status_code == 200
|
|
assert response.json()["name"] == "API Test Reflection"
|
|
|
|
# Update the reflection
|
|
response = await api_client.patch(
|
|
f"/v1/default/banks/{test_bank_id}/reflections/{reflection_id}",
|
|
json={"name": "Updated API Test Reflection"},
|
|
)
|
|
assert response.status_code == 200
|
|
assert response.json()["name"] == "Updated API Test Reflection"
|
|
|
|
# Delete the reflection
|
|
response = await api_client.delete(f"/v1/default/banks/{test_bank_id}/reflections/{reflection_id}")
|
|
assert response.status_code == 200
|
|
|
|
# Verify deletion
|
|
response = await api_client.get(f"/v1/default/banks/{test_bank_id}/reflections/{reflection_id}")
|
|
assert response.status_code == 404
|
|
|
|
# Cleanup
|
|
await api_client.delete(f"/v1/default/banks/{test_bank_id}")
|
|
|
|
|
|
class TestRecallWithMentalModelsAndReflections:
|
|
"""Test recall integration with mental models and reflections."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_recall_includes_mental_models(self, api_client, test_bank_id):
|
|
"""Test that recall can include mental models in the response."""
|
|
# Create bank first via profile endpoint
|
|
await api_client.get(f"/v1/default/banks/{test_bank_id}/profile")
|
|
|
|
# Note: Mental models are auto-created via consolidation, not manually
|
|
# This test just verifies the include parameter works
|
|
|
|
# Recall with mental models included
|
|
response = await api_client.post(
|
|
f"/v1/default/banks/{test_bank_id}/memories/recall",
|
|
json={
|
|
"query": "What is machine learning?",
|
|
"include": {
|
|
"mental_models": {"max_results": 5},
|
|
},
|
|
},
|
|
)
|
|
assert response.status_code == 200
|
|
result = response.json()
|
|
|
|
# Should have mental_models field in response (may be empty)
|
|
assert "mental_models" in result or result.get("mental_models") is None
|
|
|
|
# Cleanup
|
|
await api_client.delete(f"/v1/default/banks/{test_bank_id}")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_recall_includes_reflections(self, api_client, test_bank_id):
|
|
"""Test that recall can include reflections in the response."""
|
|
# Create bank first via profile endpoint
|
|
await api_client.get(f"/v1/default/banks/{test_bank_id}/profile")
|
|
|
|
# Create a reflection first
|
|
response = await api_client.post(
|
|
f"/v1/default/banks/{test_bank_id}/reflections",
|
|
json={
|
|
"name": "AI Overview",
|
|
"source_query": "What is AI?",
|
|
"content": "Artificial intelligence is the simulation of human intelligence",
|
|
"tags": [],
|
|
},
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
# Recall with reflections included
|
|
response = await api_client.post(
|
|
f"/v1/default/banks/{test_bank_id}/memories/recall",
|
|
json={
|
|
"query": "What is artificial intelligence?",
|
|
"include": {
|
|
"reflections": {"max_results": 5},
|
|
},
|
|
},
|
|
)
|
|
assert response.status_code == 200
|
|
result = response.json()
|
|
|
|
# Should have reflections in response (may be empty if embedding not generated yet)
|
|
assert "reflections" in result or result.get("reflections") is None
|
|
|
|
# Cleanup
|
|
await api_client.delete(f"/v1/default/banks/{test_bank_id}")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_recall_without_mental_models_by_default(self, api_client, test_bank_id):
|
|
"""Test that recall does not include mental models by default."""
|
|
# Create bank first via profile endpoint
|
|
await api_client.get(f"/v1/default/banks/{test_bank_id}/profile")
|
|
|
|
# Recall without specifying mental models
|
|
response = await api_client.post(
|
|
f"/v1/default/banks/{test_bank_id}/memories/recall",
|
|
json={
|
|
"query": "Test query",
|
|
},
|
|
)
|
|
assert response.status_code == 200
|
|
result = response.json()
|
|
|
|
# Mental models should not be in response
|
|
assert result.get("mental_models") is None
|
|
|
|
# Cleanup
|
|
await api_client.delete(f"/v1/default/banks/{test_bank_id}")
|