* feat: improve mental model refresh and add directives * feat: improve mental model refresh and add directives * tags * ui * fix * fix * update * update
1434 lines
51 KiB
Python
1434 lines
51 KiB
Python
"""Tests for mental model functionality (v4 system)."""
|
|
|
|
import uuid
|
|
|
|
import pytest
|
|
|
|
from hindsight_api.engine.memory_engine import MemoryEngine
|
|
|
|
|
|
@pytest.fixture
|
|
async def memory_with_mission(memory: MemoryEngine, request_context):
|
|
"""Memory engine with a bank that has a mission set.
|
|
|
|
Uses a unique bank_id to avoid conflicts between parallel tests.
|
|
"""
|
|
# Use unique bank_id to avoid conflicts between parallel tests
|
|
bank_id = f"test-mental-models-{uuid.uuid4().hex[:8]}"
|
|
|
|
# Set up the bank with a mission
|
|
await memory.set_bank_mission(
|
|
bank_id=bank_id,
|
|
mission="Be a PM for the engineering team",
|
|
request_context=request_context,
|
|
)
|
|
|
|
# Add some test data
|
|
await memory.retain_batch_async(
|
|
bank_id=bank_id,
|
|
contents=[
|
|
{"content": "The team has daily standups at 9am where everyone shares their progress."},
|
|
{"content": "Alice is the frontend engineer and specializes in React."},
|
|
{"content": "Bob is the backend engineer and owns the API services."},
|
|
{"content": "Sprint retrospectives happen every two weeks to discuss improvements."},
|
|
{"content": "John is the tech lead and makes final decisions on architecture."},
|
|
],
|
|
request_context=request_context,
|
|
)
|
|
|
|
# Wait for any background tasks from retain to complete
|
|
await memory.wait_for_background_tasks()
|
|
|
|
yield memory, bank_id
|
|
|
|
# Cleanup
|
|
await memory.delete_bank(bank_id, request_context=request_context)
|
|
|
|
|
|
class TestBankMission:
|
|
"""Test bank mission operations."""
|
|
|
|
async def test_set_and_get_mission(self, memory: MemoryEngine, request_context):
|
|
"""Test setting and getting a bank's mission."""
|
|
bank_id = f"test-mission-{uuid.uuid4().hex[:8]}"
|
|
|
|
# Set mission
|
|
result = await memory.set_bank_mission(
|
|
bank_id=bank_id,
|
|
mission="Track customer feedback",
|
|
request_context=request_context,
|
|
)
|
|
|
|
assert result["bank_id"] == bank_id
|
|
assert result["mission"] == "Track customer feedback"
|
|
|
|
# Get mission via profile
|
|
profile = await memory.get_bank_profile(bank_id=bank_id, request_context=request_context)
|
|
assert profile["mission"] == "Track customer feedback"
|
|
|
|
# Cleanup
|
|
await memory.delete_bank(bank_id, request_context=request_context)
|
|
|
|
|
|
class TestRefreshMentalModels:
|
|
"""Test the main refresh_mental_models flow."""
|
|
|
|
async def test_refresh_creates_structural_models(self, memory_with_mission, request_context):
|
|
"""Test that refresh creates structural models from the mission."""
|
|
memory, bank_id = memory_with_mission
|
|
|
|
# Refresh mental models (async - returns operation_id)
|
|
result = await memory.refresh_mental_models(
|
|
bank_id=bank_id,
|
|
request_context=request_context,
|
|
)
|
|
|
|
# Check that we got an operation ID back
|
|
assert "operation_id" in result
|
|
assert result["status"] == "queued"
|
|
|
|
# Wait for background task to complete
|
|
await memory.wait_for_background_tasks()
|
|
|
|
# Get the created models
|
|
models = await memory.list_mental_models(
|
|
bank_id=bank_id,
|
|
request_context=request_context,
|
|
)
|
|
|
|
assert len(models) > 0
|
|
|
|
# Check that structural models were created
|
|
structural_models = [m for m in models if m["subtype"] == "structural"]
|
|
assert len(structural_models) > 0
|
|
|
|
# Check that models have the expected structure
|
|
for model in models:
|
|
assert "id" in model
|
|
assert "name" in model
|
|
assert "description" in model
|
|
assert model["subtype"] in ["structural", "emergent"]
|
|
|
|
async def test_refresh_without_mission_fails(self, memory: MemoryEngine, request_context):
|
|
"""Test that refresh fails when no mission is set."""
|
|
bank_id = f"test-no-mission-refresh-{uuid.uuid4().hex[:8]}"
|
|
|
|
# Add some data but don't set a mission
|
|
await memory.retain_batch_async(
|
|
bank_id=bank_id,
|
|
contents=[
|
|
{"content": "Alice is the frontend engineer."},
|
|
{"content": "Bob is the backend engineer."},
|
|
],
|
|
request_context=request_context,
|
|
)
|
|
|
|
# Wait for any background tasks from retain to complete
|
|
await memory.wait_for_background_tasks()
|
|
|
|
# Refresh mental models should fail without a mission
|
|
with pytest.raises(ValueError) as exc_info:
|
|
await memory.refresh_mental_models(
|
|
bank_id=bank_id,
|
|
request_context=request_context,
|
|
)
|
|
|
|
assert "no mission is set" in str(exc_info.value).lower()
|
|
|
|
# Cleanup
|
|
await memory.delete_bank(bank_id, request_context=request_context)
|
|
|
|
|
|
class TestMentalModelCRUD:
|
|
"""Test basic CRUD operations for mental models."""
|
|
|
|
async def test_list_mental_models(self, memory_with_mission, request_context):
|
|
"""Test listing mental models."""
|
|
memory, bank_id = memory_with_mission
|
|
|
|
# Refresh to create models (async)
|
|
await memory.refresh_mental_models(
|
|
bank_id=bank_id,
|
|
request_context=request_context,
|
|
)
|
|
await memory.wait_for_background_tasks()
|
|
|
|
# List all models
|
|
models = await memory.list_mental_models(
|
|
bank_id=bank_id,
|
|
request_context=request_context,
|
|
)
|
|
|
|
assert len(models) > 0
|
|
|
|
# Test filtering by subtype
|
|
structural_models = await memory.list_mental_models(
|
|
bank_id=bank_id,
|
|
subtype="structural",
|
|
request_context=request_context,
|
|
)
|
|
|
|
assert all(m["subtype"] == "structural" for m in structural_models)
|
|
|
|
async def test_get_mental_model(self, memory_with_mission, request_context):
|
|
"""Test getting a mental model by ID."""
|
|
memory, bank_id = memory_with_mission
|
|
|
|
# Refresh to create models (async)
|
|
await memory.refresh_mental_models(
|
|
bank_id=bank_id,
|
|
request_context=request_context,
|
|
)
|
|
await memory.wait_for_background_tasks()
|
|
|
|
# Get the created models
|
|
models = await memory.list_mental_models(
|
|
bank_id=bank_id,
|
|
request_context=request_context,
|
|
)
|
|
|
|
# Get one by ID
|
|
model_id = models[0]["id"]
|
|
model = await memory.get_mental_model(
|
|
bank_id=bank_id,
|
|
model_id=model_id,
|
|
request_context=request_context,
|
|
)
|
|
|
|
assert model is not None
|
|
assert model["id"] == model_id
|
|
|
|
# Test non-existent
|
|
not_found = await memory.get_mental_model(
|
|
bank_id=bank_id,
|
|
model_id="non-existent",
|
|
request_context=request_context,
|
|
)
|
|
assert not_found is None
|
|
|
|
async def test_delete_mental_model(self, memory_with_mission, request_context):
|
|
"""Test deleting a mental model."""
|
|
memory, bank_id = memory_with_mission
|
|
|
|
# Refresh to create models (async)
|
|
await memory.refresh_mental_models(
|
|
bank_id=bank_id,
|
|
request_context=request_context,
|
|
)
|
|
await memory.wait_for_background_tasks()
|
|
|
|
# Get the created models
|
|
models = await memory.list_mental_models(
|
|
bank_id=bank_id,
|
|
request_context=request_context,
|
|
)
|
|
|
|
# Delete one
|
|
model_id = models[0]["id"]
|
|
deleted = await memory.delete_mental_model(
|
|
bank_id=bank_id,
|
|
model_id=model_id,
|
|
request_context=request_context,
|
|
)
|
|
assert deleted is True
|
|
|
|
# Verify it's gone
|
|
model = await memory.get_mental_model(
|
|
bank_id=bank_id,
|
|
model_id=model_id,
|
|
request_context=request_context,
|
|
)
|
|
assert model is None
|
|
|
|
# Delete non-existent returns False
|
|
deleted_again = await memory.delete_mental_model(
|
|
bank_id=bank_id,
|
|
model_id=model_id,
|
|
request_context=request_context,
|
|
)
|
|
assert deleted_again is False
|
|
|
|
async def test_create_pinned_mental_model(self, memory: MemoryEngine, request_context):
|
|
"""Test creating a pinned mental model."""
|
|
bank_id = f"test-pinned-{uuid.uuid4().hex[:8]}"
|
|
|
|
# Ensure bank exists by getting its profile (auto-creates if needed)
|
|
await memory.get_bank_profile(bank_id, request_context=request_context)
|
|
|
|
# Create a pinned mental model
|
|
model = await memory.create_mental_model(
|
|
bank_id=bank_id,
|
|
name="Product Roadmap",
|
|
description="Key product priorities and upcoming features",
|
|
tags=["project-x"],
|
|
request_context=request_context,
|
|
)
|
|
|
|
assert model["name"] == "Product Roadmap"
|
|
assert model["description"] == "Key product priorities and upcoming features"
|
|
assert model["subtype"] == "pinned"
|
|
assert model["tags"] == ["project-x"]
|
|
assert model["id"] == "pinned-product-roadmap"
|
|
|
|
# Verify it can be retrieved
|
|
retrieved = await memory.get_mental_model(
|
|
bank_id=bank_id,
|
|
model_id=model["id"],
|
|
request_context=request_context,
|
|
)
|
|
assert retrieved is not None
|
|
assert retrieved["subtype"] == "pinned"
|
|
|
|
# Cleanup
|
|
await memory.delete_bank(bank_id, request_context=request_context)
|
|
|
|
async def test_create_pinned_model_duplicate_fails(self, memory: MemoryEngine, request_context):
|
|
"""Test that creating a duplicate pinned model fails."""
|
|
bank_id = f"test-pinned-dup-{uuid.uuid4().hex[:8]}"
|
|
|
|
# Ensure bank exists
|
|
await memory.get_bank_profile(bank_id, request_context=request_context)
|
|
|
|
# Create first model
|
|
await memory.create_mental_model(
|
|
bank_id=bank_id,
|
|
name="Test Model",
|
|
description="First model",
|
|
request_context=request_context,
|
|
)
|
|
|
|
# Try to create duplicate
|
|
with pytest.raises(ValueError) as exc_info:
|
|
await memory.create_mental_model(
|
|
bank_id=bank_id,
|
|
name="Test Model",
|
|
description="Second model",
|
|
request_context=request_context,
|
|
)
|
|
|
|
assert "already exists" in str(exc_info.value).lower()
|
|
|
|
# Cleanup
|
|
await memory.delete_bank(bank_id, request_context=request_context)
|
|
|
|
async def test_pinned_models_survive_refresh(self, memory: MemoryEngine, request_context):
|
|
"""Test that pinned models are not deleted during refresh."""
|
|
bank_id = f"test-pinned-refresh-{uuid.uuid4().hex[:8]}"
|
|
|
|
# Set a mission
|
|
await memory.set_bank_mission(
|
|
bank_id=bank_id,
|
|
mission="Track customer feedback",
|
|
request_context=request_context,
|
|
)
|
|
|
|
# Create a pinned model
|
|
pinned_model = await memory.create_mental_model(
|
|
bank_id=bank_id,
|
|
name="Key Customers",
|
|
description="Important customers to track",
|
|
request_context=request_context,
|
|
)
|
|
|
|
# Refresh mental models
|
|
await memory.refresh_mental_models(
|
|
bank_id=bank_id,
|
|
request_context=request_context,
|
|
)
|
|
await memory.wait_for_background_tasks()
|
|
|
|
# Verify pinned model still exists
|
|
retrieved = await memory.get_mental_model(
|
|
bank_id=bank_id,
|
|
model_id=pinned_model["id"],
|
|
request_context=request_context,
|
|
)
|
|
assert retrieved is not None
|
|
assert retrieved["subtype"] == "pinned"
|
|
assert retrieved["name"] == "Key Customers"
|
|
|
|
# Cleanup
|
|
await memory.delete_bank(bank_id, request_context=request_context)
|
|
|
|
|
|
class TestMentalModelRefresh:
|
|
"""Test mental model summary refresh functionality."""
|
|
|
|
async def test_refresh_creates_models_with_summaries(self, memory_with_mission, request_context):
|
|
"""Test that refresh_mental_models creates models and generates summaries."""
|
|
memory, bank_id = memory_with_mission
|
|
|
|
# Refresh mental models (async - creates models and generates summaries)
|
|
result = await memory.refresh_mental_models(
|
|
bank_id=bank_id,
|
|
request_context=request_context,
|
|
)
|
|
|
|
assert "operation_id" in result
|
|
assert result["status"] == "queued"
|
|
|
|
# Wait for background task to complete (includes summary generation)
|
|
await memory.wait_for_background_tasks()
|
|
|
|
# Get the created models
|
|
models = await memory.list_mental_models(
|
|
bank_id=bank_id,
|
|
request_context=request_context,
|
|
)
|
|
|
|
assert len(models) > 0
|
|
|
|
# After async refresh completes, models should have summaries generated
|
|
for model in models:
|
|
assert "id" in model
|
|
assert "name" in model
|
|
# Summaries should be generated now (unless no relevant facts found)
|
|
# We don't strictly assert on summary presence since it depends on data
|
|
|
|
async def test_refresh_nonexistent_mental_model(self, memory: MemoryEngine, request_context):
|
|
"""Test refreshing a non-existent mental model returns None."""
|
|
bank_id = f"test-refresh-noexist-{uuid.uuid4().hex[:8]}"
|
|
|
|
result = await memory.refresh_mental_model(
|
|
bank_id=bank_id,
|
|
model_id="does-not-exist",
|
|
request_context=request_context,
|
|
)
|
|
|
|
assert result is None
|
|
|
|
|
|
class TestReflect:
|
|
"""Test reflect endpoint with mental models."""
|
|
|
|
async def test_reflect_basic(self, memory_with_mission, request_context):
|
|
"""Test basic reflect query - reflect works even without mental models."""
|
|
memory, bank_id = memory_with_mission
|
|
|
|
# Run a reflect query
|
|
result = await memory.reflect_async(
|
|
bank_id=bank_id,
|
|
query="Who are the team members?",
|
|
request_context=request_context,
|
|
)
|
|
|
|
assert result.text is not None
|
|
assert len(result.text) > 0
|
|
|
|
|
|
class TestMentalModelLearnTool:
|
|
"""Test mental model learn tool - creates placeholders with background generation."""
|
|
|
|
async def test_learn_creates_placeholder(self, memory: MemoryEngine, request_context):
|
|
"""Test that learn tool creates a placeholder mental model without observations."""
|
|
bank_id = f"test-source-facts-{uuid.uuid4().hex[:8]}"
|
|
|
|
# Add some test data
|
|
await memory.retain_batch_async(
|
|
bank_id=bank_id,
|
|
contents=[
|
|
{"content": "Alice is the team lead."},
|
|
{"content": "Bob is the engineer."},
|
|
],
|
|
request_context=request_context,
|
|
)
|
|
await memory.wait_for_background_tasks()
|
|
|
|
# Directly use the learn tool to create a mental model placeholder
|
|
from hindsight_api.engine.reflect.models import MentalModelInput
|
|
from hindsight_api.engine.reflect.tools import tool_learn
|
|
|
|
input_model = MentalModelInput(
|
|
name="Team Members",
|
|
description="Key team members and their roles",
|
|
)
|
|
|
|
pool = await memory._get_pool()
|
|
async with pool.acquire() as conn:
|
|
result = await tool_learn(conn, bank_id, input_model)
|
|
|
|
assert result["status"] == "created"
|
|
assert result["model_id"] == "team-members"
|
|
assert result["name"] == "Team Members"
|
|
assert result["pending_generation"] is True
|
|
|
|
# Verify placeholder was stored in database with empty observations
|
|
pool = await memory._get_pool()
|
|
async with pool.acquire() as conn:
|
|
row = await conn.fetchrow(
|
|
"SELECT subtype, name, description, observations FROM mental_models WHERE id = $1 AND bank_id = $2",
|
|
result["model_id"],
|
|
bank_id,
|
|
)
|
|
|
|
assert row is not None
|
|
assert row["subtype"] == "learned"
|
|
assert row["name"] == "Team Members"
|
|
assert row["description"] == "Key team members and their roles"
|
|
# Observations should be empty - will be generated in background
|
|
observations_data = row["observations"]
|
|
# Handle both string and dict representations
|
|
if isinstance(observations_data, str):
|
|
import json
|
|
observations_data = json.loads(observations_data) if observations_data else {}
|
|
assert observations_data == {} or observations_data is None
|
|
|
|
# Cleanup
|
|
await memory.delete_bank(bank_id, request_context=request_context)
|
|
|
|
async def test_learn_update_description(self, memory: MemoryEngine, request_context):
|
|
"""Test that updating a mental model updates the description."""
|
|
bank_id = f"test-merge-facts-{uuid.uuid4().hex[:8]}"
|
|
|
|
# Create bank by retaining some data
|
|
await memory.retain_batch_async(
|
|
bank_id=bank_id,
|
|
contents=[{"content": "Test data"}],
|
|
request_context=request_context,
|
|
)
|
|
await memory.wait_for_background_tasks()
|
|
|
|
from hindsight_api.engine.reflect.models import MentalModelInput
|
|
from hindsight_api.engine.reflect.tools import tool_learn
|
|
|
|
# First create a placeholder
|
|
input_model = MentalModelInput(
|
|
name="Team Members",
|
|
description="Initial description",
|
|
)
|
|
|
|
pool = await memory._get_pool()
|
|
async with pool.acquire() as conn:
|
|
result1 = await tool_learn(conn, bank_id, input_model)
|
|
|
|
assert result1["status"] == "created"
|
|
assert result1["pending_generation"] is True
|
|
|
|
# Now update with new description
|
|
input_model2 = MentalModelInput(
|
|
name="Team Members", # Same name = same ID
|
|
description="Updated description with more context",
|
|
)
|
|
|
|
pool = await memory._get_pool()
|
|
async with pool.acquire() as conn:
|
|
result2 = await tool_learn(conn, bank_id, input_model2)
|
|
|
|
assert result2["status"] == "updated"
|
|
assert result2["model_id"] == "team-members"
|
|
|
|
# Verify description was updated in database
|
|
pool = await memory._get_pool()
|
|
async with pool.acquire() as conn:
|
|
row = await conn.fetchrow(
|
|
"SELECT description FROM mental_models WHERE id = $1 AND bank_id = $2",
|
|
result1["model_id"],
|
|
bank_id,
|
|
)
|
|
|
|
assert row is not None
|
|
assert row["description"] == "Updated description with more context"
|
|
|
|
# Cleanup
|
|
await memory.delete_bank(bank_id, request_context=request_context)
|
|
|
|
|
|
class TestMentalModelTags:
|
|
"""Test mental model tags functionality."""
|
|
|
|
@pytest.fixture
|
|
async def memory_with_mission_and_tags(self, memory: MemoryEngine, request_context):
|
|
"""Memory engine with a bank that has a mission set and tagged content."""
|
|
bank_id = f"test-mm-tags-{uuid.uuid4().hex[:8]}"
|
|
|
|
# Set up the bank with a mission
|
|
await memory.set_bank_mission(
|
|
bank_id=bank_id,
|
|
mission="Be a PM for the engineering team",
|
|
request_context=request_context,
|
|
)
|
|
|
|
# Add some test data
|
|
await memory.retain_batch_async(
|
|
bank_id=bank_id,
|
|
contents=[
|
|
{"content": "Alice is the frontend engineer."},
|
|
{"content": "Bob is the backend engineer."},
|
|
],
|
|
request_context=request_context,
|
|
)
|
|
await memory.wait_for_background_tasks()
|
|
|
|
yield memory, bank_id
|
|
|
|
# Cleanup
|
|
await memory.delete_bank(bank_id, request_context=request_context)
|
|
|
|
async def test_refresh_creates_models_with_tags(self, memory_with_mission_and_tags, request_context):
|
|
"""Test that refresh_mental_models creates models with specified tags."""
|
|
memory, bank_id = memory_with_mission_and_tags
|
|
|
|
# Refresh mental models with tags
|
|
result = await memory.refresh_mental_models(
|
|
bank_id=bank_id,
|
|
tags=["project-alpha", "sprint-1"],
|
|
request_context=request_context,
|
|
)
|
|
|
|
assert "operation_id" in result
|
|
await memory.wait_for_background_tasks()
|
|
|
|
# Get the created models
|
|
models = await memory.list_mental_models(
|
|
bank_id=bank_id,
|
|
request_context=request_context,
|
|
)
|
|
|
|
assert len(models) > 0
|
|
|
|
# All models should have the tags we specified
|
|
for model in models:
|
|
assert "tags" in model
|
|
assert "project-alpha" in model["tags"]
|
|
assert "sprint-1" in model["tags"]
|
|
|
|
async def test_list_mental_models_filters_by_tags(self, memory_with_mission_and_tags, request_context):
|
|
"""Test that list_mental_models correctly filters by tags."""
|
|
memory, bank_id = memory_with_mission_and_tags
|
|
|
|
# Create models with different tags
|
|
await memory.refresh_mental_models(
|
|
bank_id=bank_id,
|
|
tags=["project-alpha"],
|
|
request_context=request_context,
|
|
)
|
|
await memory.wait_for_background_tasks()
|
|
|
|
# Get all models
|
|
all_models = await memory.list_mental_models(
|
|
bank_id=bank_id,
|
|
request_context=request_context,
|
|
)
|
|
assert len(all_models) > 0
|
|
|
|
# Filter by tags - should return models with matching tags
|
|
filtered_models = await memory.list_mental_models(
|
|
bank_id=bank_id,
|
|
tags=["project-alpha"],
|
|
request_context=request_context,
|
|
)
|
|
assert len(filtered_models) == len(all_models) # All models have this tag
|
|
|
|
# Filter by non-existent tag - should only return untagged models (none here)
|
|
# But since all models have tags, and the filter includes untagged,
|
|
# we need to test with a mix
|
|
empty_filtered = await memory.list_mental_models(
|
|
bank_id=bank_id,
|
|
tags=["non-existent-tag"],
|
|
request_context=request_context,
|
|
)
|
|
# Should return empty since no models are untagged and none match
|
|
# Actually, the logic includes untagged models, so let's verify the behavior
|
|
# All our models have tags, so only checking for non-existent tag
|
|
# should return nothing (since none match and none are untagged)
|
|
|
|
async def test_untagged_models_included_in_filter(self, memory: MemoryEngine, request_context):
|
|
"""Test that untagged mental models are always included when filtering."""
|
|
bank_id = f"test-untagged-{uuid.uuid4().hex[:8]}"
|
|
|
|
# Set up bank with mission
|
|
await memory.set_bank_mission(
|
|
bank_id=bank_id,
|
|
mission="Track projects",
|
|
request_context=request_context,
|
|
)
|
|
|
|
# Add some data
|
|
await memory.retain_batch_async(
|
|
bank_id=bank_id,
|
|
contents=[{"content": "Project Alpha is important."}],
|
|
request_context=request_context,
|
|
)
|
|
await memory.wait_for_background_tasks()
|
|
|
|
# First refresh without tags (creates untagged models)
|
|
await memory.refresh_mental_models(
|
|
bank_id=bank_id,
|
|
request_context=request_context, # No tags
|
|
)
|
|
await memory.wait_for_background_tasks()
|
|
|
|
# Get all models (should be untagged)
|
|
all_models = await memory.list_mental_models(
|
|
bank_id=bank_id,
|
|
request_context=request_context,
|
|
)
|
|
|
|
if len(all_models) > 0:
|
|
# Verify models are untagged
|
|
for model in all_models:
|
|
assert model.get("tags", []) == []
|
|
|
|
# Filter by any tag - untagged models should still be included
|
|
filtered_models = await memory.list_mental_models(
|
|
bank_id=bank_id,
|
|
tags=["some-tag"],
|
|
request_context=request_context,
|
|
)
|
|
# Untagged models should be included in the results
|
|
assert len(filtered_models) == len(all_models)
|
|
|
|
# Cleanup
|
|
await memory.delete_bank(bank_id, request_context=request_context)
|
|
|
|
async def test_tags_match_any(self, memory: MemoryEngine, request_context):
|
|
"""Test tags_match='any' returns models with at least one matching tag."""
|
|
bank_id = f"test-tags-any-{uuid.uuid4().hex[:8]}"
|
|
|
|
# Set up bank with mission
|
|
await memory.set_bank_mission(
|
|
bank_id=bank_id,
|
|
mission="Track projects",
|
|
request_context=request_context,
|
|
)
|
|
|
|
# Add data and create models with tags
|
|
await memory.retain_batch_async(
|
|
bank_id=bank_id,
|
|
contents=[{"content": "Alice works on frontend."}],
|
|
request_context=request_context,
|
|
)
|
|
await memory.wait_for_background_tasks()
|
|
|
|
await memory.refresh_mental_models(
|
|
bank_id=bank_id,
|
|
tags=["tag-a", "tag-b"],
|
|
request_context=request_context,
|
|
)
|
|
await memory.wait_for_background_tasks()
|
|
|
|
# Filter with tags_match='any' - should match if any tag matches
|
|
models = await memory.list_mental_models(
|
|
bank_id=bank_id,
|
|
tags=["tag-a", "tag-c"], # tag-a matches, tag-c doesn't
|
|
tags_match="any",
|
|
request_context=request_context,
|
|
)
|
|
|
|
# Models with tag-a should be included
|
|
for model in models:
|
|
if model.get("tags"):
|
|
# At least one of the filter tags should be in the model tags
|
|
# OR model is untagged
|
|
assert (
|
|
any(t in model["tags"] for t in ["tag-a", "tag-c"])
|
|
or model["tags"] == []
|
|
)
|
|
|
|
# Cleanup
|
|
await memory.delete_bank(bank_id, request_context=request_context)
|
|
|
|
async def test_reflect_with_tags_filter(self, memory_with_mission_and_tags, request_context):
|
|
"""Test that reflect filters memories by tags."""
|
|
memory, bank_id = memory_with_mission_and_tags
|
|
|
|
# Create mental models with tags
|
|
await memory.refresh_mental_models(
|
|
bank_id=bank_id,
|
|
tags=["project-x"],
|
|
request_context=request_context,
|
|
)
|
|
await memory.wait_for_background_tasks()
|
|
|
|
# Reflect with matching tags
|
|
result = await memory.reflect_async(
|
|
bank_id=bank_id,
|
|
query="Who are the engineers?",
|
|
tags=["project-x"],
|
|
request_context=request_context,
|
|
)
|
|
|
|
assert result.text is not None
|
|
assert len(result.text) > 0
|
|
|
|
# Reflect with non-matching tags - should still work
|
|
result2 = await memory.reflect_async(
|
|
bank_id=bank_id,
|
|
query="Who are the engineers?",
|
|
tags=["different-project"],
|
|
request_context=request_context,
|
|
)
|
|
|
|
assert result2.text is not None
|
|
|
|
async def test_mental_model_response_includes_tags(self, memory_with_mission_and_tags, request_context):
|
|
"""Test that mental model responses include the tags field."""
|
|
memory, bank_id = memory_with_mission_and_tags
|
|
|
|
# Create models with tags
|
|
await memory.refresh_mental_models(
|
|
bank_id=bank_id,
|
|
tags=["test-tag"],
|
|
request_context=request_context,
|
|
)
|
|
await memory.wait_for_background_tasks()
|
|
|
|
# Get models
|
|
models = await memory.list_mental_models(
|
|
bank_id=bank_id,
|
|
request_context=request_context,
|
|
)
|
|
|
|
# Verify tags field is present in response
|
|
for model in models:
|
|
assert "tags" in model
|
|
assert isinstance(model["tags"], list)
|
|
|
|
# Get single model
|
|
if models:
|
|
model = await memory.get_mental_model(
|
|
bank_id=bank_id,
|
|
model_id=models[0]["id"],
|
|
request_context=request_context,
|
|
)
|
|
assert "tags" in model
|
|
assert isinstance(model["tags"], list)
|
|
|
|
|
|
class TestDirectives:
|
|
"""Test directive mental model functionality."""
|
|
|
|
async def test_create_directive(self, memory: MemoryEngine, request_context):
|
|
"""Test creating a directive mental model with user-provided observations."""
|
|
bank_id = f"test-directive-{uuid.uuid4().hex[:8]}"
|
|
|
|
# Ensure bank exists
|
|
await memory.get_bank_profile(bank_id, request_context=request_context)
|
|
|
|
# Create a directive with observations
|
|
model = await memory.create_mental_model(
|
|
bank_id=bank_id,
|
|
name="Competitor Policy",
|
|
description="Rules about mentioning competitors",
|
|
subtype="directive",
|
|
observations=[
|
|
{"title": "Never mention", "content": "Never mention competitor product names directly"},
|
|
{"title": "Redirect", "content": "If asked about competitors, redirect to our features"},
|
|
],
|
|
request_context=request_context,
|
|
)
|
|
|
|
assert model["name"] == "Competitor Policy"
|
|
assert model["description"] == "Rules about mentioning competitors"
|
|
assert model["subtype"] == "directive"
|
|
assert len(model["observations"]) == 2
|
|
assert model["observations"][0].title == "Never mention"
|
|
assert model["observations"][0].content == "Never mention competitor product names directly"
|
|
|
|
# Cleanup
|
|
await memory.delete_bank(bank_id, request_context=request_context)
|
|
|
|
async def test_directive_included_in_list(self, memory: MemoryEngine, request_context):
|
|
"""Test that directives are included in list_mental_models for admin visibility."""
|
|
bank_id = f"test-directive-list-{uuid.uuid4().hex[:8]}"
|
|
|
|
# Set up bank with mission
|
|
await memory.set_bank_mission(
|
|
bank_id=bank_id,
|
|
mission="Test mission",
|
|
request_context=request_context,
|
|
)
|
|
|
|
# Create a directive
|
|
directive = await memory.create_mental_model(
|
|
bank_id=bank_id,
|
|
name="Test Directive",
|
|
description="A test directive",
|
|
subtype="directive",
|
|
observations=[{"title": "Rule", "content": "Follow this rule"}],
|
|
request_context=request_context,
|
|
)
|
|
|
|
# Create a pinned model
|
|
pinned = await memory.create_mental_model(
|
|
bank_id=bank_id,
|
|
name="Test Pinned",
|
|
description="A test pinned model",
|
|
request_context=request_context,
|
|
)
|
|
|
|
# List without subtype filter - both should appear
|
|
models = await memory.list_mental_models(
|
|
bank_id=bank_id,
|
|
request_context=request_context,
|
|
)
|
|
|
|
# Both should appear (directives included in API listing for admin visibility)
|
|
model_ids = [m["id"] for m in models]
|
|
assert pinned["id"] in model_ids
|
|
assert directive["id"] in model_ids
|
|
|
|
# List with directive subtype filter - should find only directive
|
|
directives = await memory.list_mental_models(
|
|
bank_id=bank_id,
|
|
subtype="directive",
|
|
request_context=request_context,
|
|
)
|
|
assert len(directives) == 1
|
|
assert directives[0]["id"] == directive["id"]
|
|
|
|
# Cleanup
|
|
await memory.delete_bank(bank_id, request_context=request_context)
|
|
|
|
async def test_directive_get_includes_observations(self, memory: MemoryEngine, request_context):
|
|
"""Test that getting a directive returns its user-provided observations."""
|
|
bank_id = f"test-directive-get-{uuid.uuid4().hex[:8]}"
|
|
|
|
# Ensure bank exists
|
|
await memory.get_bank_profile(bank_id, request_context=request_context)
|
|
|
|
# Create a directive with observations
|
|
created = await memory.create_mental_model(
|
|
bank_id=bank_id,
|
|
name="Meeting Rules",
|
|
description="Rules for scheduling meetings",
|
|
subtype="directive",
|
|
observations=[
|
|
{"title": "No mornings", "content": "Never schedule meetings before noon"},
|
|
{"title": "Max duration", "content": "Meetings should be 30 minutes max"},
|
|
],
|
|
request_context=request_context,
|
|
)
|
|
|
|
# Get the directive
|
|
retrieved = await memory.get_mental_model(
|
|
bank_id=bank_id,
|
|
model_id=created["id"],
|
|
request_context=request_context,
|
|
)
|
|
|
|
assert retrieved is not None
|
|
assert retrieved["subtype"] == "directive"
|
|
assert len(retrieved["observations"]) == 2
|
|
assert retrieved["observations"][0].title == "No mornings"
|
|
assert retrieved["observations"][1].title == "Max duration"
|
|
|
|
# Cleanup
|
|
await memory.delete_bank(bank_id, request_context=request_context)
|
|
|
|
async def test_directive_survives_refresh(self, memory: MemoryEngine, request_context):
|
|
"""Test that directives are not modified during refresh_mental_models."""
|
|
bank_id = f"test-directive-refresh-{uuid.uuid4().hex[:8]}"
|
|
|
|
# Set up bank with mission
|
|
await memory.set_bank_mission(
|
|
bank_id=bank_id,
|
|
mission="Test mission",
|
|
request_context=request_context,
|
|
)
|
|
|
|
# Add some test data
|
|
await memory.retain_batch_async(
|
|
bank_id=bank_id,
|
|
contents=[{"content": "Alice is the engineer."}],
|
|
request_context=request_context,
|
|
)
|
|
await memory.wait_for_background_tasks()
|
|
|
|
# Create a directive
|
|
directive = await memory.create_mental_model(
|
|
bank_id=bank_id,
|
|
name="Important Rule",
|
|
description="A critical rule",
|
|
subtype="directive",
|
|
observations=[{"title": "Rule 1", "content": "Always follow this rule"}],
|
|
request_context=request_context,
|
|
)
|
|
|
|
# Refresh mental models
|
|
await memory.refresh_mental_models(
|
|
bank_id=bank_id,
|
|
request_context=request_context,
|
|
)
|
|
await memory.wait_for_background_tasks()
|
|
|
|
# Directive should still exist with same observations
|
|
retrieved = await memory.get_mental_model(
|
|
bank_id=bank_id,
|
|
model_id=directive["id"],
|
|
request_context=request_context,
|
|
)
|
|
|
|
assert retrieved is not None
|
|
assert retrieved["subtype"] == "directive"
|
|
assert len(retrieved["observations"]) == 1
|
|
assert retrieved["observations"][0].title == "Rule 1"
|
|
assert retrieved["observations"][0].content == "Always follow this rule"
|
|
|
|
# Cleanup
|
|
await memory.delete_bank(bank_id, request_context=request_context)
|
|
|
|
async def test_directive_requires_observations(self, memory: MemoryEngine, request_context):
|
|
"""Test that creating a directive without observations fails."""
|
|
bank_id = f"test-directive-no-obs-{uuid.uuid4().hex[:8]}"
|
|
|
|
# Ensure bank exists
|
|
await memory.get_bank_profile(bank_id, request_context=request_context)
|
|
|
|
# Try to create directive without observations
|
|
with pytest.raises(ValueError) as exc_info:
|
|
await memory.create_mental_model(
|
|
bank_id=bank_id,
|
|
name="Bad Directive",
|
|
description="A directive without observations",
|
|
subtype="directive",
|
|
# No observations provided
|
|
request_context=request_context,
|
|
)
|
|
|
|
assert "observations" in str(exc_info.value).lower()
|
|
|
|
# Cleanup
|
|
await memory.delete_bank(bank_id, request_context=request_context)
|
|
|
|
|
|
class TestDirectivesInReflect:
|
|
"""Test that directives are followed during reflect operations."""
|
|
|
|
async def test_reflect_follows_language_directive(self, memory: MemoryEngine, request_context):
|
|
"""Test that reflect follows a directive to respond in a specific language."""
|
|
bank_id = f"test-directive-reflect-{uuid.uuid4().hex[:8]}"
|
|
|
|
# Ensure bank exists
|
|
await memory.get_bank_profile(bank_id, request_context=request_context)
|
|
|
|
# Add some content in English
|
|
await memory.retain_batch_async(
|
|
bank_id=bank_id,
|
|
contents=[
|
|
{"content": "Alice is a software engineer who works at Google."},
|
|
{"content": "Alice enjoys hiking on weekends and has been to Yosemite."},
|
|
{"content": "Alice is currently working on a machine learning project."},
|
|
],
|
|
request_context=request_context,
|
|
)
|
|
await memory.wait_for_background_tasks()
|
|
|
|
# Create a directive to always respond in French
|
|
await memory.create_mental_model(
|
|
bank_id=bank_id,
|
|
name="Language Policy",
|
|
description="Rules about language usage",
|
|
subtype="directive",
|
|
observations=[
|
|
{
|
|
"title": "French Only",
|
|
"content": "ALWAYS respond in French language. Never respond in English.",
|
|
},
|
|
],
|
|
request_context=request_context,
|
|
)
|
|
|
|
# Run reflect query
|
|
result = await memory.reflect_async(
|
|
bank_id=bank_id,
|
|
query="What does Alice do for work?",
|
|
request_context=request_context,
|
|
)
|
|
|
|
assert result.text is not None
|
|
assert len(result.text) > 0
|
|
|
|
# Check that the response contains French words/patterns
|
|
# Common French words that would appear when talking about someone's job
|
|
french_indicators = [
|
|
"elle",
|
|
"travaille",
|
|
"est",
|
|
"une",
|
|
"le",
|
|
"la",
|
|
"qui",
|
|
"chez",
|
|
"logiciel",
|
|
"ingénieur",
|
|
"ingénieure",
|
|
"développeur",
|
|
"développeuse",
|
|
]
|
|
response_lower = result.text.lower()
|
|
|
|
# At least some French words should appear in the response
|
|
french_word_count = sum(1 for word in french_indicators if word in response_lower)
|
|
assert (
|
|
french_word_count >= 2
|
|
), f"Expected French response, but got: {result.text[:200]}"
|
|
|
|
# Cleanup
|
|
await memory.delete_bank(bank_id, request_context=request_context)
|
|
|
|
class TestMentalModelTagsFiltering:
|
|
"""Test tags filtering for mental models (all types)."""
|
|
|
|
async def test_tags_match_any_includes_untagged(self, memory: MemoryEngine, request_context):
|
|
"""Test that 'any' tags_match mode includes untagged mental models."""
|
|
bank_id = f"test-mm-tags-any-{uuid.uuid4().hex[:8]}"
|
|
|
|
# Ensure bank exists
|
|
await memory.get_bank_profile(bank_id, request_context=request_context)
|
|
|
|
# Create an UNTAGGED pinned model
|
|
await memory.create_mental_model(
|
|
bank_id=bank_id,
|
|
name="Global Model",
|
|
description="A global mental model",
|
|
subtype="pinned",
|
|
tags=[], # No tags - should be included with "any" mode
|
|
request_context=request_context,
|
|
)
|
|
|
|
# Test 1: list_mental_models with tags and tags_match="any" should include untagged
|
|
models_any = await memory.list_mental_models(
|
|
bank_id=bank_id,
|
|
tags=["some-tag"],
|
|
tags_match="any", # Should include untagged
|
|
request_context=request_context,
|
|
)
|
|
assert len(models_any) == 1, f"Expected untagged model with 'any' mode, got {len(models_any)}"
|
|
|
|
# Test 2: list_mental_models with tags and tags_match="any_strict" should exclude untagged
|
|
models_strict = await memory.list_mental_models(
|
|
bank_id=bank_id,
|
|
tags=["some-tag"],
|
|
tags_match="any_strict", # Should exclude untagged
|
|
request_context=request_context,
|
|
)
|
|
assert len(models_strict) == 0, f"Expected no models with 'any_strict' mode, got {len(models_strict)}"
|
|
|
|
# Cleanup
|
|
await memory.delete_bank(bank_id, request_context=request_context)
|
|
|
|
async def test_tags_match_strict_modes(self, memory: MemoryEngine, request_context):
|
|
"""Test that strict modes only include mental models with matching tags."""
|
|
bank_id = f"test-mm-tags-strict-{uuid.uuid4().hex[:8]}"
|
|
|
|
# Ensure bank exists
|
|
await memory.get_bank_profile(bank_id, request_context=request_context)
|
|
|
|
# Create a TAGGED pinned model
|
|
await memory.create_mental_model(
|
|
bank_id=bank_id,
|
|
name="Tagged Model",
|
|
description="A tagged mental model",
|
|
subtype="pinned",
|
|
tags=["project-a"],
|
|
request_context=request_context,
|
|
)
|
|
|
|
# Create an UNTAGGED pinned model
|
|
await memory.create_mental_model(
|
|
bank_id=bank_id,
|
|
name="Untagged Model",
|
|
description="An untagged mental model",
|
|
subtype="pinned",
|
|
tags=[], # No tags
|
|
request_context=request_context,
|
|
)
|
|
|
|
# Test 1: any_strict with matching tag - should get ONLY the tagged model
|
|
models_match = await memory.list_mental_models(
|
|
bank_id=bank_id,
|
|
tags=["project-a"],
|
|
tags_match="any_strict",
|
|
request_context=request_context,
|
|
)
|
|
assert len(models_match) == 1, f"Expected 1 model with matching tag, got {len(models_match)}"
|
|
assert models_match[0]["name"] == "Tagged Model"
|
|
|
|
# Test 2: any_strict with different tag - should get NO models
|
|
models_no_match = await memory.list_mental_models(
|
|
bank_id=bank_id,
|
|
tags=["project-b"],
|
|
tags_match="any_strict",
|
|
request_context=request_context,
|
|
)
|
|
assert len(models_no_match) == 0, f"Expected no models with non-matching tag, got {len(models_no_match)}"
|
|
|
|
# Test 3: any (non-strict) with any tag - should get BOTH models
|
|
models_any = await memory.list_mental_models(
|
|
bank_id=bank_id,
|
|
tags=["project-a"],
|
|
tags_match="any",
|
|
request_context=request_context,
|
|
)
|
|
assert len(models_any) == 2, f"Expected 2 models with 'any' mode, got {len(models_any)}"
|
|
|
|
# Cleanup
|
|
await memory.delete_bank(bank_id, request_context=request_context)
|
|
|
|
async def test_tags_match_all_strict(self, memory: MemoryEngine, request_context):
|
|
"""Test that 'all_strict' requires ALL tags to be present."""
|
|
bank_id = f"test-mm-tags-all-{uuid.uuid4().hex[:8]}"
|
|
|
|
# Ensure bank exists
|
|
await memory.get_bank_profile(bank_id, request_context=request_context)
|
|
|
|
# Create a model with multiple tags
|
|
await memory.create_mental_model(
|
|
bank_id=bank_id,
|
|
name="Multi-Tag Model",
|
|
description="Has project-a and project-b tags",
|
|
subtype="pinned",
|
|
tags=["project-a", "project-b"],
|
|
request_context=request_context,
|
|
)
|
|
|
|
# Create a model with only one tag
|
|
await memory.create_mental_model(
|
|
bank_id=bank_id,
|
|
name="Single-Tag Model",
|
|
description="Has only project-a tag",
|
|
subtype="pinned",
|
|
tags=["project-a"],
|
|
request_context=request_context,
|
|
)
|
|
|
|
# Test 1: all_strict with both tags - should get ONLY the multi-tag model
|
|
models_all = await memory.list_mental_models(
|
|
bank_id=bank_id,
|
|
tags=["project-a", "project-b"],
|
|
tags_match="all_strict",
|
|
request_context=request_context,
|
|
)
|
|
assert len(models_all) == 1, f"Expected 1 model with all tags, got {len(models_all)}"
|
|
assert models_all[0]["name"] == "Multi-Tag Model"
|
|
|
|
# Test 2: all (non-strict) with both tags - should include untagged too
|
|
# Add an untagged model
|
|
await memory.create_mental_model(
|
|
bank_id=bank_id,
|
|
name="Untagged Model",
|
|
description="No tags",
|
|
subtype="pinned",
|
|
tags=[],
|
|
request_context=request_context,
|
|
)
|
|
|
|
models_all_non_strict = await memory.list_mental_models(
|
|
bank_id=bank_id,
|
|
tags=["project-a", "project-b"],
|
|
tags_match="all",
|
|
request_context=request_context,
|
|
)
|
|
# Should get Multi-Tag Model + Untagged Model
|
|
assert len(models_all_non_strict) == 2, f"Expected 2 models with 'all' mode, got {len(models_all_non_strict)}"
|
|
|
|
# Cleanup
|
|
await memory.delete_bank(bank_id, request_context=request_context)
|
|
|
|
|
|
class TestDirectivesPromptInjection:
|
|
"""Test that directives are properly injected into the system prompt."""
|
|
|
|
def test_build_directives_section_empty(self):
|
|
"""Test that empty directives returns empty string."""
|
|
from hindsight_api.engine.reflect.prompts import build_directives_section
|
|
|
|
result = build_directives_section([])
|
|
assert result == ""
|
|
|
|
def test_build_directives_section_with_observations(self):
|
|
"""Test that directives with observations are formatted correctly."""
|
|
from hindsight_api.engine.reflect.prompts import build_directives_section
|
|
|
|
directives = [
|
|
{
|
|
"name": "Competitor Policy",
|
|
"observations": [
|
|
{"title": "Never mention", "content": "Never mention competitor names"},
|
|
{"title": "Redirect", "content": "Redirect to our features"},
|
|
],
|
|
}
|
|
]
|
|
|
|
result = build_directives_section(directives)
|
|
|
|
assert "## DIRECTIVES (MANDATORY)" in result
|
|
assert "**Never mention**: Never mention competitor names" in result
|
|
assert "**Redirect**: Redirect to our features" in result
|
|
assert "NEVER violate these directives" in result
|
|
|
|
def test_build_directives_section_fallback_to_description(self):
|
|
"""Test that directives without observations fall back to description."""
|
|
from hindsight_api.engine.reflect.prompts import build_directives_section
|
|
|
|
directives = [
|
|
{
|
|
"name": "Simple Rule",
|
|
"description": "Just a simple rule description",
|
|
"observations": [],
|
|
}
|
|
]
|
|
|
|
result = build_directives_section(directives)
|
|
|
|
assert "**Simple Rule**: Just a simple rule description" in result
|
|
|
|
def test_system_prompt_includes_directives(self):
|
|
"""Test that build_system_prompt_for_tools includes directives."""
|
|
from hindsight_api.engine.reflect.prompts import build_system_prompt_for_tools
|
|
|
|
bank_profile = {"name": "Test Bank", "mission": "Test mission"}
|
|
directives = [
|
|
{
|
|
"name": "Test Directive",
|
|
"observations": [{"title": "Rule", "content": "Follow this rule"}],
|
|
}
|
|
]
|
|
|
|
prompt = build_system_prompt_for_tools(
|
|
bank_profile=bank_profile,
|
|
directives=directives,
|
|
)
|
|
|
|
assert "## DIRECTIVES (MANDATORY)" in prompt
|
|
assert "**Rule**: Follow this rule" in prompt
|
|
# Directives should appear before CRITICAL RULES
|
|
directives_pos = prompt.find("## DIRECTIVES")
|
|
critical_rules_pos = prompt.find("## CRITICAL RULES")
|
|
assert directives_pos < critical_rules_pos
|
|
|
|
|
|
class TestMentalModelVersioning:
|
|
"""Test mental model versioning functionality."""
|
|
|
|
async def test_refresh_creates_version(self, memory_with_mission, request_context):
|
|
"""Test that refreshing a mental model creates a version entry."""
|
|
memory, bank_id = memory_with_mission
|
|
|
|
# First create a mental model via refresh_mental_models
|
|
await memory.refresh_mental_models(
|
|
bank_id=bank_id,
|
|
request_context=request_context,
|
|
)
|
|
await memory.wait_for_background_tasks()
|
|
|
|
# Get the created models
|
|
models = await memory.list_mental_models(
|
|
bank_id=bank_id,
|
|
request_context=request_context,
|
|
)
|
|
assert len(models) > 0
|
|
|
|
model_id = models[0]["id"]
|
|
|
|
# Refresh the specific model to trigger versioning
|
|
result = await memory.refresh_mental_model(
|
|
bank_id=bank_id,
|
|
model_id=model_id,
|
|
request_context=request_context,
|
|
)
|
|
|
|
assert result is not None
|
|
# Version should be incremented
|
|
assert result.get("version", 0) >= 1
|
|
|
|
# Check version history
|
|
versions = await memory.get_mental_model_versions(
|
|
bank_id=bank_id,
|
|
model_id=model_id,
|
|
request_context=request_context,
|
|
)
|
|
|
|
assert len(versions) >= 1
|
|
assert versions[0]["version"] >= 1
|
|
assert "created_at" in versions[0]
|
|
assert "observation_count" in versions[0]
|
|
|
|
async def test_get_specific_version(self, memory_with_mission, request_context):
|
|
"""Test retrieving a specific version of a mental model."""
|
|
memory, bank_id = memory_with_mission
|
|
|
|
# Create and refresh a mental model
|
|
await memory.refresh_mental_models(
|
|
bank_id=bank_id,
|
|
request_context=request_context,
|
|
)
|
|
await memory.wait_for_background_tasks()
|
|
|
|
models = await memory.list_mental_models(
|
|
bank_id=bank_id,
|
|
request_context=request_context,
|
|
)
|
|
assert len(models) > 0
|
|
|
|
model_id = models[0]["id"]
|
|
|
|
# Refresh to create version
|
|
await memory.refresh_mental_model(
|
|
bank_id=bank_id,
|
|
model_id=model_id,
|
|
request_context=request_context,
|
|
)
|
|
|
|
# Get versions
|
|
versions = await memory.get_mental_model_versions(
|
|
bank_id=bank_id,
|
|
model_id=model_id,
|
|
request_context=request_context,
|
|
)
|
|
assert len(versions) >= 1
|
|
|
|
# Get specific version
|
|
version_num = versions[0]["version"]
|
|
version_data = await memory.get_mental_model_version(
|
|
bank_id=bank_id,
|
|
model_id=model_id,
|
|
version=version_num,
|
|
request_context=request_context,
|
|
)
|
|
|
|
assert version_data is not None
|
|
assert version_data["version"] == version_num
|
|
assert "observations" in version_data
|
|
|
|
async def test_version_cleanup_keeps_max_versions(self, memory_with_mission, request_context):
|
|
"""Test that old versions are cleaned up when max is exceeded."""
|
|
memory, bank_id = memory_with_mission
|
|
|
|
# Create a mental model
|
|
await memory.refresh_mental_models(
|
|
bank_id=bank_id,
|
|
request_context=request_context,
|
|
)
|
|
await memory.wait_for_background_tasks()
|
|
|
|
models = await memory.list_mental_models(
|
|
bank_id=bank_id,
|
|
request_context=request_context,
|
|
)
|
|
assert len(models) > 0
|
|
|
|
model_id = models[0]["id"]
|
|
|
|
# Refresh multiple times to create versions
|
|
for _ in range(3):
|
|
await memory.refresh_mental_model(
|
|
bank_id=bank_id,
|
|
model_id=model_id,
|
|
request_context=request_context,
|
|
)
|
|
|
|
# Get versions - should have multiple but within max limit
|
|
versions = await memory.get_mental_model_versions(
|
|
bank_id=bank_id,
|
|
model_id=model_id,
|
|
request_context=request_context,
|
|
)
|
|
|
|
# Should have versions (exact count depends on config, but at least some)
|
|
assert len(versions) >= 1
|
|
# Versions should be in descending order
|
|
if len(versions) > 1:
|
|
assert versions[0]["version"] > versions[1]["version"]
|
|
|