atlas-chat 0.1.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.
Files changed (250) hide show
  1. atlas/__init__.py +40 -0
  2. atlas/application/__init__.py +7 -0
  3. atlas/application/chat/__init__.py +7 -0
  4. atlas/application/chat/agent/__init__.py +10 -0
  5. atlas/application/chat/agent/act_loop.py +179 -0
  6. atlas/application/chat/agent/factory.py +142 -0
  7. atlas/application/chat/agent/protocols.py +46 -0
  8. atlas/application/chat/agent/react_loop.py +338 -0
  9. atlas/application/chat/agent/think_act_loop.py +171 -0
  10. atlas/application/chat/approval_manager.py +151 -0
  11. atlas/application/chat/elicitation_manager.py +191 -0
  12. atlas/application/chat/events/__init__.py +1 -0
  13. atlas/application/chat/events/agent_event_relay.py +112 -0
  14. atlas/application/chat/modes/__init__.py +1 -0
  15. atlas/application/chat/modes/agent.py +125 -0
  16. atlas/application/chat/modes/plain.py +74 -0
  17. atlas/application/chat/modes/rag.py +81 -0
  18. atlas/application/chat/modes/tools.py +179 -0
  19. atlas/application/chat/orchestrator.py +213 -0
  20. atlas/application/chat/policies/__init__.py +1 -0
  21. atlas/application/chat/policies/tool_authorization.py +99 -0
  22. atlas/application/chat/preprocessors/__init__.py +1 -0
  23. atlas/application/chat/preprocessors/message_builder.py +92 -0
  24. atlas/application/chat/preprocessors/prompt_override_service.py +104 -0
  25. atlas/application/chat/service.py +454 -0
  26. atlas/application/chat/utilities/__init__.py +6 -0
  27. atlas/application/chat/utilities/error_handler.py +367 -0
  28. atlas/application/chat/utilities/event_notifier.py +546 -0
  29. atlas/application/chat/utilities/file_processor.py +613 -0
  30. atlas/application/chat/utilities/tool_executor.py +789 -0
  31. atlas/atlas_chat_cli.py +347 -0
  32. atlas/atlas_client.py +238 -0
  33. atlas/core/__init__.py +0 -0
  34. atlas/core/auth.py +205 -0
  35. atlas/core/authorization_manager.py +27 -0
  36. atlas/core/capabilities.py +123 -0
  37. atlas/core/compliance.py +215 -0
  38. atlas/core/domain_whitelist.py +147 -0
  39. atlas/core/domain_whitelist_middleware.py +82 -0
  40. atlas/core/http_client.py +28 -0
  41. atlas/core/log_sanitizer.py +102 -0
  42. atlas/core/metrics_logger.py +59 -0
  43. atlas/core/middleware.py +131 -0
  44. atlas/core/otel_config.py +242 -0
  45. atlas/core/prompt_risk.py +200 -0
  46. atlas/core/rate_limit.py +0 -0
  47. atlas/core/rate_limit_middleware.py +64 -0
  48. atlas/core/security_headers_middleware.py +51 -0
  49. atlas/domain/__init__.py +37 -0
  50. atlas/domain/chat/__init__.py +1 -0
  51. atlas/domain/chat/dtos.py +85 -0
  52. atlas/domain/errors.py +96 -0
  53. atlas/domain/messages/__init__.py +12 -0
  54. atlas/domain/messages/models.py +160 -0
  55. atlas/domain/rag_mcp_service.py +664 -0
  56. atlas/domain/sessions/__init__.py +7 -0
  57. atlas/domain/sessions/models.py +36 -0
  58. atlas/domain/unified_rag_service.py +371 -0
  59. atlas/infrastructure/__init__.py +10 -0
  60. atlas/infrastructure/app_factory.py +135 -0
  61. atlas/infrastructure/events/__init__.py +1 -0
  62. atlas/infrastructure/events/cli_event_publisher.py +140 -0
  63. atlas/infrastructure/events/websocket_publisher.py +140 -0
  64. atlas/infrastructure/sessions/in_memory_repository.py +56 -0
  65. atlas/infrastructure/transport/__init__.py +7 -0
  66. atlas/infrastructure/transport/websocket_connection_adapter.py +33 -0
  67. atlas/init_cli.py +226 -0
  68. atlas/interfaces/__init__.py +15 -0
  69. atlas/interfaces/events.py +134 -0
  70. atlas/interfaces/llm.py +54 -0
  71. atlas/interfaces/rag.py +40 -0
  72. atlas/interfaces/sessions.py +75 -0
  73. atlas/interfaces/tools.py +57 -0
  74. atlas/interfaces/transport.py +24 -0
  75. atlas/main.py +564 -0
  76. atlas/mcp/api_key_demo/README.md +76 -0
  77. atlas/mcp/api_key_demo/main.py +172 -0
  78. atlas/mcp/api_key_demo/run.sh +56 -0
  79. atlas/mcp/basictable/main.py +147 -0
  80. atlas/mcp/calculator/main.py +149 -0
  81. atlas/mcp/code-executor/execution_engine.py +98 -0
  82. atlas/mcp/code-executor/execution_environment.py +95 -0
  83. atlas/mcp/code-executor/main.py +528 -0
  84. atlas/mcp/code-executor/result_processing.py +276 -0
  85. atlas/mcp/code-executor/script_generation.py +195 -0
  86. atlas/mcp/code-executor/security_checker.py +140 -0
  87. atlas/mcp/corporate_cars/main.py +437 -0
  88. atlas/mcp/csv_reporter/main.py +545 -0
  89. atlas/mcp/duckduckgo/main.py +182 -0
  90. atlas/mcp/elicitation_demo/README.md +171 -0
  91. atlas/mcp/elicitation_demo/main.py +262 -0
  92. atlas/mcp/env-demo/README.md +158 -0
  93. atlas/mcp/env-demo/main.py +199 -0
  94. atlas/mcp/file_size_test/main.py +284 -0
  95. atlas/mcp/filesystem/main.py +348 -0
  96. atlas/mcp/image_demo/main.py +113 -0
  97. atlas/mcp/image_demo/requirements.txt +4 -0
  98. atlas/mcp/logging_demo/README.md +72 -0
  99. atlas/mcp/logging_demo/main.py +103 -0
  100. atlas/mcp/many_tools_demo/main.py +50 -0
  101. atlas/mcp/order_database/__init__.py +0 -0
  102. atlas/mcp/order_database/main.py +369 -0
  103. atlas/mcp/order_database/signal_data.csv +1001 -0
  104. atlas/mcp/pdfbasic/main.py +394 -0
  105. atlas/mcp/pptx_generator/main.py +760 -0
  106. atlas/mcp/pptx_generator/requirements.txt +13 -0
  107. atlas/mcp/pptx_generator/run_test.sh +1 -0
  108. atlas/mcp/pptx_generator/test_pptx_generator_security.py +169 -0
  109. atlas/mcp/progress_demo/main.py +167 -0
  110. atlas/mcp/progress_updates_demo/QUICKSTART.md +273 -0
  111. atlas/mcp/progress_updates_demo/README.md +120 -0
  112. atlas/mcp/progress_updates_demo/main.py +497 -0
  113. atlas/mcp/prompts/main.py +222 -0
  114. atlas/mcp/public_demo/main.py +189 -0
  115. atlas/mcp/sampling_demo/README.md +169 -0
  116. atlas/mcp/sampling_demo/main.py +234 -0
  117. atlas/mcp/thinking/main.py +77 -0
  118. atlas/mcp/tool_planner/main.py +240 -0
  119. atlas/mcp/ui-demo/badmesh.png +0 -0
  120. atlas/mcp/ui-demo/main.py +383 -0
  121. atlas/mcp/ui-demo/templates/button_demo.html +32 -0
  122. atlas/mcp/ui-demo/templates/data_visualization.html +32 -0
  123. atlas/mcp/ui-demo/templates/form_demo.html +28 -0
  124. atlas/mcp/username-override-demo/README.md +320 -0
  125. atlas/mcp/username-override-demo/main.py +308 -0
  126. atlas/modules/__init__.py +0 -0
  127. atlas/modules/config/__init__.py +34 -0
  128. atlas/modules/config/cli.py +231 -0
  129. atlas/modules/config/config_manager.py +1096 -0
  130. atlas/modules/file_storage/__init__.py +22 -0
  131. atlas/modules/file_storage/cli.py +330 -0
  132. atlas/modules/file_storage/content_extractor.py +290 -0
  133. atlas/modules/file_storage/manager.py +295 -0
  134. atlas/modules/file_storage/mock_s3_client.py +402 -0
  135. atlas/modules/file_storage/s3_client.py +417 -0
  136. atlas/modules/llm/__init__.py +19 -0
  137. atlas/modules/llm/caller.py +287 -0
  138. atlas/modules/llm/litellm_caller.py +675 -0
  139. atlas/modules/llm/models.py +19 -0
  140. atlas/modules/mcp_tools/__init__.py +17 -0
  141. atlas/modules/mcp_tools/client.py +2123 -0
  142. atlas/modules/mcp_tools/token_storage.py +556 -0
  143. atlas/modules/prompts/prompt_provider.py +130 -0
  144. atlas/modules/rag/__init__.py +24 -0
  145. atlas/modules/rag/atlas_rag_client.py +336 -0
  146. atlas/modules/rag/client.py +129 -0
  147. atlas/routes/admin_routes.py +865 -0
  148. atlas/routes/config_routes.py +484 -0
  149. atlas/routes/feedback_routes.py +361 -0
  150. atlas/routes/files_routes.py +274 -0
  151. atlas/routes/health_routes.py +40 -0
  152. atlas/routes/mcp_auth_routes.py +223 -0
  153. atlas/server_cli.py +164 -0
  154. atlas/tests/conftest.py +20 -0
  155. atlas/tests/integration/test_mcp_auth_integration.py +152 -0
  156. atlas/tests/manual_test_sampling.py +87 -0
  157. atlas/tests/modules/mcp_tools/test_client_auth.py +226 -0
  158. atlas/tests/modules/mcp_tools/test_client_env.py +191 -0
  159. atlas/tests/test_admin_mcp_server_management_routes.py +141 -0
  160. atlas/tests/test_agent_roa.py +135 -0
  161. atlas/tests/test_app_factory_smoke.py +47 -0
  162. atlas/tests/test_approval_manager.py +439 -0
  163. atlas/tests/test_atlas_client.py +188 -0
  164. atlas/tests/test_atlas_rag_client.py +447 -0
  165. atlas/tests/test_atlas_rag_integration.py +224 -0
  166. atlas/tests/test_attach_file_flow.py +287 -0
  167. atlas/tests/test_auth_utils.py +165 -0
  168. atlas/tests/test_backend_public_url.py +185 -0
  169. atlas/tests/test_banner_logging.py +287 -0
  170. atlas/tests/test_capability_tokens_and_injection.py +203 -0
  171. atlas/tests/test_compliance_level.py +54 -0
  172. atlas/tests/test_compliance_manager.py +253 -0
  173. atlas/tests/test_config_manager.py +617 -0
  174. atlas/tests/test_config_manager_paths.py +12 -0
  175. atlas/tests/test_core_auth.py +18 -0
  176. atlas/tests/test_core_utils.py +190 -0
  177. atlas/tests/test_docker_env_sync.py +202 -0
  178. atlas/tests/test_domain_errors.py +329 -0
  179. atlas/tests/test_domain_whitelist.py +359 -0
  180. atlas/tests/test_elicitation_manager.py +408 -0
  181. atlas/tests/test_elicitation_routing.py +296 -0
  182. atlas/tests/test_env_demo_server.py +88 -0
  183. atlas/tests/test_error_classification.py +113 -0
  184. atlas/tests/test_error_flow_integration.py +116 -0
  185. atlas/tests/test_feedback_routes.py +333 -0
  186. atlas/tests/test_file_content_extraction.py +1134 -0
  187. atlas/tests/test_file_extraction_routes.py +158 -0
  188. atlas/tests/test_file_library.py +107 -0
  189. atlas/tests/test_file_manager_unit.py +18 -0
  190. atlas/tests/test_health_route.py +49 -0
  191. atlas/tests/test_http_client_stub.py +8 -0
  192. atlas/tests/test_imports_smoke.py +30 -0
  193. atlas/tests/test_interfaces_llm_response.py +9 -0
  194. atlas/tests/test_issue_access_denied_fix.py +136 -0
  195. atlas/tests/test_llm_env_expansion.py +836 -0
  196. atlas/tests/test_log_level_sensitive_data.py +285 -0
  197. atlas/tests/test_mcp_auth_routes.py +341 -0
  198. atlas/tests/test_mcp_client_auth.py +331 -0
  199. atlas/tests/test_mcp_data_injection.py +270 -0
  200. atlas/tests/test_mcp_get_authorized_servers.py +95 -0
  201. atlas/tests/test_mcp_hot_reload.py +512 -0
  202. atlas/tests/test_mcp_image_content.py +424 -0
  203. atlas/tests/test_mcp_logging.py +172 -0
  204. atlas/tests/test_mcp_progress_updates.py +313 -0
  205. atlas/tests/test_mcp_prompt_override_system_prompt.py +102 -0
  206. atlas/tests/test_mcp_prompts_server.py +39 -0
  207. atlas/tests/test_mcp_tool_result_parsing.py +296 -0
  208. atlas/tests/test_metrics_logger.py +56 -0
  209. atlas/tests/test_middleware_auth.py +379 -0
  210. atlas/tests/test_prompt_risk_and_acl.py +141 -0
  211. atlas/tests/test_rag_mcp_aggregator.py +204 -0
  212. atlas/tests/test_rag_mcp_service.py +224 -0
  213. atlas/tests/test_rate_limit_middleware.py +45 -0
  214. atlas/tests/test_routes_config_smoke.py +60 -0
  215. atlas/tests/test_routes_files_download_token.py +41 -0
  216. atlas/tests/test_routes_files_health.py +18 -0
  217. atlas/tests/test_runtime_imports.py +53 -0
  218. atlas/tests/test_sampling_integration.py +482 -0
  219. atlas/tests/test_security_admin_routes.py +61 -0
  220. atlas/tests/test_security_capability_tokens.py +65 -0
  221. atlas/tests/test_security_file_stats_scope.py +21 -0
  222. atlas/tests/test_security_header_injection.py +191 -0
  223. atlas/tests/test_security_headers_and_filename.py +63 -0
  224. atlas/tests/test_shared_session_repository.py +101 -0
  225. atlas/tests/test_system_prompt_loading.py +181 -0
  226. atlas/tests/test_token_storage.py +505 -0
  227. atlas/tests/test_tool_approval_config.py +93 -0
  228. atlas/tests/test_tool_approval_utils.py +356 -0
  229. atlas/tests/test_tool_authorization_group_filtering.py +223 -0
  230. atlas/tests/test_tool_details_in_config.py +108 -0
  231. atlas/tests/test_tool_planner.py +300 -0
  232. atlas/tests/test_unified_rag_service.py +398 -0
  233. atlas/tests/test_username_override_in_approval.py +258 -0
  234. atlas/tests/test_websocket_auth_header.py +168 -0
  235. atlas/version.py +6 -0
  236. atlas_chat-0.1.0.data/data/.env.example +253 -0
  237. atlas_chat-0.1.0.data/data/config/defaults/compliance-levels.json +44 -0
  238. atlas_chat-0.1.0.data/data/config/defaults/domain-whitelist.json +123 -0
  239. atlas_chat-0.1.0.data/data/config/defaults/file-extractors.json +74 -0
  240. atlas_chat-0.1.0.data/data/config/defaults/help-config.json +198 -0
  241. atlas_chat-0.1.0.data/data/config/defaults/llmconfig-buggy.yml +11 -0
  242. atlas_chat-0.1.0.data/data/config/defaults/llmconfig.yml +19 -0
  243. atlas_chat-0.1.0.data/data/config/defaults/mcp.json +138 -0
  244. atlas_chat-0.1.0.data/data/config/defaults/rag-sources.json +17 -0
  245. atlas_chat-0.1.0.data/data/config/defaults/splash-config.json +16 -0
  246. atlas_chat-0.1.0.dist-info/METADATA +236 -0
  247. atlas_chat-0.1.0.dist-info/RECORD +250 -0
  248. atlas_chat-0.1.0.dist-info/WHEEL +5 -0
  249. atlas_chat-0.1.0.dist-info/entry_points.txt +4 -0
  250. atlas_chat-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,296 @@
1
+ """Tests for generic MCP tool result parsing.
2
+
3
+ These tests verify that Atlas can parse tool results from both:
4
+ 1. MCP responses with structuredContent field
5
+ 2. MCP responses with data only in content[0].text (without structuredContent)
6
+ """
7
+
8
+ import json
9
+ from unittest.mock import AsyncMock, Mock, patch
10
+
11
+ import pytest
12
+
13
+ from atlas.domain.messages.models import ToolCall
14
+ from atlas.modules.mcp_tools.client import MCPToolManager
15
+
16
+
17
+ class MockTextContent:
18
+ """Mock for MCP text content item."""
19
+ def __init__(self, text: str):
20
+ self.text = text
21
+ self.type = "text"
22
+
23
+
24
+ class MockMCPResultWithStructuredContent:
25
+ """Mock MCP result that includes structured_content field."""
26
+ def __init__(self, payload: dict):
27
+ self.content = [MockTextContent(json.dumps(payload))]
28
+ self.structured_content = payload
29
+ self.data = None
30
+ self.is_error = False
31
+ self.meta = None
32
+
33
+
34
+ class MockMCPResultWithoutStructuredContent:
35
+ """Mock MCP result that only has data in content[0].text (no structured_content)."""
36
+ def __init__(self, payload: dict):
37
+ self.content = [MockTextContent(json.dumps(payload))]
38
+ self.structured_content = None
39
+ self.data = None
40
+ self.is_error = False
41
+ self.meta = None
42
+
43
+
44
+ class MockMCPResultPlainText:
45
+ """Mock MCP result with non-JSON plain text content."""
46
+ def __init__(self, text: str = "Just a plain text result"):
47
+ self.content = [MockTextContent(text)]
48
+ self.structured_content = None
49
+ self.data = None
50
+ self.is_error = False
51
+
52
+
53
+ class MockMCPResultMalformedJson:
54
+ """Mock MCP result with malformed JSON in content."""
55
+ def __init__(self, text: str = "{invalid json: true"):
56
+ self.content = [MockTextContent(text)]
57
+ self.structured_content = None
58
+ self.data = None
59
+ self.is_error = False
60
+
61
+
62
+ class TestMCPToolResultParsing:
63
+ """Tests for parsing MCP tool results."""
64
+
65
+ def _create_screenshot_payload(self):
66
+ """Create a sample screenshot tool response payload."""
67
+ return {
68
+ "results": {"content": "Screenshot captured successfully"},
69
+ "artifacts": [
70
+ {
71
+ "name": "screenshot.jpg",
72
+ "b64": "/9j/4AAQSkZJRgABAQEAkACQAAD...",
73
+ "mime": "image/jpeg",
74
+ "size": 160805,
75
+ "description": "screenshot",
76
+ "viewer": "image"
77
+ }
78
+ ],
79
+ "display": {
80
+ "open_canvas": True,
81
+ "primary_file": "screenshot.jpg",
82
+ "mode": "replace",
83
+ "viewer_hint": "image",
84
+ "title": "screenshot.jpg"
85
+ },
86
+ "meta_data": {
87
+ "capture_time": "2024-01-15T10:30:00Z"
88
+ }
89
+ }
90
+
91
+ def test_normalize_mcp_tool_result_with_structured_content(self):
92
+ """Test _normalize_mcp_tool_result with structured_content."""
93
+ manager = MCPToolManager.__new__(MCPToolManager)
94
+ payload = self._create_screenshot_payload()
95
+ raw_result = MockMCPResultWithStructuredContent(payload)
96
+
97
+ normalized = manager._normalize_mcp_tool_result(raw_result)
98
+
99
+ assert "results" in normalized
100
+ assert normalized["results"]["content"] == "Screenshot captured successfully"
101
+
102
+ def test_normalize_mcp_tool_result_without_structured_content(self):
103
+ """Test _normalize_mcp_tool_result falls back to parsing content[0].text."""
104
+ manager = MCPToolManager.__new__(MCPToolManager)
105
+ payload = self._create_screenshot_payload()
106
+ raw_result = MockMCPResultWithoutStructuredContent(payload)
107
+
108
+ normalized = manager._normalize_mcp_tool_result(raw_result)
109
+
110
+ assert "results" in normalized
111
+ assert normalized["results"]["content"] == "Screenshot captured successfully"
112
+
113
+ @pytest.mark.asyncio
114
+ async def test_execute_tool_extracts_artifacts_from_structured_content(self):
115
+ """Test execute_tool extracts artifacts when structured_content is present."""
116
+ server_config = {
117
+ "url": "http://localhost:8001/mcp",
118
+ "transport": "http",
119
+ }
120
+
121
+ with patch('atlas.modules.mcp_tools.client.config_manager') as mock_config_manager:
122
+ mock_config_manager.mcp_config.servers = {"test-server": Mock()}
123
+ mock_config_manager.mcp_config.servers["test-server"].model_dump.return_value = server_config
124
+ mock_config_manager.app_settings.mcp_call_timeout = 120
125
+
126
+ manager = MCPToolManager()
127
+ manager.servers_config = {"test-server": server_config}
128
+
129
+ with patch('atlas.modules.mcp_tools.client.Client') as MockFastMCPClient:
130
+ mock_client_instance = MockFastMCPClient.return_value
131
+ mock_client_instance.__aenter__.return_value = mock_client_instance
132
+
133
+ payload = self._create_screenshot_payload()
134
+ mock_result = MockMCPResultWithStructuredContent(payload)
135
+ mock_client_instance.call_tool = AsyncMock(return_value=mock_result)
136
+
137
+ await manager.initialize_clients()
138
+
139
+ tool_call = ToolCall(id="call_1", name="test-server_capture", arguments={})
140
+ mock_tool = Mock()
141
+ mock_tool.name = "capture"
142
+ manager._tool_index = {
143
+ "test-server_capture": {
144
+ 'server': 'test-server',
145
+ 'tool': mock_tool
146
+ }
147
+ }
148
+
149
+ result = await manager.execute_tool(tool_call)
150
+
151
+ assert result.success is True
152
+ assert len(result.artifacts) == 1
153
+ assert result.artifacts[0]["name"] == "screenshot.jpg"
154
+ assert result.artifacts[0]["b64"] is not None
155
+ assert result.display_config is not None
156
+ assert result.display_config["open_canvas"] is True
157
+ assert result.meta_data is not None
158
+ assert result.meta_data["capture_time"] == "2024-01-15T10:30:00Z"
159
+
160
+ @pytest.mark.asyncio
161
+ async def test_execute_tool_extracts_artifacts_from_content_text_fallback(self):
162
+ """Test execute_tool extracts artifacts when only content[0].text is available.
163
+
164
+ This is the key fix: artifacts/display/meta_data should be extracted
165
+ even when structured_content is not present in the MCP response.
166
+ """
167
+ server_config = {
168
+ "url": "http://localhost:8001/mcp",
169
+ "transport": "http",
170
+ }
171
+
172
+ with patch('atlas.modules.mcp_tools.client.config_manager') as mock_config_manager:
173
+ mock_config_manager.mcp_config.servers = {"test-server": Mock()}
174
+ mock_config_manager.mcp_config.servers["test-server"].model_dump.return_value = server_config
175
+ mock_config_manager.app_settings.mcp_call_timeout = 120
176
+
177
+ manager = MCPToolManager()
178
+ manager.servers_config = {"test-server": server_config}
179
+
180
+ with patch('atlas.modules.mcp_tools.client.Client') as MockFastMCPClient:
181
+ mock_client_instance = MockFastMCPClient.return_value
182
+ mock_client_instance.__aenter__.return_value = mock_client_instance
183
+
184
+ payload = self._create_screenshot_payload()
185
+ # Use result WITHOUT structured_content - only has data in content[0].text
186
+ mock_result = MockMCPResultWithoutStructuredContent(payload)
187
+ mock_client_instance.call_tool = AsyncMock(return_value=mock_result)
188
+
189
+ await manager.initialize_clients()
190
+
191
+ tool_call = ToolCall(id="call_1", name="test-server_capture", arguments={})
192
+ mock_tool = Mock()
193
+ mock_tool.name = "capture"
194
+ manager._tool_index = {
195
+ "test-server_capture": {
196
+ 'server': 'test-server',
197
+ 'tool': mock_tool
198
+ }
199
+ }
200
+
201
+ result = await manager.execute_tool(tool_call)
202
+
203
+ assert result.success is True
204
+ # These assertions verify the fix - artifacts/display/metadata
205
+ # should be extracted even without structured_content
206
+ assert len(result.artifacts) == 1
207
+ assert result.artifacts[0]["name"] == "screenshot.jpg"
208
+ assert result.artifacts[0]["b64"] is not None
209
+ assert result.display_config is not None
210
+ assert result.display_config["open_canvas"] is True
211
+ assert result.meta_data is not None
212
+ assert result.meta_data["capture_time"] == "2024-01-15T10:30:00Z"
213
+
214
+ @pytest.mark.asyncio
215
+ async def test_execute_tool_handles_empty_content(self):
216
+ """Test execute_tool handles result with no parseable content gracefully."""
217
+ server_config = {
218
+ "url": "http://localhost:8001/mcp",
219
+ "transport": "http",
220
+ }
221
+
222
+ with patch('atlas.modules.mcp_tools.client.config_manager') as mock_config_manager:
223
+ mock_config_manager.mcp_config.servers = {"test-server": Mock()}
224
+ mock_config_manager.mcp_config.servers["test-server"].model_dump.return_value = server_config
225
+ mock_config_manager.app_settings.mcp_call_timeout = 120
226
+
227
+ manager = MCPToolManager()
228
+ manager.servers_config = {"test-server": server_config}
229
+
230
+ with patch('atlas.modules.mcp_tools.client.Client') as MockFastMCPClient:
231
+ mock_client_instance = MockFastMCPClient.return_value
232
+ mock_client_instance.__aenter__.return_value = mock_client_instance
233
+
234
+ mock_client_instance.call_tool = AsyncMock(return_value=MockMCPResultPlainText())
235
+
236
+ await manager.initialize_clients()
237
+
238
+ tool_call = ToolCall(id="call_1", name="test-server_plain", arguments={})
239
+ mock_tool = Mock()
240
+ mock_tool.name = "plain"
241
+ manager._tool_index = {
242
+ "test-server_plain": {
243
+ 'server': 'test-server',
244
+ 'tool': mock_tool
245
+ }
246
+ }
247
+
248
+ result = await manager.execute_tool(tool_call)
249
+
250
+ # Should succeed but with no artifacts/display/metadata
251
+ assert result.success is True
252
+ assert result.artifacts == []
253
+ assert result.display_config is None
254
+ assert result.meta_data is None
255
+
256
+ @pytest.mark.asyncio
257
+ async def test_execute_tool_handles_malformed_json_in_content(self):
258
+ """Test execute_tool handles malformed JSON in content[0].text gracefully."""
259
+ server_config = {
260
+ "url": "http://localhost:8001/mcp",
261
+ "transport": "http",
262
+ }
263
+
264
+ with patch('atlas.modules.mcp_tools.client.config_manager') as mock_config_manager:
265
+ mock_config_manager.mcp_config.servers = {"test-server": Mock()}
266
+ mock_config_manager.mcp_config.servers["test-server"].model_dump.return_value = server_config
267
+ mock_config_manager.app_settings.mcp_call_timeout = 120
268
+
269
+ manager = MCPToolManager()
270
+ manager.servers_config = {"test-server": server_config}
271
+
272
+ with patch('atlas.modules.mcp_tools.client.Client') as MockFastMCPClient:
273
+ mock_client_instance = MockFastMCPClient.return_value
274
+ mock_client_instance.__aenter__.return_value = mock_client_instance
275
+
276
+ mock_client_instance.call_tool = AsyncMock(return_value=MockMCPResultMalformedJson())
277
+
278
+ await manager.initialize_clients()
279
+
280
+ tool_call = ToolCall(id="call_1", name="test-server_malformed", arguments={})
281
+ mock_tool = Mock()
282
+ mock_tool.name = "malformed"
283
+ manager._tool_index = {
284
+ "test-server_malformed": {
285
+ 'server': 'test-server',
286
+ 'tool': mock_tool
287
+ }
288
+ }
289
+
290
+ result = await manager.execute_tool(tool_call)
291
+
292
+ # Should still succeed but with no extracted artifacts
293
+ assert result.success is True
294
+ assert result.artifacts == []
295
+ assert result.display_config is None
296
+ assert result.meta_data is None
@@ -0,0 +1,56 @@
1
+ """Tests for core.metrics_logger module."""
2
+
3
+ import logging
4
+ from unittest.mock import MagicMock, patch
5
+
6
+ from atlas.core.metrics_logger import log_metric
7
+
8
+
9
+ def _make_settings(enabled: bool):
10
+ settings = MagicMock()
11
+ settings.feature_metrics_logging_enabled = enabled
12
+ return settings
13
+
14
+
15
+ def _patch_config(enabled: bool):
16
+ mock_cm = MagicMock()
17
+ mock_cm.app_settings = _make_settings(enabled)
18
+ return patch("atlas.modules.config.config_manager", mock_cm)
19
+
20
+
21
+ class TestLogMetric:
22
+ def test_logs_when_enabled(self, caplog):
23
+ with _patch_config(True):
24
+ with caplog.at_level(logging.INFO, logger="atlas.core.metrics_logger"):
25
+ log_metric("llm_call", "user@example.com", model="gpt-4", message_count=5)
26
+ assert "[METRIC]" in caplog.text
27
+ assert "[user@example.com]" in caplog.text
28
+ assert "llm_call" in caplog.text
29
+ assert "model=gpt-4" in caplog.text
30
+ assert "message_count=5" in caplog.text
31
+
32
+ def test_suppressed_when_disabled(self, caplog):
33
+ with _patch_config(False):
34
+ with caplog.at_level(logging.INFO, logger="atlas.core.metrics_logger"):
35
+ log_metric("llm_call", "user@example.com", model="gpt-4")
36
+ assert "[METRIC]" not in caplog.text
37
+
38
+ def test_none_user_email_logs_unknown(self, caplog):
39
+ with _patch_config(True):
40
+ with caplog.at_level(logging.INFO, logger="atlas.core.metrics_logger"):
41
+ log_metric("error", None, error_type="timeout")
42
+ assert "[unknown]" in caplog.text
43
+ assert "error_type=timeout" in caplog.text
44
+
45
+ def test_integer_kwargs_handled(self, caplog):
46
+ with _patch_config(True):
47
+ with caplog.at_level(logging.INFO, logger="atlas.core.metrics_logger"):
48
+ log_metric("file_upload", "u@test.com", file_size=1024, tool_count=0)
49
+ assert "file_size=1024" in caplog.text
50
+ assert "tool_count=0" in caplog.text
51
+
52
+ def test_no_kwargs_produces_clean_output(self, caplog):
53
+ with _patch_config(True):
54
+ with caplog.at_level(logging.INFO, logger="atlas.core.metrics_logger"):
55
+ log_metric("tool_call", "u@test.com")
56
+ assert "[METRIC] [u@test.com] tool_call" in caplog.text