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.
- atlas/__init__.py +40 -0
- atlas/application/__init__.py +7 -0
- atlas/application/chat/__init__.py +7 -0
- atlas/application/chat/agent/__init__.py +10 -0
- atlas/application/chat/agent/act_loop.py +179 -0
- atlas/application/chat/agent/factory.py +142 -0
- atlas/application/chat/agent/protocols.py +46 -0
- atlas/application/chat/agent/react_loop.py +338 -0
- atlas/application/chat/agent/think_act_loop.py +171 -0
- atlas/application/chat/approval_manager.py +151 -0
- atlas/application/chat/elicitation_manager.py +191 -0
- atlas/application/chat/events/__init__.py +1 -0
- atlas/application/chat/events/agent_event_relay.py +112 -0
- atlas/application/chat/modes/__init__.py +1 -0
- atlas/application/chat/modes/agent.py +125 -0
- atlas/application/chat/modes/plain.py +74 -0
- atlas/application/chat/modes/rag.py +81 -0
- atlas/application/chat/modes/tools.py +179 -0
- atlas/application/chat/orchestrator.py +213 -0
- atlas/application/chat/policies/__init__.py +1 -0
- atlas/application/chat/policies/tool_authorization.py +99 -0
- atlas/application/chat/preprocessors/__init__.py +1 -0
- atlas/application/chat/preprocessors/message_builder.py +92 -0
- atlas/application/chat/preprocessors/prompt_override_service.py +104 -0
- atlas/application/chat/service.py +454 -0
- atlas/application/chat/utilities/__init__.py +6 -0
- atlas/application/chat/utilities/error_handler.py +367 -0
- atlas/application/chat/utilities/event_notifier.py +546 -0
- atlas/application/chat/utilities/file_processor.py +613 -0
- atlas/application/chat/utilities/tool_executor.py +789 -0
- atlas/atlas_chat_cli.py +347 -0
- atlas/atlas_client.py +238 -0
- atlas/core/__init__.py +0 -0
- atlas/core/auth.py +205 -0
- atlas/core/authorization_manager.py +27 -0
- atlas/core/capabilities.py +123 -0
- atlas/core/compliance.py +215 -0
- atlas/core/domain_whitelist.py +147 -0
- atlas/core/domain_whitelist_middleware.py +82 -0
- atlas/core/http_client.py +28 -0
- atlas/core/log_sanitizer.py +102 -0
- atlas/core/metrics_logger.py +59 -0
- atlas/core/middleware.py +131 -0
- atlas/core/otel_config.py +242 -0
- atlas/core/prompt_risk.py +200 -0
- atlas/core/rate_limit.py +0 -0
- atlas/core/rate_limit_middleware.py +64 -0
- atlas/core/security_headers_middleware.py +51 -0
- atlas/domain/__init__.py +37 -0
- atlas/domain/chat/__init__.py +1 -0
- atlas/domain/chat/dtos.py +85 -0
- atlas/domain/errors.py +96 -0
- atlas/domain/messages/__init__.py +12 -0
- atlas/domain/messages/models.py +160 -0
- atlas/domain/rag_mcp_service.py +664 -0
- atlas/domain/sessions/__init__.py +7 -0
- atlas/domain/sessions/models.py +36 -0
- atlas/domain/unified_rag_service.py +371 -0
- atlas/infrastructure/__init__.py +10 -0
- atlas/infrastructure/app_factory.py +135 -0
- atlas/infrastructure/events/__init__.py +1 -0
- atlas/infrastructure/events/cli_event_publisher.py +140 -0
- atlas/infrastructure/events/websocket_publisher.py +140 -0
- atlas/infrastructure/sessions/in_memory_repository.py +56 -0
- atlas/infrastructure/transport/__init__.py +7 -0
- atlas/infrastructure/transport/websocket_connection_adapter.py +33 -0
- atlas/init_cli.py +226 -0
- atlas/interfaces/__init__.py +15 -0
- atlas/interfaces/events.py +134 -0
- atlas/interfaces/llm.py +54 -0
- atlas/interfaces/rag.py +40 -0
- atlas/interfaces/sessions.py +75 -0
- atlas/interfaces/tools.py +57 -0
- atlas/interfaces/transport.py +24 -0
- atlas/main.py +564 -0
- atlas/mcp/api_key_demo/README.md +76 -0
- atlas/mcp/api_key_demo/main.py +172 -0
- atlas/mcp/api_key_demo/run.sh +56 -0
- atlas/mcp/basictable/main.py +147 -0
- atlas/mcp/calculator/main.py +149 -0
- atlas/mcp/code-executor/execution_engine.py +98 -0
- atlas/mcp/code-executor/execution_environment.py +95 -0
- atlas/mcp/code-executor/main.py +528 -0
- atlas/mcp/code-executor/result_processing.py +276 -0
- atlas/mcp/code-executor/script_generation.py +195 -0
- atlas/mcp/code-executor/security_checker.py +140 -0
- atlas/mcp/corporate_cars/main.py +437 -0
- atlas/mcp/csv_reporter/main.py +545 -0
- atlas/mcp/duckduckgo/main.py +182 -0
- atlas/mcp/elicitation_demo/README.md +171 -0
- atlas/mcp/elicitation_demo/main.py +262 -0
- atlas/mcp/env-demo/README.md +158 -0
- atlas/mcp/env-demo/main.py +199 -0
- atlas/mcp/file_size_test/main.py +284 -0
- atlas/mcp/filesystem/main.py +348 -0
- atlas/mcp/image_demo/main.py +113 -0
- atlas/mcp/image_demo/requirements.txt +4 -0
- atlas/mcp/logging_demo/README.md +72 -0
- atlas/mcp/logging_demo/main.py +103 -0
- atlas/mcp/many_tools_demo/main.py +50 -0
- atlas/mcp/order_database/__init__.py +0 -0
- atlas/mcp/order_database/main.py +369 -0
- atlas/mcp/order_database/signal_data.csv +1001 -0
- atlas/mcp/pdfbasic/main.py +394 -0
- atlas/mcp/pptx_generator/main.py +760 -0
- atlas/mcp/pptx_generator/requirements.txt +13 -0
- atlas/mcp/pptx_generator/run_test.sh +1 -0
- atlas/mcp/pptx_generator/test_pptx_generator_security.py +169 -0
- atlas/mcp/progress_demo/main.py +167 -0
- atlas/mcp/progress_updates_demo/QUICKSTART.md +273 -0
- atlas/mcp/progress_updates_demo/README.md +120 -0
- atlas/mcp/progress_updates_demo/main.py +497 -0
- atlas/mcp/prompts/main.py +222 -0
- atlas/mcp/public_demo/main.py +189 -0
- atlas/mcp/sampling_demo/README.md +169 -0
- atlas/mcp/sampling_demo/main.py +234 -0
- atlas/mcp/thinking/main.py +77 -0
- atlas/mcp/tool_planner/main.py +240 -0
- atlas/mcp/ui-demo/badmesh.png +0 -0
- atlas/mcp/ui-demo/main.py +383 -0
- atlas/mcp/ui-demo/templates/button_demo.html +32 -0
- atlas/mcp/ui-demo/templates/data_visualization.html +32 -0
- atlas/mcp/ui-demo/templates/form_demo.html +28 -0
- atlas/mcp/username-override-demo/README.md +320 -0
- atlas/mcp/username-override-demo/main.py +308 -0
- atlas/modules/__init__.py +0 -0
- atlas/modules/config/__init__.py +34 -0
- atlas/modules/config/cli.py +231 -0
- atlas/modules/config/config_manager.py +1096 -0
- atlas/modules/file_storage/__init__.py +22 -0
- atlas/modules/file_storage/cli.py +330 -0
- atlas/modules/file_storage/content_extractor.py +290 -0
- atlas/modules/file_storage/manager.py +295 -0
- atlas/modules/file_storage/mock_s3_client.py +402 -0
- atlas/modules/file_storage/s3_client.py +417 -0
- atlas/modules/llm/__init__.py +19 -0
- atlas/modules/llm/caller.py +287 -0
- atlas/modules/llm/litellm_caller.py +675 -0
- atlas/modules/llm/models.py +19 -0
- atlas/modules/mcp_tools/__init__.py +17 -0
- atlas/modules/mcp_tools/client.py +2123 -0
- atlas/modules/mcp_tools/token_storage.py +556 -0
- atlas/modules/prompts/prompt_provider.py +130 -0
- atlas/modules/rag/__init__.py +24 -0
- atlas/modules/rag/atlas_rag_client.py +336 -0
- atlas/modules/rag/client.py +129 -0
- atlas/routes/admin_routes.py +865 -0
- atlas/routes/config_routes.py +484 -0
- atlas/routes/feedback_routes.py +361 -0
- atlas/routes/files_routes.py +274 -0
- atlas/routes/health_routes.py +40 -0
- atlas/routes/mcp_auth_routes.py +223 -0
- atlas/server_cli.py +164 -0
- atlas/tests/conftest.py +20 -0
- atlas/tests/integration/test_mcp_auth_integration.py +152 -0
- atlas/tests/manual_test_sampling.py +87 -0
- atlas/tests/modules/mcp_tools/test_client_auth.py +226 -0
- atlas/tests/modules/mcp_tools/test_client_env.py +191 -0
- atlas/tests/test_admin_mcp_server_management_routes.py +141 -0
- atlas/tests/test_agent_roa.py +135 -0
- atlas/tests/test_app_factory_smoke.py +47 -0
- atlas/tests/test_approval_manager.py +439 -0
- atlas/tests/test_atlas_client.py +188 -0
- atlas/tests/test_atlas_rag_client.py +447 -0
- atlas/tests/test_atlas_rag_integration.py +224 -0
- atlas/tests/test_attach_file_flow.py +287 -0
- atlas/tests/test_auth_utils.py +165 -0
- atlas/tests/test_backend_public_url.py +185 -0
- atlas/tests/test_banner_logging.py +287 -0
- atlas/tests/test_capability_tokens_and_injection.py +203 -0
- atlas/tests/test_compliance_level.py +54 -0
- atlas/tests/test_compliance_manager.py +253 -0
- atlas/tests/test_config_manager.py +617 -0
- atlas/tests/test_config_manager_paths.py +12 -0
- atlas/tests/test_core_auth.py +18 -0
- atlas/tests/test_core_utils.py +190 -0
- atlas/tests/test_docker_env_sync.py +202 -0
- atlas/tests/test_domain_errors.py +329 -0
- atlas/tests/test_domain_whitelist.py +359 -0
- atlas/tests/test_elicitation_manager.py +408 -0
- atlas/tests/test_elicitation_routing.py +296 -0
- atlas/tests/test_env_demo_server.py +88 -0
- atlas/tests/test_error_classification.py +113 -0
- atlas/tests/test_error_flow_integration.py +116 -0
- atlas/tests/test_feedback_routes.py +333 -0
- atlas/tests/test_file_content_extraction.py +1134 -0
- atlas/tests/test_file_extraction_routes.py +158 -0
- atlas/tests/test_file_library.py +107 -0
- atlas/tests/test_file_manager_unit.py +18 -0
- atlas/tests/test_health_route.py +49 -0
- atlas/tests/test_http_client_stub.py +8 -0
- atlas/tests/test_imports_smoke.py +30 -0
- atlas/tests/test_interfaces_llm_response.py +9 -0
- atlas/tests/test_issue_access_denied_fix.py +136 -0
- atlas/tests/test_llm_env_expansion.py +836 -0
- atlas/tests/test_log_level_sensitive_data.py +285 -0
- atlas/tests/test_mcp_auth_routes.py +341 -0
- atlas/tests/test_mcp_client_auth.py +331 -0
- atlas/tests/test_mcp_data_injection.py +270 -0
- atlas/tests/test_mcp_get_authorized_servers.py +95 -0
- atlas/tests/test_mcp_hot_reload.py +512 -0
- atlas/tests/test_mcp_image_content.py +424 -0
- atlas/tests/test_mcp_logging.py +172 -0
- atlas/tests/test_mcp_progress_updates.py +313 -0
- atlas/tests/test_mcp_prompt_override_system_prompt.py +102 -0
- atlas/tests/test_mcp_prompts_server.py +39 -0
- atlas/tests/test_mcp_tool_result_parsing.py +296 -0
- atlas/tests/test_metrics_logger.py +56 -0
- atlas/tests/test_middleware_auth.py +379 -0
- atlas/tests/test_prompt_risk_and_acl.py +141 -0
- atlas/tests/test_rag_mcp_aggregator.py +204 -0
- atlas/tests/test_rag_mcp_service.py +224 -0
- atlas/tests/test_rate_limit_middleware.py +45 -0
- atlas/tests/test_routes_config_smoke.py +60 -0
- atlas/tests/test_routes_files_download_token.py +41 -0
- atlas/tests/test_routes_files_health.py +18 -0
- atlas/tests/test_runtime_imports.py +53 -0
- atlas/tests/test_sampling_integration.py +482 -0
- atlas/tests/test_security_admin_routes.py +61 -0
- atlas/tests/test_security_capability_tokens.py +65 -0
- atlas/tests/test_security_file_stats_scope.py +21 -0
- atlas/tests/test_security_header_injection.py +191 -0
- atlas/tests/test_security_headers_and_filename.py +63 -0
- atlas/tests/test_shared_session_repository.py +101 -0
- atlas/tests/test_system_prompt_loading.py +181 -0
- atlas/tests/test_token_storage.py +505 -0
- atlas/tests/test_tool_approval_config.py +93 -0
- atlas/tests/test_tool_approval_utils.py +356 -0
- atlas/tests/test_tool_authorization_group_filtering.py +223 -0
- atlas/tests/test_tool_details_in_config.py +108 -0
- atlas/tests/test_tool_planner.py +300 -0
- atlas/tests/test_unified_rag_service.py +398 -0
- atlas/tests/test_username_override_in_approval.py +258 -0
- atlas/tests/test_websocket_auth_header.py +168 -0
- atlas/version.py +6 -0
- atlas_chat-0.1.0.data/data/.env.example +253 -0
- atlas_chat-0.1.0.data/data/config/defaults/compliance-levels.json +44 -0
- atlas_chat-0.1.0.data/data/config/defaults/domain-whitelist.json +123 -0
- atlas_chat-0.1.0.data/data/config/defaults/file-extractors.json +74 -0
- atlas_chat-0.1.0.data/data/config/defaults/help-config.json +198 -0
- atlas_chat-0.1.0.data/data/config/defaults/llmconfig-buggy.yml +11 -0
- atlas_chat-0.1.0.data/data/config/defaults/llmconfig.yml +19 -0
- atlas_chat-0.1.0.data/data/config/defaults/mcp.json +138 -0
- atlas_chat-0.1.0.data/data/config/defaults/rag-sources.json +17 -0
- atlas_chat-0.1.0.data/data/config/defaults/splash-config.json +16 -0
- atlas_chat-0.1.0.dist-info/METADATA +236 -0
- atlas_chat-0.1.0.dist-info/RECORD +250 -0
- atlas_chat-0.1.0.dist-info/WHEEL +5 -0
- atlas_chat-0.1.0.dist-info/entry_points.txt +4 -0
- 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
|