import uuid import pytest # Import shared fixtures and constants from conftest from conftest import ( DEFAULT_EMBEDDING_CONFIG, ) from letta.errors import LettaAgentNotFoundError from letta.orm.errors import NoResultFound from letta.schemas.agent import CreateAgent from letta.schemas.embedding_config import EmbeddingConfig from letta.schemas.llm_config import LLMConfig from letta.schemas.passage import Passage as PydanticPassage from letta.server.server import SyncServer # ====================================================================================================================== # Archive Manager Tests # ====================================================================================================================== @pytest.mark.asyncio async def test_archive_manager_delete_archive_async(server: SyncServer, default_user): """Test the delete_archive_async function.""" archive = await server.archive_manager.create_archive_async( name="test_archive_to_delete", description="This archive will be deleted", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user, ) retrieved_archive = await server.archive_manager.get_archive_by_id_async(archive_id=archive.id, actor=default_user) assert retrieved_archive.id == archive.id await server.archive_manager.delete_archive_async(archive_id=archive.id, actor=default_user) with pytest.raises(Exception): await server.archive_manager.get_archive_by_id_async(archive_id=archive.id, actor=default_user) @pytest.mark.asyncio async def test_archive_manager_get_agents_for_archive_async(server: SyncServer, default_user, sarah_agent): """Test getting all agents that have access to an archive.""" archive = await server.archive_manager.create_archive_async( name="shared_archive", description="Archive shared by multiple agents", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user, ) agent2 = await server.agent_manager.create_agent_async( agent_create=CreateAgent( name="test_agent_2", agent_type="memgpt_v2_agent", memory_blocks=[], llm_config=LLMConfig.default_config("gpt-4o-mini"), embedding_config=EmbeddingConfig.default_config(provider="openai"), include_base_tools=False, ), actor=default_user, ) await server.archive_manager.attach_agent_to_archive_async( agent_id=sarah_agent.id, archive_id=archive.id, is_owner=True, actor=default_user ) await server.archive_manager.attach_agent_to_archive_async( agent_id=agent2.id, archive_id=archive.id, is_owner=False, actor=default_user ) agents = await server.archive_manager.get_agents_for_archive_async(archive_id=archive.id, actor=default_user) assert len(agents) == 2 agent_ids = [a.id for a in agents] assert sarah_agent.id in agent_ids assert agent2.id in agent_ids # Cleanup await server.agent_manager.delete_agent_async(agent2.id, actor=default_user) await server.archive_manager.delete_archive_async(archive.id, actor=default_user) @pytest.mark.asyncio async def test_archive_manager_race_condition_handling(server: SyncServer, default_user, sarah_agent): """Test that the race condition fix in get_or_create_default_archive_for_agent_async works.""" from unittest.mock import patch from sqlalchemy.exc import IntegrityError agent = await server.agent_manager.create_agent_async( agent_create=CreateAgent( name="test_agent_race_condition", agent_type="memgpt_v2_agent", memory_blocks=[], llm_config=LLMConfig.default_config("gpt-4o-mini"), embedding_config=EmbeddingConfig.default_config(provider="openai"), include_base_tools=False, ), actor=default_user, ) created_archives = [] original_create = server.archive_manager.create_archive_async async def track_create(*args, **kwargs): result = await original_create(*args, **kwargs) created_archives.append(result) return result # First, create an archive that will be attached by a "concurrent" request concurrent_archive = await server.archive_manager.create_archive_async( name=f"{agent.name}'s Archive", description="Default archive created automatically", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user, ) call_count = 0 original_attach = server.archive_manager.attach_agent_to_archive_async async def failing_attach(*args, **kwargs): nonlocal call_count call_count += 1 if call_count == 1: # Simulate another request already attached the agent to an archive await original_attach(agent_id=agent.id, archive_id=concurrent_archive.id, is_owner=True, actor=default_user) # Now raise the IntegrityError as if our attempt failed raise IntegrityError("duplicate key value violates unique constraint", None, None) # This shouldn't be called since we already have an archive raise Exception("Should not reach here") with patch.object(server.archive_manager, "create_archive_async", side_effect=track_create): with patch.object(server.archive_manager, "attach_agent_to_archive_async", side_effect=failing_attach): archive = await server.archive_manager.get_or_create_default_archive_for_agent_async(agent_state=agent, actor=default_user) assert archive is not None assert archive.id == concurrent_archive.id # Should return the existing archive assert archive.name == f"{agent.name}'s Archive" # One archive was created in our attempt (but then deleted) assert len(created_archives) == 1 # Verify only one archive is attached to the agent archive_ids = await server.agent_manager.get_agent_archive_ids_async(agent_id=agent.id, actor=default_user) assert len(archive_ids) == 1 assert archive_ids[0] == concurrent_archive.id # Cleanup await server.agent_manager.delete_agent_async(agent.id, actor=default_user) await server.archive_manager.delete_archive_async(concurrent_archive.id, actor=default_user) @pytest.mark.asyncio async def test_archive_manager_get_agent_from_passage_async(server: SyncServer, default_user, sarah_agent): """Test getting the agent ID that owns a passage through its archive.""" archive = await server.archive_manager.get_or_create_default_archive_for_agent_async(agent_state=sarah_agent, actor=default_user) passage = await server.passage_manager.create_agent_passage_async( PydanticPassage( text="Test passage for agent ownership", archive_id=archive.id, organization_id=default_user.organization_id, embedding=[0.1], embedding_config=DEFAULT_EMBEDDING_CONFIG, ), actor=default_user, ) agent_id = await server.archive_manager.get_agent_from_passage_async(passage_id=passage.id, actor=default_user) assert agent_id == sarah_agent.id orphan_archive = await server.archive_manager.create_archive_async( name="orphan_archive", description="Archive with no agents", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user ) orphan_passage = await server.passage_manager.create_agent_passage_async( PydanticPassage( text="Orphan passage", archive_id=orphan_archive.id, organization_id=default_user.organization_id, embedding=[0.1], embedding_config=DEFAULT_EMBEDDING_CONFIG, ), actor=default_user, ) agent_id = await server.archive_manager.get_agent_from_passage_async(passage_id=orphan_passage.id, actor=default_user) assert agent_id is None # Cleanup await server.passage_manager.delete_passage_by_id_async(passage.id, actor=default_user) await server.passage_manager.delete_passage_by_id_async(orphan_passage.id, actor=default_user) await server.archive_manager.delete_archive_async(orphan_archive.id, actor=default_user) @pytest.mark.asyncio async def test_archive_manager_create_archive_async(server: SyncServer, default_user): """Test creating a new archive with various parameters.""" # test creating with name and description archive = await server.archive_manager.create_archive_async( name="test_archive_basic", description="Test archive description", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user ) assert archive.name == "test_archive_basic" assert archive.description == "Test archive description" assert archive.organization_id == default_user.organization_id assert archive.id is not None # test creating without description archive2 = await server.archive_manager.create_archive_async( name="test_archive_no_desc", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user ) assert archive2.name == "test_archive_no_desc" assert archive2.description is None assert archive2.organization_id == default_user.organization_id # cleanup await server.archive_manager.delete_archive_async(archive.id, actor=default_user) await server.archive_manager.delete_archive_async(archive2.id, actor=default_user) @pytest.mark.asyncio async def test_archive_manager_get_archive_by_id_async(server: SyncServer, default_user): """Test retrieving an archive by its ID.""" # create an archive archive = await server.archive_manager.create_archive_async( name="test_get_by_id", description="Archive to test get_by_id", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user ) # retrieve the archive retrieved = await server.archive_manager.get_archive_by_id_async(archive_id=archive.id, actor=default_user) assert retrieved.id == archive.id assert retrieved.name == "test_get_by_id" assert retrieved.description == "Archive to test get_by_id" assert retrieved.organization_id == default_user.organization_id # cleanup await server.archive_manager.delete_archive_async(archive.id, actor=default_user) # test getting non-existent archive should raise with pytest.raises(Exception): await server.archive_manager.get_archive_by_id_async(archive_id=str(uuid.uuid4()), actor=default_user) @pytest.mark.asyncio async def test_archive_manager_update_archive_async(server: SyncServer, default_user): """Test updating archive name and description.""" # create an archive archive = await server.archive_manager.create_archive_async( name="original_name", description="original description", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user ) # update name only updated = await server.archive_manager.update_archive_async(archive_id=archive.id, name="updated_name", actor=default_user) assert updated.id == archive.id assert updated.name == "updated_name" assert updated.description == "original description" # update description only updated = await server.archive_manager.update_archive_async( archive_id=archive.id, description="updated description", actor=default_user ) assert updated.name == "updated_name" assert updated.description == "updated description" # update both updated = await server.archive_manager.update_archive_async( archive_id=archive.id, name="final_name", description="final description", actor=default_user ) assert updated.name == "final_name" assert updated.description == "final description" # verify changes persisted retrieved = await server.archive_manager.get_archive_by_id_async(archive_id=archive.id, actor=default_user) assert retrieved.name == "final_name" assert retrieved.description == "final description" # cleanup await server.archive_manager.delete_archive_async(archive.id, actor=default_user) @pytest.mark.asyncio async def test_archive_manager_list_archives_async(server: SyncServer, default_user, sarah_agent): """Test listing archives with various filters and pagination.""" # create test archives archives = [] for i in range(5): archive = await server.archive_manager.create_archive_async( name=f"list_test_archive_{i}", description=f"Description {i}", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user ) archives.append(archive) # test basic listing result = await server.archive_manager.list_archives_async(actor=default_user, limit=10) assert len(result) >= 5 # test with limit result = await server.archive_manager.list_archives_async(actor=default_user, limit=3) assert len(result) == 3 # test filtering by name result = await server.archive_manager.list_archives_async(actor=default_user, name="list_test_archive_2") assert len(result) == 1 assert result[0].name == "list_test_archive_2" # attach an archive to agent and test agent_id filter await server.archive_manager.attach_agent_to_archive_async( agent_id=sarah_agent.id, archive_id=archives[0].id, is_owner=True, actor=default_user ) result = await server.archive_manager.list_archives_async(actor=default_user, agent_id=sarah_agent.id) assert len(result) >= 1 assert archives[0].id in [a.id for a in result] # test pagination with after all_archives = await server.archive_manager.list_archives_async(actor=default_user, limit=100) if len(all_archives) > 2: first_batch = await server.archive_manager.list_archives_async(actor=default_user, limit=2) second_batch = await server.archive_manager.list_archives_async(actor=default_user, after=first_batch[-1].id, limit=2) assert len(second_batch) <= 2 assert first_batch[-1].id not in [a.id for a in second_batch] # cleanup for archive in archives: await server.archive_manager.delete_archive_async(archive.id, actor=default_user) @pytest.mark.asyncio async def test_archive_manager_attach_agent_to_archive_async(server: SyncServer, default_user, sarah_agent): """Test attaching agents to archives with ownership settings.""" # create archives archive1 = await server.archive_manager.create_archive_async( name="archive_for_attachment_1", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user ) archive2 = await server.archive_manager.create_archive_async( name="archive_for_attachment_2", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user ) # create another agent agent2 = await server.agent_manager.create_agent_async( agent_create=CreateAgent( name="test_attach_agent", agent_type="memgpt_v2_agent", memory_blocks=[], llm_config=LLMConfig.default_config("gpt-4o-mini"), embedding_config=EmbeddingConfig.default_config(provider="openai"), include_base_tools=False, ), actor=default_user, ) # attach agent as owner await server.archive_manager.attach_agent_to_archive_async( agent_id=sarah_agent.id, archive_id=archive1.id, is_owner=True, actor=default_user ) # verify attachment agents = await server.archive_manager.get_agents_for_archive_async(archive_id=archive1.id, actor=default_user) assert sarah_agent.id in [a.id for a in agents] # attach agent as non-owner await server.archive_manager.attach_agent_to_archive_async( agent_id=agent2.id, archive_id=archive1.id, is_owner=False, actor=default_user ) agents = await server.archive_manager.get_agents_for_archive_async(archive_id=archive1.id, actor=default_user) assert len(agents) == 2 assert agent2.id in [a.id for a in agents] # test updating ownership (attach again with different is_owner) await server.archive_manager.attach_agent_to_archive_async( agent_id=agent2.id, archive_id=archive1.id, is_owner=True, actor=default_user ) # verify still only 2 agents (no duplicate) agents = await server.archive_manager.get_agents_for_archive_async(archive_id=archive1.id, actor=default_user) assert len(agents) == 2 # cleanup await server.agent_manager.delete_agent_async(agent2.id, actor=default_user) await server.archive_manager.delete_archive_async(archive1.id, actor=default_user) await server.archive_manager.delete_archive_async(archive2.id, actor=default_user) @pytest.mark.asyncio async def test_archive_manager_detach_agent_from_archive_async(server: SyncServer, default_user): """Test detaching agents from archives.""" # create archive and agents archive = await server.archive_manager.create_archive_async( name="archive_for_detachment", description="Test archive for detachment", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user, ) agent1 = await server.agent_manager.create_agent_async( agent_create=CreateAgent( name="test_detach_agent_1", agent_type="memgpt_v2_agent", memory_blocks=[], llm_config=LLMConfig.default_config("gpt-4o-mini"), embedding_config=EmbeddingConfig.default_config(provider="openai"), include_base_tools=False, ), actor=default_user, ) agent2 = await server.agent_manager.create_agent_async( agent_create=CreateAgent( name="test_detach_agent_2", agent_type="memgpt_v2_agent", memory_blocks=[], llm_config=LLMConfig.default_config("gpt-4o-mini"), embedding_config=EmbeddingConfig.default_config(provider="openai"), include_base_tools=False, ), actor=default_user, ) # attach both agents await server.archive_manager.attach_agent_to_archive_async(agent_id=agent1.id, archive_id=archive.id, is_owner=True, actor=default_user) await server.archive_manager.attach_agent_to_archive_async( agent_id=agent2.id, archive_id=archive.id, is_owner=False, actor=default_user ) # verify both are attached agents = await server.archive_manager.get_agents_for_archive_async(archive_id=archive.id, actor=default_user) assert len(agents) == 2 agent_ids = [a.id for a in agents] assert agent1.id in agent_ids assert agent2.id in agent_ids # detach agent1 await server.archive_manager.detach_agent_from_archive_async(agent_id=agent1.id, archive_id=archive.id, actor=default_user) # verify only agent2 remains agents = await server.archive_manager.get_agents_for_archive_async(archive_id=archive.id, actor=default_user) assert len(agents) == 1 agent_ids = [a.id for a in agents] assert agent2.id in agent_ids assert agent1.id not in agent_ids # test idempotency - detach agent1 again (should not error) await server.archive_manager.detach_agent_from_archive_async(agent_id=agent1.id, archive_id=archive.id, actor=default_user) # verify still only agent2 agents = await server.archive_manager.get_agents_for_archive_async(archive_id=archive.id, actor=default_user) assert len(agents) == 1 assert agent2.id in [a.id for a in agents] # detach agent2 await server.archive_manager.detach_agent_from_archive_async(agent_id=agent2.id, archive_id=archive.id, actor=default_user) # verify archive has no agents agents = await server.archive_manager.get_agents_for_archive_async(archive_id=archive.id, actor=default_user) assert len(agents) == 0 # cleanup await server.agent_manager.delete_agent_async(agent1.id, actor=default_user) await server.agent_manager.delete_agent_async(agent2.id, actor=default_user) await server.archive_manager.delete_archive_async(archive.id, actor=default_user) @pytest.mark.asyncio async def test_archive_manager_attach_detach_idempotency(server: SyncServer, default_user): """Test that attach and detach operations are idempotent.""" # create archive and agent archive = await server.archive_manager.create_archive_async( name="idempotency_test_archive", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user ) agent = await server.agent_manager.create_agent_async( agent_create=CreateAgent( name="idempotency_test_agent", agent_type="memgpt_v2_agent", memory_blocks=[], llm_config=LLMConfig.default_config("gpt-4o-mini"), embedding_config=EmbeddingConfig.default_config(provider="openai"), include_base_tools=False, ), actor=default_user, ) # test multiple attachments - should be idempotent await server.archive_manager.attach_agent_to_archive_async(agent_id=agent.id, archive_id=archive.id, is_owner=False, actor=default_user) await server.archive_manager.attach_agent_to_archive_async(agent_id=agent.id, archive_id=archive.id, is_owner=False, actor=default_user) # verify only one relationship exists agents = await server.archive_manager.get_agents_for_archive_async(archive_id=archive.id, actor=default_user) assert len(agents) == 1 assert agent.id in [a.id for a in agents] # test ownership update through re-attachment await server.archive_manager.attach_agent_to_archive_async(agent_id=agent.id, archive_id=archive.id, is_owner=True, actor=default_user) # still only one relationship agents = await server.archive_manager.get_agents_for_archive_async(archive_id=archive.id, actor=default_user) assert len(agents) == 1 # test detaching non-existent relationship (should be idempotent) non_existent_agent = await server.agent_manager.create_agent_async( agent_create=CreateAgent( name="never_attached_agent", agent_type="memgpt_v2_agent", memory_blocks=[], llm_config=LLMConfig.default_config("gpt-4o-mini"), embedding_config=EmbeddingConfig.default_config(provider="openai"), include_base_tools=False, ), actor=default_user, ) # this should not error await server.archive_manager.detach_agent_from_archive_async(agent_id=non_existent_agent.id, archive_id=archive.id, actor=default_user) # verify original agent still attached agents = await server.archive_manager.get_agents_for_archive_async(archive_id=archive.id, actor=default_user) assert len(agents) == 1 assert agent.id in [a.id for a in agents] # cleanup await server.agent_manager.delete_agent_async(agent.id, actor=default_user) await server.agent_manager.delete_agent_async(non_existent_agent.id, actor=default_user) await server.archive_manager.delete_archive_async(archive.id, actor=default_user) @pytest.mark.asyncio async def test_archive_manager_detach_with_multiple_archives(server: SyncServer, default_user): """Test detaching an agent from one archive doesn't affect others.""" # create two archives archive1 = await server.archive_manager.create_archive_async( name="multi_archive_1", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user ) archive2 = await server.archive_manager.create_archive_async( name="multi_archive_2", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user ) # create two agents agent1 = await server.agent_manager.create_agent_async( agent_create=CreateAgent( name="multi_test_agent_1", agent_type="memgpt_v2_agent", memory_blocks=[], llm_config=LLMConfig.default_config("gpt-4o-mini"), embedding_config=EmbeddingConfig.default_config(provider="openai"), include_base_tools=False, ), actor=default_user, ) agent2 = await server.agent_manager.create_agent_async( agent_create=CreateAgent( name="multi_test_agent_2", agent_type="memgpt_v2_agent", memory_blocks=[], llm_config=LLMConfig.default_config("gpt-4o-mini"), embedding_config=EmbeddingConfig.default_config(provider="openai"), include_base_tools=False, ), actor=default_user, ) # Note: Due to unique constraint, each agent can only be attached to one archive # So we'll attach different agents to different archives await server.archive_manager.attach_agent_to_archive_async( agent_id=agent1.id, archive_id=archive1.id, is_owner=True, actor=default_user ) await server.archive_manager.attach_agent_to_archive_async( agent_id=agent2.id, archive_id=archive2.id, is_owner=True, actor=default_user ) # verify initial state agents_archive1 = await server.archive_manager.get_agents_for_archive_async(archive_id=archive1.id, actor=default_user) agents_archive2 = await server.archive_manager.get_agents_for_archive_async(archive_id=archive2.id, actor=default_user) assert agent1.id in [a.id for a in agents_archive1] assert agent2.id in [a.id for a in agents_archive2] # detach agent1 from archive1 await server.archive_manager.detach_agent_from_archive_async(agent_id=agent1.id, archive_id=archive1.id, actor=default_user) # verify agent1 is detached from archive1 agents_archive1 = await server.archive_manager.get_agents_for_archive_async(archive_id=archive1.id, actor=default_user) assert agent1.id not in [a.id for a in agents_archive1] assert len(agents_archive1) == 0 # verify agent2 is still attached to archive2 agents_archive2 = await server.archive_manager.get_agents_for_archive_async(archive_id=archive2.id, actor=default_user) assert agent2.id in [a.id for a in agents_archive2] assert len(agents_archive2) == 1 # cleanup await server.agent_manager.delete_agent_async(agent1.id, actor=default_user) await server.agent_manager.delete_agent_async(agent2.id, actor=default_user) await server.archive_manager.delete_archive_async(archive1.id, actor=default_user) await server.archive_manager.delete_archive_async(archive2.id, actor=default_user) @pytest.mark.asyncio async def test_archive_manager_detach_deleted_agent(server: SyncServer, default_user): """Test behavior when detaching a deleted agent.""" # create archive archive = await server.archive_manager.create_archive_async( name="test_deleted_agent_archive", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user ) # create and attach agent agent = await server.agent_manager.create_agent_async( agent_create=CreateAgent( name="agent_to_be_deleted", agent_type="memgpt_v2_agent", memory_blocks=[], llm_config=LLMConfig.default_config("gpt-4o-mini"), embedding_config=EmbeddingConfig.default_config(provider="openai"), include_base_tools=False, ), actor=default_user, ) await server.archive_manager.attach_agent_to_archive_async(agent_id=agent.id, archive_id=archive.id, is_owner=True, actor=default_user) # save the agent id before deletion agent_id = agent.id # delete the agent (should cascade delete the relationship due to ondelete="CASCADE") await server.agent_manager.delete_agent_async(agent.id, actor=default_user) # verify agent is no longer attached agents = await server.archive_manager.get_agents_for_archive_async(archive_id=archive.id, actor=default_user) assert len(agents) == 0 # attempting to detach the deleted agent # 2025-10-27: used to be idempotent (no error) but now we raise an error with pytest.raises(LettaAgentNotFoundError): await server.archive_manager.detach_agent_from_archive_async(agent_id=agent_id, archive_id=archive.id, actor=default_user) # cleanup await server.archive_manager.delete_archive_async(archive.id, actor=default_user) @pytest.mark.asyncio async def test_archive_manager_cascade_delete_on_archive_deletion(server: SyncServer, default_user): """Test that deleting an archive cascades to delete relationships in archives_agents table.""" # create archive archive = await server.archive_manager.create_archive_async( name="archive_to_be_deleted", description="This archive will be deleted to test CASCADE", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user, ) # create multiple agents and attach them to the archive agent1 = await server.agent_manager.create_agent_async( agent_create=CreateAgent( name="cascade_test_agent_1", agent_type="memgpt_v2_agent", memory_blocks=[], llm_config=LLMConfig.default_config("gpt-4o-mini"), embedding_config=EmbeddingConfig.default_config(provider="openai"), include_base_tools=False, ), actor=default_user, ) agent2 = await server.agent_manager.create_agent_async( agent_create=CreateAgent( name="cascade_test_agent_2", agent_type="memgpt_v2_agent", memory_blocks=[], llm_config=LLMConfig.default_config("gpt-4o-mini"), embedding_config=EmbeddingConfig.default_config(provider="openai"), include_base_tools=False, ), actor=default_user, ) # attach both agents to the archive await server.archive_manager.attach_agent_to_archive_async(agent_id=agent1.id, archive_id=archive.id, is_owner=True, actor=default_user) await server.archive_manager.attach_agent_to_archive_async( agent_id=agent2.id, archive_id=archive.id, is_owner=False, actor=default_user ) # verify both agents are attached agents = await server.archive_manager.get_agents_for_archive_async(archive_id=archive.id, actor=default_user) assert len(agents) == 2 agent_ids = [a.id for a in agents] assert agent1.id in agent_ids assert agent2.id in agent_ids # save archive id for later archive_id = archive.id # delete the archive (should cascade delete the relationships) await server.archive_manager.delete_archive_async(archive.id, actor=default_user) # verify archive is deleted with pytest.raises(Exception): await server.archive_manager.get_archive_by_id_async(archive_id=archive_id, actor=default_user) # verify agents still exist but have no archives attached # (agents should NOT be deleted, only the relationships) agent1_still_exists = await server.agent_manager.get_agent_by_id_async(agent1.id, actor=default_user) assert agent1_still_exists is not None assert agent1_still_exists.id == agent1.id agent2_still_exists = await server.agent_manager.get_agent_by_id_async(agent2.id, actor=default_user) assert agent2_still_exists is not None assert agent2_still_exists.id == agent2.id # verify agents no longer have any archives agent1_archives = await server.agent_manager.get_agent_archive_ids_async(agent_id=agent1.id, actor=default_user) assert len(agent1_archives) == 0 agent2_archives = await server.agent_manager.get_agent_archive_ids_async(agent_id=agent2.id, actor=default_user) assert len(agent2_archives) == 0 # cleanup agents await server.agent_manager.delete_agent_async(agent1.id, actor=default_user) await server.agent_manager.delete_agent_async(agent2.id, actor=default_user) @pytest.mark.asyncio async def test_archive_manager_list_agents_with_pagination(server: SyncServer, default_user): """Test listing agents for an archive with pagination support.""" # create archive archive = await server.archive_manager.create_archive_async( name="pagination_test_archive", description="Archive for testing pagination", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user, ) # create multiple agents agents = [] for i in range(5): agent = await server.agent_manager.create_agent_async( agent_create=CreateAgent( name=f"pagination_test_agent_{i}", agent_type="memgpt_v2_agent", memory_blocks=[], llm_config=LLMConfig.default_config("gpt-4o-mini"), embedding_config=EmbeddingConfig.default_config(provider="openai"), include_base_tools=False, ), actor=default_user, ) agents.append(agent) # Attach to archive await server.archive_manager.attach_agent_to_archive_async( agent_id=agent.id, archive_id=archive.id, is_owner=(i == 0), actor=default_user ) # Test basic listing (should get all 5) all_agents = await server.archive_manager.get_agents_for_archive_async(archive_id=archive.id, actor=default_user, limit=10) assert len(all_agents) == 5 all_agent_ids = [a.id for a in all_agents] for agent in agents: assert agent.id in all_agent_ids # Test with limit limited_agents = await server.archive_manager.get_agents_for_archive_async(archive_id=archive.id, actor=default_user, limit=3) assert len(limited_agents) == 3 # Test that pagination parameters are accepted without errors paginated = await server.archive_manager.get_agents_for_archive_async(archive_id=archive.id, actor=default_user, limit=2) assert len(paginated) == 2 assert all(a.id in all_agent_ids for a in paginated) # Test ascending/descending order by checking we get all agents in both ascending_agents = await server.archive_manager.get_agents_for_archive_async( archive_id=archive.id, actor=default_user, ascending=True, limit=10 ) assert len(ascending_agents) == 5 descending_agents = await server.archive_manager.get_agents_for_archive_async( archive_id=archive.id, actor=default_user, ascending=False, limit=10 ) assert len(descending_agents) == 5 # Verify both orders contain all agents assert set([a.id for a in ascending_agents]) == set([a.id for a in descending_agents]) # Cleanup for agent in agents: await server.agent_manager.delete_agent_async(agent.id, actor=default_user) await server.archive_manager.delete_archive_async(archive.id, actor=default_user) @pytest.mark.asyncio async def test_archive_manager_get_default_archive_for_agent_async(server: SyncServer, default_user): """Test getting default archive for an agent.""" # create agent without archive agent = await server.agent_manager.create_agent_async( agent_create=CreateAgent( name="test_default_archive_agent", agent_type="memgpt_v2_agent", memory_blocks=[], llm_config=LLMConfig.default_config("gpt-4o-mini"), embedding_config=EmbeddingConfig.default_config(provider="openai"), include_base_tools=False, ), actor=default_user, ) # should return None when no archive exists archive = await server.archive_manager.get_default_archive_for_agent_async(agent_id=agent.id, actor=default_user) assert archive is None # create and attach an archive created_archive = await server.archive_manager.create_archive_async( name="default_archive", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user ) await server.archive_manager.attach_agent_to_archive_async( agent_id=agent.id, archive_id=created_archive.id, is_owner=True, actor=default_user ) # should now return the archive archive = await server.archive_manager.get_default_archive_for_agent_async(agent_id=agent.id, actor=default_user) assert archive is not None assert archive.id == created_archive.id # cleanup await server.agent_manager.delete_agent_async(agent.id, actor=default_user) await server.archive_manager.delete_archive_async(created_archive.id, actor=default_user) @pytest.mark.asyncio async def test_archive_manager_get_or_set_vector_db_namespace_async(server: SyncServer, default_user): """Test getting or setting vector database namespace for an archive.""" # create an archive archive = await server.archive_manager.create_archive_async( name="test_vector_namespace", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user ) # get/set namespace for the first time namespace = await server.archive_manager.get_or_set_vector_db_namespace_async(archive_id=archive.id) assert namespace is not None assert archive.id in namespace # verify it returns the same namespace on subsequent calls namespace2 = await server.archive_manager.get_or_set_vector_db_namespace_async(archive_id=archive.id) assert namespace == namespace2 # cleanup await server.archive_manager.delete_archive_async(archive.id, actor=default_user) @pytest.mark.asyncio async def test_archive_manager_get_agents_with_include_parameter(server: SyncServer, default_user): """Test getting agents for an archive with include parameter to load relationships.""" # create an archive archive = await server.archive_manager.create_archive_async( name="test_include_archive", description="Test archive for include parameter", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user, ) # create agent without base tools (to avoid needing tools in test DB) agent = await server.agent_manager.create_agent_async( agent_create=CreateAgent( name="test_include_agent", agent_type="memgpt_v2_agent", memory_blocks=[], llm_config=LLMConfig.default_config("gpt-4o-mini"), embedding_config=EmbeddingConfig.default_config(provider="openai"), include_base_tools=False, ), actor=default_user, ) # attach agent to archive await server.archive_manager.attach_agent_to_archive_async(agent_id=agent.id, archive_id=archive.id, is_owner=True, actor=default_user) # test without include parameter (default - no relationships loaded) agents_no_include = await server.archive_manager.get_agents_for_archive_async(archive_id=archive.id, actor=default_user) assert len(agents_no_include) == 1 # By default, tools should be empty list (not loaded) assert agents_no_include[0].tools == [] # By default, tags should also be empty (not loaded) assert agents_no_include[0].tags == [] # test with include parameter to load tags agents_with_tags = await server.archive_manager.get_agents_for_archive_async( archive_id=archive.id, actor=default_user, include=["agent.tags"] ) assert len(agents_with_tags) == 1 # With include, tags should be loaded (as a list, even if empty) assert isinstance(agents_with_tags[0].tags, list) # test with include parameter to load blocks agents_with_blocks = await server.archive_manager.get_agents_for_archive_async( archive_id=archive.id, actor=default_user, include=["agent.blocks"] ) assert len(agents_with_blocks) == 1 # With include, blocks should be loaded assert isinstance(agents_with_blocks[0].blocks, list) # Agent should have blocks since we passed memory_blocks=[] which creates default blocks assert len(agents_with_blocks[0].blocks) >= 0 # test with multiple includes agents_with_multiple = await server.archive_manager.get_agents_for_archive_async( archive_id=archive.id, actor=default_user, include=["agent.tags", "agent.blocks", "agent.tools"] ) assert len(agents_with_multiple) == 1 # All requested relationships should be loaded assert isinstance(agents_with_multiple[0].tags, list) assert isinstance(agents_with_multiple[0].blocks, list) assert isinstance(agents_with_multiple[0].tools, list) # cleanup await server.agent_manager.delete_agent_async(agent.id, actor=default_user) await server.archive_manager.delete_archive_async(archive.id, actor=default_user) @pytest.mark.asyncio async def test_archive_manager_delete_passage_from_archive_async(server: SyncServer, default_user): """Test deleting a passage from an archive.""" # create archive archive = await server.archive_manager.create_archive_async( name="test_passage_deletion_archive", description="Archive for testing passage deletion", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user, ) # create passages passage1 = await server.passage_manager.create_agent_passage_async( PydanticPassage( text="First test passage", archive_id=archive.id, organization_id=default_user.organization_id, embedding=[0.1, 0.2], embedding_config=DEFAULT_EMBEDDING_CONFIG, ), actor=default_user, ) passage2 = await server.passage_manager.create_agent_passage_async( PydanticPassage( text="Second test passage", archive_id=archive.id, organization_id=default_user.organization_id, embedding=[0.3, 0.4], embedding_config=DEFAULT_EMBEDDING_CONFIG, ), actor=default_user, ) # verify both passages exist retrieved_passage1 = await server.passage_manager.get_agent_passage_by_id_async(passage_id=passage1.id, actor=default_user) assert retrieved_passage1.id == passage1.id assert retrieved_passage1.archive_id == archive.id retrieved_passage2 = await server.passage_manager.get_agent_passage_by_id_async(passage_id=passage2.id, actor=default_user) assert retrieved_passage2.id == passage2.id # delete passage1 from archive await server.archive_manager.delete_passage_from_archive_async(archive_id=archive.id, passage_id=passage1.id, actor=default_user) # verify passage1 is deleted with pytest.raises(NoResultFound): await server.passage_manager.get_agent_passage_by_id_async(passage_id=passage1.id, actor=default_user) # verify passage2 still exists retrieved_passage2 = await server.passage_manager.get_agent_passage_by_id_async(passage_id=passage2.id, actor=default_user) assert retrieved_passage2.id == passage2.id # cleanup await server.passage_manager.delete_agent_passage_by_id_async(passage2.id, actor=default_user) await server.archive_manager.delete_archive_async(archive.id, actor=default_user) @pytest.mark.asyncio async def test_archive_manager_delete_passage_from_wrong_archive(server: SyncServer, default_user): """Test that deleting a passage from the wrong archive raises an error.""" # create two archives archive1 = await server.archive_manager.create_archive_async( name="archive_1", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user ) archive2 = await server.archive_manager.create_archive_async( name="archive_2", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user ) # create passage in archive1 passage = await server.passage_manager.create_agent_passage_async( PydanticPassage( text="Passage in archive 1", archive_id=archive1.id, organization_id=default_user.organization_id, embedding=[0.1, 0.2], embedding_config=DEFAULT_EMBEDDING_CONFIG, ), actor=default_user, ) # attempt to delete passage from archive2 (wrong archive) with pytest.raises(ValueError, match="does not belong to archive"): await server.archive_manager.delete_passage_from_archive_async(archive_id=archive2.id, passage_id=passage.id, actor=default_user) # verify passage still exists retrieved_passage = await server.passage_manager.get_agent_passage_by_id_async(passage_id=passage.id, actor=default_user) assert retrieved_passage.id == passage.id # cleanup await server.passage_manager.delete_agent_passage_by_id_async(passage.id, actor=default_user) await server.archive_manager.delete_archive_async(archive1.id, actor=default_user) await server.archive_manager.delete_archive_async(archive2.id, actor=default_user) @pytest.mark.asyncio async def test_archive_manager_delete_nonexistent_passage(server: SyncServer, default_user): """Test that deleting a non-existent passage raises an error.""" # create archive archive = await server.archive_manager.create_archive_async( name="test_nonexistent_passage_archive", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user ) # attempt to delete non-existent passage (use valid UUID4 format) fake_passage_id = f"passage-{uuid.uuid4()}" with pytest.raises(NoResultFound): await server.archive_manager.delete_passage_from_archive_async( archive_id=archive.id, passage_id=fake_passage_id, actor=default_user ) # cleanup await server.archive_manager.delete_archive_async(archive.id, actor=default_user) @pytest.mark.asyncio async def test_archive_manager_delete_passage_from_nonexistent_archive(server: SyncServer, default_user): """Test that deleting a passage from a non-existent archive raises an error.""" # create archive and passage archive = await server.archive_manager.create_archive_async( name="temp_archive", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user ) passage = await server.passage_manager.create_agent_passage_async( PydanticPassage( text="Test passage", archive_id=archive.id, organization_id=default_user.organization_id, embedding=[0.1, 0.2], embedding_config=DEFAULT_EMBEDDING_CONFIG, ), actor=default_user, ) # attempt to delete passage from non-existent archive (use valid UUID4 format) fake_archive_id = f"archive-{uuid.uuid4()}" with pytest.raises(NoResultFound): await server.archive_manager.delete_passage_from_archive_async( archive_id=fake_archive_id, passage_id=passage.id, actor=default_user ) # verify passage still exists retrieved_passage = await server.passage_manager.get_agent_passage_by_id_async(passage_id=passage.id, actor=default_user) assert retrieved_passage.id == passage.id # cleanup await server.passage_manager.delete_agent_passage_by_id_async(passage.id, actor=default_user) await server.archive_manager.delete_archive_async(archive.id, actor=default_user) @pytest.mark.asyncio async def test_archive_manager_create_passage_in_archive_async(server: SyncServer, default_user): """Test creating a passage in an archive.""" # create archive archive = await server.archive_manager.create_archive_async( name="test_passage_creation_archive", description="Archive for testing passage creation", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user, ) # create a passage in the archive created_passage = await server.archive_manager.create_passage_in_archive_async( archive_id=archive.id, text="This is a test passage for creation", actor=default_user, ) # verify the passage was created assert created_passage.id is not None assert created_passage.text == "This is a test passage for creation" assert created_passage.archive_id == archive.id assert created_passage.organization_id == default_user.organization_id # verify we can retrieve it retrieved_passage = await server.passage_manager.get_agent_passage_by_id_async(passage_id=created_passage.id, actor=default_user) assert retrieved_passage.id == created_passage.id assert retrieved_passage.text == created_passage.text assert retrieved_passage.archive_id == archive.id # cleanup await server.passage_manager.delete_agent_passage_by_id_async(created_passage.id, actor=default_user) await server.archive_manager.delete_archive_async(archive.id, actor=default_user) @pytest.mark.asyncio async def test_archive_manager_create_passage_with_metadata_and_tags(server: SyncServer, default_user): """Test creating a passage with metadata and tags.""" # create archive archive = await server.archive_manager.create_archive_async( name="test_passage_metadata_archive", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user, ) # create passage with metadata and tags test_metadata = {"source": "unit_test", "version": 1} test_tags = ["test", "archive", "passage"] created_passage = await server.archive_manager.create_passage_in_archive_async( archive_id=archive.id, text="Passage with metadata and tags", metadata=test_metadata, tags=test_tags, actor=default_user, ) # verify metadata and tags were stored assert created_passage.metadata == test_metadata assert set(created_passage.tags) == set(test_tags) # Use set comparison to ignore order # retrieve and verify persistence retrieved_passage = await server.passage_manager.get_agent_passage_by_id_async(passage_id=created_passage.id, actor=default_user) assert retrieved_passage.metadata == test_metadata assert set(retrieved_passage.tags) == set(test_tags) # cleanup await server.passage_manager.delete_agent_passage_by_id_async(created_passage.id, actor=default_user) await server.archive_manager.delete_archive_async(archive.id, actor=default_user) @pytest.mark.asyncio async def test_archive_manager_create_passage_in_nonexistent_archive(server: SyncServer, default_user): """Test that creating a passage in a non-existent archive raises an error.""" # attempt to create passage in non-existent archive fake_archive_id = f"archive-{uuid.uuid4()}" with pytest.raises(NoResultFound): await server.archive_manager.create_passage_in_archive_async( archive_id=fake_archive_id, text="This should fail", actor=default_user, ) @pytest.mark.asyncio async def test_archive_manager_create_passage_inherits_embedding_config(server: SyncServer, default_user): """Test that created passages inherit the archive's embedding configuration.""" # create archive with specific embedding config specific_embedding_config = EmbeddingConfig.default_config(provider="openai") archive = await server.archive_manager.create_archive_async( name="test_embedding_inheritance_archive", embedding_config=specific_embedding_config, actor=default_user, ) # create passage created_passage = await server.archive_manager.create_passage_in_archive_async( archive_id=archive.id, text="Test passage for embedding config inheritance", actor=default_user, ) # verify the passage inherited the archive's embedding config assert created_passage.embedding_config is not None assert created_passage.embedding_config.embedding_endpoint_type == specific_embedding_config.embedding_endpoint_type assert created_passage.embedding_config.embedding_model == specific_embedding_config.embedding_model assert created_passage.embedding_config.embedding_dim == specific_embedding_config.embedding_dim # cleanup await server.passage_manager.delete_agent_passage_by_id_async(created_passage.id, actor=default_user) await server.archive_manager.delete_archive_async(archive.id, actor=default_user) @pytest.mark.asyncio async def test_archive_manager_create_multiple_passages_in_archive(server: SyncServer, default_user): """Test creating multiple passages in the same archive.""" # create archive archive = await server.archive_manager.create_archive_async( name="test_multiple_passages_archive", embedding_config=DEFAULT_EMBEDDING_CONFIG, actor=default_user, ) # create multiple passages passages = [] for i in range(3): passage = await server.archive_manager.create_passage_in_archive_async( archive_id=archive.id, text=f"Test passage number {i}", metadata={"index": i}, tags=[f"passage_{i}"], actor=default_user, ) passages.append(passage) # verify all passages were created with correct data for i, passage in enumerate(passages): assert passage.text == f"Test passage number {i}" assert passage.metadata["index"] == i assert f"passage_{i}" in passage.tags assert passage.archive_id == archive.id # cleanup for passage in passages: await server.passage_manager.delete_agent_passage_by_id_async(passage.id, actor=default_user) await server.archive_manager.delete_archive_async(archive.id, actor=default_user)