aixtools 0.1.10__py3-none-any.whl → 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of aixtools might be problematic. Click here for more details.

Files changed (62) hide show
  1. aixtools/_version.py +2 -2
  2. aixtools/agents/agent.py +26 -7
  3. aixtools/agents/print_nodes.py +54 -0
  4. aixtools/agents/prompt.py +2 -2
  5. aixtools/compliance/private_data.py +1 -1
  6. aixtools/evals/discovery.py +174 -0
  7. aixtools/evals/evals.py +74 -0
  8. aixtools/evals/run_evals.py +110 -0
  9. aixtools/logging/log_objects.py +24 -23
  10. aixtools/mcp/client.py +148 -2
  11. aixtools/server/__init__.py +0 -6
  12. aixtools/server/path.py +88 -31
  13. aixtools/testing/aix_test_model.py +9 -1
  14. aixtools/tools/doctor/mcp_tool_doctor.py +79 -0
  15. aixtools/tools/doctor/tool_doctor.py +4 -0
  16. aixtools/tools/doctor/tool_recommendation.py +5 -0
  17. aixtools/utils/config.py +0 -1
  18. {aixtools-0.1.10.dist-info → aixtools-0.2.0.dist-info}/METADATA +186 -30
  19. {aixtools-0.1.10.dist-info → aixtools-0.2.0.dist-info}/RECORD +23 -55
  20. aixtools-0.2.0.dist-info/entry_points.txt +4 -0
  21. aixtools-0.2.0.dist-info/top_level.txt +1 -0
  22. aixtools/server/workspace_privacy.py +0 -65
  23. aixtools-0.1.10.dist-info/entry_points.txt +0 -2
  24. aixtools-0.1.10.dist-info/top_level.txt +0 -5
  25. docker/mcp-base/Dockerfile +0 -33
  26. docker/mcp-base/zscaler.crt +0 -28
  27. notebooks/example_faulty_mcp_server.ipynb +0 -74
  28. notebooks/example_mcp_server_stdio.ipynb +0 -76
  29. notebooks/example_raw_mcp_client.ipynb +0 -84
  30. notebooks/example_tool_doctor.ipynb +0 -65
  31. scripts/config.sh +0 -28
  32. scripts/lint.sh +0 -32
  33. scripts/log_view.sh +0 -18
  34. scripts/run_example_mcp_server.sh +0 -14
  35. scripts/run_faulty_mcp_server.sh +0 -13
  36. scripts/run_server.sh +0 -29
  37. scripts/test.sh +0 -30
  38. tests/unit/__init__.py +0 -0
  39. tests/unit/a2a/__init__.py +0 -0
  40. tests/unit/a2a/google_sdk/__init__.py +0 -0
  41. tests/unit/a2a/google_sdk/pydantic_ai_adapter/__init__.py +0 -0
  42. tests/unit/a2a/google_sdk/pydantic_ai_adapter/test_agent_executor.py +0 -188
  43. tests/unit/a2a/google_sdk/pydantic_ai_adapter/test_storage.py +0 -156
  44. tests/unit/a2a/google_sdk/test_card.py +0 -114
  45. tests/unit/a2a/google_sdk/test_remote_agent_connection.py +0 -413
  46. tests/unit/a2a/google_sdk/test_utils.py +0 -208
  47. tests/unit/agents/__init__.py +0 -0
  48. tests/unit/agents/test_prompt.py +0 -363
  49. tests/unit/compliance/test_private_data.py +0 -329
  50. tests/unit/google/__init__.py +0 -1
  51. tests/unit/google/test_client.py +0 -233
  52. tests/unit/mcp/__init__.py +0 -0
  53. tests/unit/mcp/test_client.py +0 -242
  54. tests/unit/server/__init__.py +0 -0
  55. tests/unit/server/test_path.py +0 -225
  56. tests/unit/server/test_utils.py +0 -362
  57. tests/unit/utils/__init__.py +0 -0
  58. tests/unit/utils/test_files.py +0 -146
  59. tests/unit/vault/__init__.py +0 -0
  60. tests/unit/vault/test_vault.py +0 -246
  61. {tests → aixtools/evals}/__init__.py +0 -0
  62. {aixtools-0.1.10.dist-info → aixtools-0.2.0.dist-info}/WHEEL +0 -0
@@ -1,208 +0,0 @@
1
- """Tests for the A2A utils module."""
2
-
3
- import unittest
4
- from unittest.mock import AsyncMock, MagicMock, patch
5
-
6
- import httpx
7
- from a2a.client import ClientConfig, ClientFactory, A2ACardResolver
8
- from a2a.server.agent_execution import RequestContext
9
- from a2a.types import AgentCard
10
-
11
- from aixtools.a2a.google_sdk.utils import (
12
- _AgentCardResolver,
13
- get_a2a_clients,
14
- get_session_id_tuple,
15
- )
16
- from aixtools.a2a.google_sdk.remote_agent_connection import RemoteAgentConnection
17
- from aixtools.context import DEFAULT_USER_ID, DEFAULT_SESSION_ID
18
-
19
-
20
- class TestAgentCardResolver(unittest.IsolatedAsyncioTestCase):
21
- """Tests for the _AgentCardResolver class."""
22
-
23
- def setUp(self):
24
- self.mock_client = AsyncMock(spec=httpx.AsyncClient)
25
- self.resolver = _AgentCardResolver(self.mock_client)
26
-
27
- @patch("aixtools.a2a.google_sdk.utils.ClientFactory")
28
- def test_init(self, mock_client_factory_class):
29
- """Test _AgentCardResolver initialization."""
30
- mock_factory = MagicMock(spec=ClientFactory)
31
- mock_client_factory_class.return_value = mock_factory
32
-
33
- resolver = _AgentCardResolver(self.mock_client)
34
-
35
- # Verify ClientFactory was created with correct config
36
- mock_client_factory_class.assert_called_once()
37
- call_args = mock_client_factory_class.call_args[0][0]
38
- self.assertIsInstance(call_args, ClientConfig)
39
- self.assertEqual(call_args.httpx_client, self.mock_client)
40
-
41
- # Verify attributes are set
42
- self.assertEqual(resolver._httpx_client, self.mock_client)
43
- self.assertEqual(resolver._a2a_client_factory, mock_factory)
44
- self.assertEqual(resolver.clients, {})
45
-
46
- @patch("aixtools.a2a.google_sdk.utils.RemoteAgentConnection")
47
- def test_register_agent_card(self, mock_connection_class):
48
- """Test registering an agent card."""
49
- mock_card = MagicMock(spec=AgentCard)
50
- mock_card.name = "test_agent"
51
-
52
- mock_client = MagicMock()
53
- mock_factory = MagicMock(spec=ClientFactory)
54
- mock_factory.create.return_value = mock_client
55
- self.resolver._a2a_client_factory = mock_factory
56
-
57
- mock_connection = MagicMock(spec=RemoteAgentConnection)
58
- mock_connection_class.return_value = mock_connection
59
-
60
- self.resolver.register_agent_card(mock_card)
61
-
62
- # Verify client was created
63
- mock_factory.create.assert_called_once_with(mock_card)
64
-
65
- # Verify RemoteAgentConnection was created
66
- mock_connection_class.assert_called_once_with(mock_card, mock_client)
67
-
68
- # Verify connection was stored
69
- self.assertEqual(self.resolver.clients["test_agent"], mock_connection)
70
-
71
- @patch("aixtools.a2a.google_sdk.utils.A2ACardResolver")
72
- async def test_retrieve_card(self, mock_resolver_class):
73
- """Test retrieving a card from an address."""
74
- mock_resolver = AsyncMock(spec=A2ACardResolver)
75
- mock_resolver_class.return_value = mock_resolver
76
-
77
- mock_card = MagicMock(spec=AgentCard)
78
- mock_card.name = "test_agent"
79
- mock_resolver.get_agent_card.return_value = mock_card
80
-
81
- with patch.object(self.resolver, 'register_agent_card') as mock_register:
82
- await self.resolver.retrieve_card("http://test.com")
83
-
84
- # Verify resolver was created correctly (it tries the first card path)
85
- mock_resolver_class.assert_called_with(self.mock_client, "http://test.com", "/.well-known/agent-card.json")
86
-
87
- # Verify card was retrieved
88
- mock_resolver.get_agent_card.assert_called_once()
89
-
90
- # Verify card was registered
91
- mock_register.assert_called_once_with(mock_card)
92
-
93
- async def test_get_a2a_clients(self):
94
- """Test getting A2A clients for multiple hosts."""
95
- agent_hosts = ["http://agent1.com", "http://agent2.com"]
96
-
97
- with patch.object(self.resolver, 'retrieve_card') as mock_retrieve:
98
- mock_retrieve.return_value = None # Mock async function
99
-
100
- # Set up some mock clients
101
- mock_connection1 = MagicMock(spec=RemoteAgentConnection)
102
- mock_connection2 = MagicMock(spec=RemoteAgentConnection)
103
- self.resolver.clients = {
104
- "agent1": mock_connection1,
105
- "agent2": mock_connection2
106
- }
107
-
108
- result = await self.resolver.get_a2a_clients(agent_hosts)
109
-
110
- # Verify retrieve_card was called for each host
111
- self.assertEqual(mock_retrieve.call_count, 2)
112
- mock_retrieve.assert_any_call("http://agent1.com")
113
- mock_retrieve.assert_any_call("http://agent2.com")
114
-
115
- # Verify result contains the clients
116
- self.assertEqual(result, self.resolver.clients)
117
-
118
-
119
- class TestGetA2AClients(unittest.IsolatedAsyncioTestCase):
120
- """Tests for the get_a2a_clients function."""
121
-
122
- @patch("aixtools.a2a.google_sdk.utils._AgentCardResolver")
123
- @patch("aixtools.a2a.google_sdk.utils.httpx.AsyncClient")
124
- async def test_get_a2a_clients(self, mock_client_class, mock_resolver_class):
125
- """Test the get_a2a_clients function."""
126
- mock_client = AsyncMock()
127
- mock_client_class.return_value = mock_client
128
-
129
- mock_resolver = AsyncMock(spec=_AgentCardResolver)
130
- mock_resolver_class.return_value = mock_resolver
131
-
132
- mock_clients = {"agent1": MagicMock(), "agent2": MagicMock()}
133
- mock_resolver.get_a2a_clients.return_value = mock_clients
134
-
135
- ctx = ("user123", "session456")
136
- agent_hosts = ["http://agent1.com", "http://agent2.com"]
137
-
138
- result = await get_a2a_clients(ctx, agent_hosts)
139
-
140
- # Verify httpx client was created with correct headers
141
- expected_headers = {
142
- "user-id": "user123",
143
- "session-id": "session456",
144
- }
145
- mock_client_class.assert_called_once_with(headers=expected_headers, timeout=60.0)
146
-
147
- # Verify resolver was created with the client
148
- mock_resolver_class.assert_called_once_with(mock_client)
149
-
150
- # Verify get_a2a_clients was called with the hosts
151
- mock_resolver.get_a2a_clients.assert_called_once_with(agent_hosts)
152
-
153
- # Verify result
154
- self.assertEqual(result, mock_clients)
155
-
156
-
157
- class TestGetSessionIdTuple(unittest.TestCase):
158
- """Tests for the get_session_id_tuple function."""
159
-
160
- def test_get_session_id_tuple_with_headers(self):
161
- """Test getting session ID tuple when headers are present."""
162
- mock_context = MagicMock(spec=RequestContext)
163
- mock_context.call_context.state = {
164
- "headers": {
165
- "user-id": "test_user",
166
- "session-id": "test_session"
167
- }
168
- }
169
-
170
- result = get_session_id_tuple(mock_context)
171
-
172
- self.assertEqual(result, ("test_user", "test_session"))
173
-
174
- def test_get_session_id_tuple_with_partial_headers(self):
175
- """Test getting session ID tuple when only some headers are present."""
176
- mock_context = MagicMock(spec=RequestContext)
177
- mock_context.call_context.state = {
178
- "headers": {
179
- "user-id": "test_user"
180
- # session-id is missing
181
- }
182
- }
183
-
184
- result = get_session_id_tuple(mock_context)
185
-
186
- self.assertEqual(result, ("test_user", DEFAULT_SESSION_ID))
187
-
188
- def test_get_session_id_tuple_no_headers(self):
189
- """Test getting session ID tuple when no headers are present."""
190
- mock_context = MagicMock(spec=RequestContext)
191
- mock_context.call_context.state = {}
192
-
193
- result = get_session_id_tuple(mock_context)
194
-
195
- self.assertEqual(result, (DEFAULT_USER_ID, DEFAULT_SESSION_ID))
196
-
197
- def test_get_session_id_tuple_empty_headers(self):
198
- """Test getting session ID tuple when headers dict is empty."""
199
- mock_context = MagicMock(spec=RequestContext)
200
- mock_context.call_context.state = {"headers": {}}
201
-
202
- result = get_session_id_tuple(mock_context)
203
-
204
- self.assertEqual(result, (DEFAULT_USER_ID, DEFAULT_SESSION_ID))
205
-
206
-
207
- if __name__ == '__main__':
208
- unittest.main()
File without changes
@@ -1,363 +0,0 @@
1
- """Unit tests for aixtools.agents.prompt module."""
2
-
3
- import tempfile
4
- import unittest
5
- from pathlib import Path
6
- from unittest.mock import Mock, patch, mock_open
7
-
8
- from pydantic_ai import BinaryContent
9
-
10
- from aixtools.agents.prompt import (
11
- CLAUDE_IMAGE_MAX_FILE_SIZE_IN_CONTEXT,
12
- CLAUDE_MAX_FILE_SIZE_IN_CONTEXT,
13
- build_user_input,
14
- file_to_binary_content,
15
- should_be_included_into_context,
16
- )
17
-
18
-
19
- class TestShouldBeIncludedIntoContext(unittest.TestCase):
20
- """Test cases for should_be_included_into_context function."""
21
-
22
- def test_non_binary_content_returns_false(self):
23
- """Test that non-BinaryContent returns False."""
24
- result = should_be_included_into_context("text content", 100)
25
- self.assertFalse(result)
26
-
27
- result = should_be_included_into_context(None, 100)
28
- self.assertFalse(result)
29
-
30
- def test_text_media_type_returns_false(self):
31
- """Test that text media types return False."""
32
- binary_content = BinaryContent(data=b"test", media_type="text/plain")
33
- result = should_be_included_into_context(binary_content, 100)
34
- self.assertFalse(result)
35
-
36
- binary_content = BinaryContent(data=b"test", media_type="text/html")
37
- result = should_be_included_into_context(binary_content, 100)
38
- self.assertFalse(result)
39
-
40
- def test_archive_types_return_false(self):
41
- """Test that archive media types return False."""
42
- archive_types = [
43
- "application/zip",
44
- "application/x-tar",
45
- "application/gzip",
46
- "application/x-gzip",
47
- "application/x-rar-compressed",
48
- "application/x-7z-compressed",
49
- ]
50
-
51
- for media_type in archive_types:
52
- binary_content = BinaryContent(data=b"test", media_type=media_type)
53
- result = should_be_included_into_context(binary_content, 100)
54
- self.assertFalse(result, f"Archive type {media_type} should return False")
55
-
56
- def test_image_within_size_limit_returns_true(self):
57
- """Test that images within size limit return True."""
58
- with patch.object(BinaryContent, 'is_image', new_callable=lambda: True):
59
- binary_content = BinaryContent(data=b"image_data", media_type="image/png")
60
-
61
- # Test with size under limit
62
- result = should_be_included_into_context(binary_content, 1024)
63
- self.assertTrue(result)
64
-
65
- def test_image_over_size_limit_returns_false(self):
66
- """Test that images over size limit return False."""
67
- with patch.object(BinaryContent, 'is_image', new_callable=lambda: True):
68
- binary_content = BinaryContent(data=b"image_data", media_type="image/png")
69
-
70
- # Test with size over limit
71
- result = should_be_included_into_context(binary_content, CLAUDE_IMAGE_MAX_FILE_SIZE_IN_CONTEXT + 1)
72
- self.assertFalse(result)
73
-
74
- def test_non_image_within_size_limit_returns_true(self):
75
- """Test that non-images within size limit return True."""
76
- with patch.object(BinaryContent, 'is_image', new_callable=lambda: False):
77
- binary_content = BinaryContent(data=b"pdf_data", media_type="application/pdf")
78
-
79
- # Test with size under limit
80
- result = should_be_included_into_context(binary_content, 1024)
81
- self.assertTrue(result)
82
-
83
- def test_non_image_over_size_limit_returns_false(self):
84
- """Test that non-images over size limit return False."""
85
- with patch.object(BinaryContent, 'is_image', new_callable=lambda: False):
86
- binary_content = BinaryContent(data=b"pdf_data", media_type="application/pdf")
87
-
88
- # Test with size over limit
89
- result = should_be_included_into_context(binary_content, CLAUDE_MAX_FILE_SIZE_IN_CONTEXT + 1)
90
- self.assertFalse(result)
91
-
92
- def test_custom_size_limits(self):
93
- """Test with custom size limits."""
94
- # Test non-image content with custom limits (since image detection is complex)
95
- pdf_content = BinaryContent(data=b"pdf_data", media_type="application/pdf")
96
-
97
- # Test non-image over custom file limit
98
- result = should_be_included_into_context(
99
- pdf_content, 5000, max_img_size_bytes=1024, max_file_size_bytes=4096
100
- )
101
- self.assertFalse(result)
102
-
103
- # Test non-image under custom file limit
104
- result = should_be_included_into_context(
105
- pdf_content, 2000, max_img_size_bytes=1024, max_file_size_bytes=4096
106
- )
107
- self.assertTrue(result)
108
-
109
- # Test with very small custom limits
110
- result = should_be_included_into_context(
111
- pdf_content, 100, max_img_size_bytes=50, max_file_size_bytes=80
112
- )
113
- self.assertFalse(result)
114
-
115
- # Test with very large custom limits
116
- result = should_be_included_into_context(
117
- pdf_content, 100, max_img_size_bytes=200, max_file_size_bytes=200
118
- )
119
- self.assertTrue(result)
120
-
121
-
122
- class TestFileToBinaryContent(unittest.TestCase):
123
- """Test cases for file_to_binary_content function."""
124
-
125
- def setUp(self):
126
- """Set up test fixtures."""
127
- self.temp_dir = tempfile.mkdtemp()
128
- self.temp_path = Path(self.temp_dir)
129
-
130
- def tearDown(self):
131
- """Clean up test fixtures."""
132
- import shutil
133
- shutil.rmtree(self.temp_dir)
134
-
135
- @patch('aixtools.agents.prompt.is_text_content')
136
- @patch('mimetypes.guess_type')
137
- def test_text_file_returns_string(self, mock_guess_type, mock_is_text):
138
- """Test that text files return decoded strings."""
139
- mock_guess_type.return_value = ("text/plain", None)
140
- mock_is_text.return_value = True
141
-
142
- test_file = self.temp_path / "test.txt"
143
- test_content = "Hello, world!"
144
- test_file.write_text(test_content, encoding="utf-8")
145
-
146
- result = file_to_binary_content(test_file)
147
-
148
- self.assertEqual(result, test_content)
149
- mock_is_text.assert_called_once()
150
-
151
- @patch('aixtools.agents.prompt.is_text_content')
152
- @patch('mimetypes.guess_type')
153
- def test_binary_file_returns_binary_content(self, mock_guess_type, mock_is_text):
154
- """Test that binary files return BinaryContent."""
155
- mock_guess_type.return_value = ("image/png", None)
156
- mock_is_text.return_value = False
157
-
158
- test_file = self.temp_path / "test.png"
159
- test_data = b'\x89PNG\r\n\x1a\n'
160
- test_file.write_bytes(test_data)
161
-
162
- result = file_to_binary_content(test_file)
163
-
164
- self.assertIsInstance(result, BinaryContent)
165
- if isinstance(result, BinaryContent):
166
- self.assertEqual(result.data, test_data)
167
- self.assertEqual(result.media_type, "image/png")
168
-
169
- @patch('aixtools.agents.prompt.is_text_content')
170
- @patch('mimetypes.guess_type')
171
- def test_unknown_mime_type_defaults_to_octet_stream(self, mock_guess_type, mock_is_text):
172
- """Test that unknown mime types default to application/octet-stream."""
173
- mock_guess_type.return_value = (None, None)
174
- mock_is_text.return_value = False
175
-
176
- test_file = self.temp_path / "test.unknown"
177
- test_data = b'unknown data'
178
- test_file.write_bytes(test_data)
179
-
180
- result = file_to_binary_content(test_file)
181
-
182
- self.assertIsInstance(result, BinaryContent)
183
- if isinstance(result, BinaryContent):
184
- self.assertEqual(result.media_type, "application/octet-stream")
185
-
186
- @patch('aixtools.agents.prompt.is_text_content')
187
- def test_explicit_mime_type(self, mock_is_text):
188
- """Test with explicitly provided mime type."""
189
- mock_is_text.return_value = False
190
-
191
- test_file = self.temp_path / "test.data"
192
- test_data = b'test data'
193
- test_file.write_bytes(test_data)
194
-
195
- result = file_to_binary_content(test_file, "application/custom")
196
-
197
- self.assertIsInstance(result, BinaryContent)
198
- if isinstance(result, BinaryContent):
199
- self.assertEqual(result.media_type, "application/custom")
200
-
201
-
202
- class TestBuildUserInput(unittest.TestCase):
203
- """Test cases for build_user_input function."""
204
-
205
- def setUp(self):
206
- """Set up test fixtures."""
207
- self.session_tuple = ("user123", "session456")
208
- self.user_text = "Please analyze these files"
209
-
210
- def test_no_file_paths_returns_text_only(self):
211
- """Test that no file paths returns just the user text."""
212
- result = build_user_input(self.session_tuple, self.user_text, [])
213
-
214
- self.assertEqual(result, self.user_text)
215
-
216
- @patch('aixtools.agents.prompt.should_be_included_into_context')
217
- @patch('aixtools.agents.prompt.file_to_binary_content')
218
- @patch('aixtools.agents.prompt.container_to_host_path')
219
- @patch('mimetypes.guess_type')
220
- def test_single_file_not_included_in_context(
221
- self, mock_guess_type, mock_container_to_host,
222
- mock_file_to_binary, mock_should_include
223
- ):
224
- """Test with single file that should not be included in context."""
225
- # Setup mocks
226
- mock_guess_type.return_value = ("text/plain", None)
227
- mock_should_include.return_value = False
228
-
229
- mock_host_path = Mock()
230
- mock_host_path.stat.return_value.st_size = 1024
231
- mock_container_to_host.return_value = mock_host_path
232
-
233
- mock_file_to_binary.return_value = "file content"
234
-
235
- file_paths = [Path("/workspace/test.txt")]
236
-
237
- result = build_user_input(self.session_tuple, self.user_text, file_paths)
238
-
239
- self.assertIsInstance(result, list)
240
- self.assertEqual(len(result), 1) # Only the prompt, no binary attachments
241
- self.assertIsInstance(result[0], str)
242
- prompt_text = str(result[0])
243
- self.assertIn("Please analyze these files", prompt_text)
244
- self.assertIn("Attachments:", prompt_text)
245
- self.assertIn("test.txt", prompt_text)
246
- self.assertIn("file_size=1024 bytes", prompt_text)
247
-
248
- @patch('aixtools.agents.prompt.should_be_included_into_context')
249
- @patch('aixtools.agents.prompt.file_to_binary_content')
250
- @patch('aixtools.agents.prompt.container_to_host_path')
251
- @patch('mimetypes.guess_type')
252
- def test_single_file_included_in_context(
253
- self, mock_guess_type, mock_container_to_host,
254
- mock_file_to_binary, mock_should_include
255
- ):
256
- """Test with single file that should be included in context."""
257
- # Setup mocks
258
- mock_guess_type.return_value = ("image/png", None)
259
- mock_should_include.return_value = True
260
-
261
- mock_host_path = Mock()
262
- mock_host_path.stat.return_value.st_size = 2048
263
- mock_container_to_host.return_value = mock_host_path
264
-
265
- mock_binary_content = BinaryContent(data=b"image data", media_type="image/png")
266
- mock_file_to_binary.return_value = mock_binary_content
267
-
268
- file_paths = [Path("/workspace/image.png")]
269
-
270
- result = build_user_input(self.session_tuple, self.user_text, file_paths)
271
-
272
- self.assertIsInstance(result, list)
273
- self.assertEqual(len(result), 2) # Prompt + 1 binary attachment
274
- self.assertIsInstance(result[0], str)
275
- prompt_text = str(result[0])
276
- self.assertIn("Please analyze these files", prompt_text)
277
- self.assertIn("Attachments:", prompt_text)
278
- self.assertIn("image.png", prompt_text)
279
- self.assertIn("file_size=2048 bytes", prompt_text)
280
- self.assertIn("provided to model context at index 0", prompt_text)
281
- self.assertEqual(result[1], mock_binary_content)
282
-
283
- @patch('aixtools.agents.prompt.should_be_included_into_context')
284
- @patch('aixtools.agents.prompt.file_to_binary_content')
285
- @patch('aixtools.agents.prompt.container_to_host_path')
286
- @patch('mimetypes.guess_type')
287
- def test_multiple_files_mixed_inclusion(
288
- self, mock_guess_type, mock_container_to_host,
289
- mock_file_to_binary, mock_should_include
290
- ):
291
- """Test with multiple files, some included in context, some not."""
292
- # Setup mocks
293
- mock_guess_type.side_effect = [("text/plain", None), ("image/png", None)]
294
- mock_should_include.side_effect = [False, True] # First file not included, second included
295
-
296
- mock_host_path1 = Mock()
297
- mock_host_path1.stat.return_value.st_size = 1024
298
- mock_host_path2 = Mock()
299
- mock_host_path2.stat.return_value.st_size = 2048
300
- mock_container_to_host.side_effect = [mock_host_path1, mock_host_path2]
301
-
302
- mock_text_content = "text content"
303
- mock_binary_content = BinaryContent(data=b"image data", media_type="image/png")
304
- mock_file_to_binary.side_effect = [mock_text_content, mock_binary_content]
305
-
306
- file_paths = [Path("/workspace/text.txt"), Path("/workspace/image.png")]
307
-
308
- result = build_user_input(self.session_tuple, self.user_text, file_paths)
309
-
310
- self.assertIsInstance(result, list)
311
- self.assertEqual(len(result), 2) # Prompt + 1 binary attachment
312
- self.assertIsInstance(result[0], str)
313
- prompt_text = str(result[0])
314
- self.assertIn("Please analyze these files", prompt_text)
315
- self.assertIn("Attachments:", prompt_text)
316
- self.assertIn("text.txt", prompt_text)
317
- self.assertIn("image.png", prompt_text)
318
- self.assertIn("file_size=1024 bytes", prompt_text)
319
- self.assertIn("file_size=2048 bytes", prompt_text)
320
- self.assertIn("provided to model context at index 0", prompt_text)
321
- self.assertEqual(result[1], mock_binary_content)
322
-
323
- def test_non_workspace_path_raises_error(self):
324
- """Test that non-workspace paths raise ValueError."""
325
- file_paths = [Path("/invalid/path.txt")]
326
-
327
- with self.assertRaises(ValueError) as context:
328
- build_user_input(self.session_tuple, self.user_text, file_paths)
329
-
330
- self.assertIn(
331
- "Container path must be a subdir of '/workspace', got '/invalid/path.txt' instead",
332
- str(context.exception),
333
- )
334
-
335
- @patch('aixtools.agents.prompt.should_be_included_into_context')
336
- @patch('aixtools.agents.prompt.file_to_binary_content')
337
- @patch('aixtools.agents.prompt.container_to_host_path')
338
- @patch('mimetypes.guess_type')
339
- def test_unknown_mime_type_defaults(
340
- self, mock_guess_type, mock_container_to_host,
341
- mock_file_to_binary, mock_should_include
342
- ):
343
- """Test that unknown mime types default to application/octet-stream."""
344
- # Setup mocks
345
- mock_guess_type.return_value = (None, None) # Unknown mime type
346
- mock_should_include.return_value = False
347
-
348
- mock_host_path = Mock()
349
- mock_host_path.stat.return_value.st_size = 1024
350
- mock_container_to_host.return_value = mock_host_path
351
-
352
- mock_file_to_binary.return_value = "file content"
353
-
354
- file_paths = [Path("/workspace/unknown.dat")]
355
-
356
- build_user_input(self.session_tuple, self.user_text, file_paths)
357
-
358
- # Verify that file_to_binary_content was called with the default mime type
359
- mock_file_to_binary.assert_called_once_with(mock_host_path, "application/octet-stream")
360
-
361
-
362
- if __name__ == '__main__':
363
- unittest.main()