test_attachment_manager.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. """
  2. Tests for agency_swarm.agent.attachment_manager module.
  3. Tests the AttachmentManager class functionality including vector store management,
  4. file attachment handling, and cleanup operations.
  5. """
  6. from unittest.mock import Mock
  7. import pytest
  8. from agents import CodeInterpreterTool
  9. from agents.exceptions import AgentsException
  10. from agency_swarm.agent.attachment_manager import AttachmentManager
  11. class TestAttachmentManager:
  12. """Test AttachmentManager class functionality."""
  13. def test_init_without_file_manager(self):
  14. """Test AttachmentManager initialization when agent has no file_manager."""
  15. # Create a mock agent without file_manager
  16. mock_agent = Mock()
  17. mock_agent.name = "TestAgent"
  18. mock_agent.file_manager = None
  19. # Should raise AgentsException on line 48
  20. with pytest.raises(
  21. AgentsException, match="Cannot use AttachmentManager for agent TestAgent without file manager"
  22. ):
  23. AttachmentManager(mock_agent)
  24. def test_init_attachments_vs_existing_vector_store(self):
  25. """Test init_attachments_vs when vector store already exists."""
  26. # Setup mock agent with file_manager
  27. mock_agent = Mock()
  28. mock_agent.name = "TestAgent"
  29. mock_agent.file_manager = Mock()
  30. # Mock vector store list response with existing VS
  31. mock_vs_data = Mock()
  32. mock_vs_data.name = "attachments_vs"
  33. mock_vs_data.id = "vs_test123"
  34. mock_vs_list = Mock()
  35. mock_vs_list.data = [mock_vs_data]
  36. mock_agent.client_sync.vector_stores.list.return_value = mock_vs_list
  37. attachment_manager = AttachmentManager(mock_agent)
  38. # Call init_attachments_vs with existing VS name - should return existing ID
  39. result = attachment_manager.init_attachments_vs("attachments_vs")
  40. assert result == "vs_test123"
  41. mock_agent.client_sync.vector_stores.list.assert_called_once()
  42. mock_agent.client_sync.vector_stores.create.assert_not_called()
  43. def test_init_attachments_vs_create_new(self):
  44. """Test init_attachments_vs when creating new vector store."""
  45. mock_agent = Mock()
  46. mock_agent.name = "TestAgent"
  47. mock_agent.file_manager = Mock()
  48. # Mock vector store list response with no existing VS
  49. mock_vs_list = Mock()
  50. mock_vs_list.data = []
  51. # Mock create response
  52. mock_created_vs = Mock()
  53. mock_created_vs.id = "vs_new456"
  54. mock_agent.client_sync.vector_stores.list.return_value = mock_vs_list
  55. mock_agent.client_sync.vector_stores.create.return_value = mock_created_vs
  56. attachment_manager = AttachmentManager(mock_agent)
  57. # Call init_attachments_vs with new VS name - should create new VS
  58. result = attachment_manager.init_attachments_vs("new_vs")
  59. assert result == "vs_new456"
  60. mock_agent.client_sync.vector_stores.list.assert_called_once()
  61. mock_agent.client_sync.vector_stores.create.assert_called_once_with(name="new_vs")
  62. @pytest.mark.asyncio
  63. async def test_sort_file_attachments_unsupported_extension(self):
  64. """Test sort_file_attachments with unsupported file extension."""
  65. mock_agent = Mock()
  66. mock_agent.name = "TestAgent"
  67. mock_agent.file_manager = Mock()
  68. # Mock _get_filename_by_id to return file with unsupported extension
  69. attachment_manager = AttachmentManager(mock_agent)
  70. attachment_manager._get_filename_by_id = Mock(return_value="test.xyz")
  71. # Should raise AgentsException
  72. with pytest.raises(AgentsException, match="Unsupported file extension: .xyz for file test.xyz"):
  73. await attachment_manager.sort_file_attachments(["file-123"])
  74. @pytest.mark.asyncio
  75. async def test_prepare_and_attach_files_rejects_message_files(self):
  76. """Test prepare_and_attach_files rejects deprecated message_files usage."""
  77. mock_agent = Mock()
  78. mock_agent.name = "TestAgent"
  79. mock_agent.file_manager = Mock()
  80. attachment_manager = AttachmentManager(mock_agent)
  81. with pytest.raises(TypeError, match="message_files"):
  82. await attachment_manager.prepare_and_attach_files(
  83. [{"role": "user", "content": "hello"}],
  84. None,
  85. {"message_files": ["file-123"]},
  86. )
  87. @pytest.mark.asyncio
  88. async def test_prepare_and_attach_files_adds_code_interpreter_instruction(self):
  89. """Code-interpreter attachments should tell the model how to inspect uploaded files."""
  90. mock_agent = Mock()
  91. mock_agent.name = "TestAgent"
  92. mock_agent.file_manager = Mock()
  93. attachment_manager = AttachmentManager(mock_agent)
  94. attachment_manager._get_filename_by_id = Mock(return_value="report.txt")
  95. message_items = [{"role": "user", "content": "read the file"}]
  96. await attachment_manager.prepare_and_attach_files(message_items, ["file-123"], {})
  97. assert message_items == [
  98. {
  99. "role": "user",
  100. "content": [
  101. {"type": "input_text", "text": "read the file"},
  102. {
  103. "type": "input_text",
  104. "_agency_swarm_ephemeral": True,
  105. "text": (
  106. "The uploaded file(s) report.txt are available through the code_interpreter tool. "
  107. "Use that tool to inspect their contents before answering."
  108. ),
  109. },
  110. ],
  111. }
  112. ]
  113. mock_agent.file_manager.add_code_interpreter_tool.assert_called_once_with(["file-123"])
  114. def test_attachments_cleanup_code_interpreter_files(self):
  115. """Test attachments_cleanup with temporary code interpreter files."""
  116. mock_agent = Mock()
  117. mock_agent.name = "TestAgent"
  118. mock_agent.file_manager = Mock()
  119. # Add a CodeInterpreterTool with temp files
  120. mock_code_tool = Mock(spec=CodeInterpreterTool)
  121. mock_code_tool.tool_config = {"container": {"file_ids": ["file-123", "file-456", "file-789"]}}
  122. mock_agent.tools = [mock_code_tool]
  123. attachment_manager = AttachmentManager(mock_agent)
  124. attachment_manager._temp_code_interpreter_file_ids = ["file-123", "file-789"]
  125. # Call cleanup - should remove temp files from tool
  126. attachment_manager.attachments_cleanup()
  127. # Verify temp files were removed but tool kept (still has file-456)
  128. expected_file_ids = ["file-456"]
  129. assert mock_code_tool.tool_config["container"]["file_ids"] == expected_file_ids
  130. assert mock_code_tool in mock_agent.tools
  131. def test_attachments_cleanup_remove_code_interpreter_tool(self):
  132. """Test attachments_cleanup removing CodeInterpreterTool when no files left."""
  133. mock_agent = Mock()
  134. mock_agent.name = "TestAgent"
  135. mock_agent.file_manager = Mock()
  136. # Add a CodeInterpreterTool with only temp files
  137. mock_code_tool = Mock(spec=CodeInterpreterTool)
  138. mock_code_tool.tool_config = {"container": {"file_ids": ["file-123", "file-456"]}}
  139. mock_agent.tools = [mock_code_tool]
  140. attachment_manager = AttachmentManager(mock_agent)
  141. attachment_manager._temp_code_interpreter_file_ids = ["file-123", "file-456"]
  142. # Call cleanup - should remove entire tool
  143. attachment_manager.attachments_cleanup()
  144. # Verify tool was completely removed
  145. assert mock_code_tool not in mock_agent.tools
  146. def test_attachments_cleanup_code_interpreter_string_container(self):
  147. """Test attachments_cleanup with CodeInterpreterTool using string container."""
  148. mock_agent = Mock()
  149. mock_agent.name = "TestAgent"
  150. mock_agent.file_manager = Mock()
  151. # Add a CodeInterpreterTool with string container (can't modify)
  152. mock_code_tool = Mock(spec=CodeInterpreterTool)
  153. mock_code_tool.tool_config = {"container": "some_container_id"}
  154. mock_agent.tools = [mock_code_tool]
  155. attachment_manager = AttachmentManager(mock_agent)
  156. attachment_manager._temp_code_interpreter_file_ids = ["file-123"]
  157. # Call cleanup - should handle string container gracefully
  158. attachment_manager.attachments_cleanup() # Should not raise exception
  159. # Verify tool configuration wasn't modified
  160. assert mock_code_tool.tool_config["container"] == "some_container_id"