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,482 @@
|
|
|
1
|
+
"""Integration test for MCP sampling functionality."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from atlas.domain.messages.models import ToolCall
|
|
8
|
+
from atlas.modules.mcp_tools.client import MCPToolManager
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestSamplingIntegration:
|
|
12
|
+
"""Integration tests for MCP sampling."""
|
|
13
|
+
|
|
14
|
+
@pytest.mark.asyncio
|
|
15
|
+
async def test_sampling_handler_basic(self):
|
|
16
|
+
"""Test that sampling handler can be created and configured."""
|
|
17
|
+
manager = MCPToolManager()
|
|
18
|
+
|
|
19
|
+
# Create a sampling handler
|
|
20
|
+
handler = manager._create_sampling_handler("test_server")
|
|
21
|
+
|
|
22
|
+
# Verify handler is callable
|
|
23
|
+
assert callable(handler)
|
|
24
|
+
|
|
25
|
+
@pytest.mark.asyncio
|
|
26
|
+
async def test_sampling_context_manager(self):
|
|
27
|
+
"""Test the sampling context manager."""
|
|
28
|
+
manager = MCPToolManager()
|
|
29
|
+
|
|
30
|
+
# Create a mock tool call and update callback
|
|
31
|
+
tool_call = ToolCall(
|
|
32
|
+
id="test_tool_call_1",
|
|
33
|
+
name="test_tool",
|
|
34
|
+
arguments={}
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
update_cb = AsyncMock()
|
|
38
|
+
|
|
39
|
+
# Use the context manager
|
|
40
|
+
async with manager._use_sampling_context("test_server", tool_call, update_cb):
|
|
41
|
+
# Verify routing is set up with composite key (server_name, tool_call.id)
|
|
42
|
+
from atlas.modules.mcp_tools.client import _SAMPLING_ROUTING
|
|
43
|
+
routing_key = ("test_server", "test_tool_call_1")
|
|
44
|
+
assert routing_key in _SAMPLING_ROUTING
|
|
45
|
+
routing = _SAMPLING_ROUTING[routing_key]
|
|
46
|
+
assert routing.server_name == "test_server"
|
|
47
|
+
assert routing.tool_call == tool_call
|
|
48
|
+
assert routing.update_cb == update_cb
|
|
49
|
+
|
|
50
|
+
# Verify routing is cleaned up
|
|
51
|
+
assert routing_key not in _SAMPLING_ROUTING
|
|
52
|
+
|
|
53
|
+
@pytest.mark.asyncio
|
|
54
|
+
async def test_sampling_handler_with_routing(self):
|
|
55
|
+
"""Test sampling handler with routing context."""
|
|
56
|
+
manager = MCPToolManager()
|
|
57
|
+
|
|
58
|
+
# Create a mock tool call
|
|
59
|
+
tool_call = ToolCall(
|
|
60
|
+
id="test_tool_call_1",
|
|
61
|
+
name="test_tool",
|
|
62
|
+
arguments={}
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
update_cb = AsyncMock()
|
|
66
|
+
|
|
67
|
+
# Mock the LLM caller - patch where it's imported in the handler
|
|
68
|
+
with patch('atlas.modules.llm.litellm_caller.LiteLLMCaller') as mock_llm_class:
|
|
69
|
+
mock_llm_instance = AsyncMock()
|
|
70
|
+
mock_llm_instance.call_plain = AsyncMock(return_value="Mocked LLM response")
|
|
71
|
+
mock_llm_class.return_value = mock_llm_instance
|
|
72
|
+
|
|
73
|
+
# Set up routing context
|
|
74
|
+
async with manager._use_sampling_context("test_server", tool_call, update_cb):
|
|
75
|
+
handler = manager._create_sampling_handler("test_server")
|
|
76
|
+
|
|
77
|
+
# Create mock sampling params
|
|
78
|
+
mock_params = MagicMock()
|
|
79
|
+
mock_params.systemPrompt = "You are helpful"
|
|
80
|
+
mock_params.temperature = 0.7
|
|
81
|
+
mock_params.maxTokens = 500
|
|
82
|
+
mock_params.modelPreferences = None
|
|
83
|
+
|
|
84
|
+
# Call the handler
|
|
85
|
+
result = await handler(
|
|
86
|
+
messages=["Test message"],
|
|
87
|
+
params=mock_params
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Verify result
|
|
91
|
+
assert result.content.text == "Mocked LLM response"
|
|
92
|
+
|
|
93
|
+
# Verify LLM was called correctly
|
|
94
|
+
mock_llm_instance.call_plain.assert_called_once()
|
|
95
|
+
call_args = mock_llm_instance.call_plain.call_args
|
|
96
|
+
assert call_args.kwargs.get('temperature') == 0.7
|
|
97
|
+
assert call_args.kwargs.get('max_tokens') == 500
|
|
98
|
+
|
|
99
|
+
@pytest.mark.asyncio
|
|
100
|
+
async def test_sampling_without_routing_context(self):
|
|
101
|
+
"""Test that sampling fails without routing context."""
|
|
102
|
+
manager = MCPToolManager()
|
|
103
|
+
|
|
104
|
+
handler = manager._create_sampling_handler("test_server")
|
|
105
|
+
|
|
106
|
+
# Try to call handler without routing context
|
|
107
|
+
with pytest.raises(Exception, match="No routing context"):
|
|
108
|
+
await handler(messages=["Test"], params=None)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class TestSamplingDemoTools:
|
|
112
|
+
"""Integration tests for sampling_demo MCP server tools."""
|
|
113
|
+
|
|
114
|
+
@pytest.mark.asyncio
|
|
115
|
+
async def test_summarize_text_tool(self):
|
|
116
|
+
"""Test summarize_text tool with basic sampling."""
|
|
117
|
+
import sys
|
|
118
|
+
from pathlib import Path
|
|
119
|
+
|
|
120
|
+
from fastmcp import Client
|
|
121
|
+
from fastmcp.client.transports import StdioTransport
|
|
122
|
+
from mcp.types import CreateMessageResult, TextContent
|
|
123
|
+
|
|
124
|
+
# Create mock sampling handler
|
|
125
|
+
async def mock_sampling_handler(messages, params=None, context=None):
|
|
126
|
+
# Verify basic sampling call
|
|
127
|
+
assert len(messages) > 0
|
|
128
|
+
return CreateMessageResult(
|
|
129
|
+
role="assistant",
|
|
130
|
+
content=TextContent(type="text", text="This is a concise summary of the text."),
|
|
131
|
+
model="test-model"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Get absolute path to the sampling demo server
|
|
135
|
+
server_path = Path(__file__).parent.parent / "mcp" / "sampling_demo" / "main.py"
|
|
136
|
+
|
|
137
|
+
# Use StdioTransport explicitly
|
|
138
|
+
transport = StdioTransport(
|
|
139
|
+
command=sys.executable,
|
|
140
|
+
args=[str(server_path)]
|
|
141
|
+
)
|
|
142
|
+
client = Client(transport, sampling_handler=mock_sampling_handler)
|
|
143
|
+
|
|
144
|
+
async with client:
|
|
145
|
+
result = await client.call_tool(
|
|
146
|
+
"summarize_text",
|
|
147
|
+
{"text": "Long text that needs summarization..."}
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Verify tool returns the sampled text
|
|
151
|
+
assert "summary" in result.content[0].text.lower()
|
|
152
|
+
|
|
153
|
+
@pytest.mark.asyncio
|
|
154
|
+
async def test_analyze_sentiment_tool(self):
|
|
155
|
+
"""Test analyze_sentiment tool with system prompt and low temperature."""
|
|
156
|
+
import sys
|
|
157
|
+
from pathlib import Path
|
|
158
|
+
|
|
159
|
+
from fastmcp import Client
|
|
160
|
+
from fastmcp.client.transports import StdioTransport
|
|
161
|
+
from mcp.types import CreateMessageResult, TextContent
|
|
162
|
+
|
|
163
|
+
captured_params = {}
|
|
164
|
+
|
|
165
|
+
async def mock_sampling_handler(messages, params=None, context=None):
|
|
166
|
+
# Capture params to verify system prompt and temperature
|
|
167
|
+
if params:
|
|
168
|
+
captured_params['system_prompt'] = getattr(params, 'systemPrompt', None)
|
|
169
|
+
captured_params['temperature'] = getattr(params, 'temperature', None)
|
|
170
|
+
|
|
171
|
+
return CreateMessageResult(
|
|
172
|
+
role="assistant",
|
|
173
|
+
content=TextContent(
|
|
174
|
+
type="text",
|
|
175
|
+
text="Positive sentiment - the text expresses enthusiasm and satisfaction."
|
|
176
|
+
),
|
|
177
|
+
model="test-model"
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
server_path = Path(__file__).parent.parent / "mcp" / "sampling_demo" / "main.py"
|
|
181
|
+
transport = StdioTransport(
|
|
182
|
+
command=sys.executable,
|
|
183
|
+
args=[str(server_path)]
|
|
184
|
+
)
|
|
185
|
+
client = Client(transport, sampling_handler=mock_sampling_handler)
|
|
186
|
+
|
|
187
|
+
async with client:
|
|
188
|
+
result = await client.call_tool(
|
|
189
|
+
"analyze_sentiment",
|
|
190
|
+
{"text": "I love this product!"}
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# Verify tool used system prompt and low temperature
|
|
194
|
+
assert captured_params.get('system_prompt') is not None
|
|
195
|
+
assert "sentiment" in captured_params['system_prompt'].lower()
|
|
196
|
+
assert captured_params.get('temperature') == 0.3
|
|
197
|
+
assert "sentiment" in result.content[0].text.lower()
|
|
198
|
+
|
|
199
|
+
@pytest.mark.asyncio
|
|
200
|
+
async def test_generate_code_tool(self):
|
|
201
|
+
"""Test generate_code tool with model preferences."""
|
|
202
|
+
import sys
|
|
203
|
+
from pathlib import Path
|
|
204
|
+
|
|
205
|
+
from fastmcp import Client
|
|
206
|
+
from fastmcp.client.transports import StdioTransport
|
|
207
|
+
from mcp.types import CreateMessageResult, TextContent
|
|
208
|
+
|
|
209
|
+
captured_params = {}
|
|
210
|
+
|
|
211
|
+
async def mock_sampling_handler(messages, params=None, context=None):
|
|
212
|
+
# Capture model preferences
|
|
213
|
+
if params:
|
|
214
|
+
captured_params['model_preferences'] = getattr(params, 'modelPreferences', None)
|
|
215
|
+
captured_params['max_tokens'] = getattr(params, 'maxTokens', None)
|
|
216
|
+
captured_params['temperature'] = getattr(params, 'temperature', None)
|
|
217
|
+
|
|
218
|
+
return CreateMessageResult(
|
|
219
|
+
role="assistant",
|
|
220
|
+
content=TextContent(
|
|
221
|
+
type="text",
|
|
222
|
+
text="def fibonacci(n):\n if n <= 1:\n return n\n return fibonacci(n-1) + fibonacci(n-2)"
|
|
223
|
+
),
|
|
224
|
+
model="test-model"
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
server_path = Path(__file__).parent.parent / "mcp" / "sampling_demo" / "main.py"
|
|
228
|
+
transport = StdioTransport(
|
|
229
|
+
command=sys.executable,
|
|
230
|
+
args=[str(server_path)]
|
|
231
|
+
)
|
|
232
|
+
client = Client(transport, sampling_handler=mock_sampling_handler)
|
|
233
|
+
|
|
234
|
+
async with client:
|
|
235
|
+
result = await client.call_tool(
|
|
236
|
+
"generate_code",
|
|
237
|
+
{
|
|
238
|
+
"description": "calculate fibonacci numbers",
|
|
239
|
+
"language": "Python"
|
|
240
|
+
}
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
# Verify model preferences were set
|
|
244
|
+
assert captured_params.get('model_preferences') is not None
|
|
245
|
+
# ModelPreferences can be an object or list, just verify it exists
|
|
246
|
+
assert captured_params['model_preferences'] is not None
|
|
247
|
+
# Verify reasonable parameters for code generation
|
|
248
|
+
assert captured_params.get('max_tokens') == 1000
|
|
249
|
+
assert captured_params.get('temperature') == 0.7
|
|
250
|
+
assert "def" in result.content[0].text or "fibonacci" in result.content[0].text.lower()
|
|
251
|
+
|
|
252
|
+
@pytest.mark.asyncio
|
|
253
|
+
async def test_creative_story_tool(self):
|
|
254
|
+
"""Test creative_story tool with high temperature."""
|
|
255
|
+
import sys
|
|
256
|
+
from pathlib import Path
|
|
257
|
+
|
|
258
|
+
from fastmcp import Client
|
|
259
|
+
from fastmcp.client.transports import StdioTransport
|
|
260
|
+
from mcp.types import CreateMessageResult, TextContent
|
|
261
|
+
|
|
262
|
+
captured_params = {}
|
|
263
|
+
|
|
264
|
+
async def mock_sampling_handler(messages, params=None, context=None):
|
|
265
|
+
# Capture temperature to verify high value for creativity
|
|
266
|
+
if params:
|
|
267
|
+
captured_params['temperature'] = getattr(params, 'temperature', None)
|
|
268
|
+
captured_params['max_tokens'] = getattr(params, 'maxTokens', None)
|
|
269
|
+
|
|
270
|
+
return CreateMessageResult(
|
|
271
|
+
role="assistant",
|
|
272
|
+
content=TextContent(
|
|
273
|
+
type="text",
|
|
274
|
+
text="Once upon a time, in a world of circuits and code, there lived a robot who dreamed of painting..."
|
|
275
|
+
),
|
|
276
|
+
model="test-model"
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
server_path = Path(__file__).parent.parent / "mcp" / "sampling_demo" / "main.py"
|
|
280
|
+
transport = StdioTransport(
|
|
281
|
+
command=sys.executable,
|
|
282
|
+
args=[str(server_path)]
|
|
283
|
+
)
|
|
284
|
+
client = Client(transport, sampling_handler=mock_sampling_handler)
|
|
285
|
+
|
|
286
|
+
async with client:
|
|
287
|
+
result = await client.call_tool(
|
|
288
|
+
"creative_story",
|
|
289
|
+
{"prompt": "a robot learning to paint"}
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# Verify high temperature for creativity
|
|
293
|
+
assert captured_params.get('temperature') == 0.9
|
|
294
|
+
assert captured_params.get('max_tokens') == 500
|
|
295
|
+
# Story should be present
|
|
296
|
+
assert len(result.content[0].text) > 0
|
|
297
|
+
|
|
298
|
+
@pytest.mark.asyncio
|
|
299
|
+
async def test_multi_turn_conversation_tool(self):
|
|
300
|
+
"""Test multi_turn_conversation tool with SamplingMessage objects."""
|
|
301
|
+
import sys
|
|
302
|
+
from pathlib import Path
|
|
303
|
+
|
|
304
|
+
from fastmcp import Client
|
|
305
|
+
from fastmcp.client.transports import StdioTransport
|
|
306
|
+
from mcp.types import CreateMessageResult, TextContent
|
|
307
|
+
|
|
308
|
+
call_count = 0
|
|
309
|
+
|
|
310
|
+
async def mock_sampling_handler(messages, params=None, context=None):
|
|
311
|
+
nonlocal call_count
|
|
312
|
+
call_count += 1
|
|
313
|
+
|
|
314
|
+
# Verify messages include SamplingMessage objects for multi-turn
|
|
315
|
+
if call_count == 1:
|
|
316
|
+
# First turn - initial question
|
|
317
|
+
assert len(messages) == 1
|
|
318
|
+
return CreateMessageResult(
|
|
319
|
+
role="assistant",
|
|
320
|
+
content=TextContent(
|
|
321
|
+
type="text",
|
|
322
|
+
text="Key aspects to consider: history, current state, and future trends."
|
|
323
|
+
),
|
|
324
|
+
model="test-model"
|
|
325
|
+
)
|
|
326
|
+
else:
|
|
327
|
+
# Second turn - should have conversation history
|
|
328
|
+
assert len(messages) >= 3 # User, Assistant, User
|
|
329
|
+
return CreateMessageResult(
|
|
330
|
+
role="assistant",
|
|
331
|
+
content=TextContent(
|
|
332
|
+
type="text",
|
|
333
|
+
text="The most important point is understanding the historical context."
|
|
334
|
+
),
|
|
335
|
+
model="test-model"
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
server_path = Path(__file__).parent.parent / "mcp" / "sampling_demo" / "main.py"
|
|
339
|
+
transport = StdioTransport(
|
|
340
|
+
command=sys.executable,
|
|
341
|
+
args=[str(server_path)]
|
|
342
|
+
)
|
|
343
|
+
client = Client(transport, sampling_handler=mock_sampling_handler)
|
|
344
|
+
|
|
345
|
+
async with client:
|
|
346
|
+
result = await client.call_tool(
|
|
347
|
+
"multi_turn_conversation",
|
|
348
|
+
{"topic": "artificial intelligence"}
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
# Verify two sampling calls were made
|
|
352
|
+
assert call_count == 2
|
|
353
|
+
# Result should contain both turns
|
|
354
|
+
text = result.content[0].text
|
|
355
|
+
assert "Discussion" in text or "Initial Response" in text
|
|
356
|
+
|
|
357
|
+
@pytest.mark.asyncio
|
|
358
|
+
async def test_research_question_tool(self):
|
|
359
|
+
"""Test research_question tool with multi-step agentic workflow."""
|
|
360
|
+
import sys
|
|
361
|
+
from pathlib import Path
|
|
362
|
+
|
|
363
|
+
from fastmcp import Client
|
|
364
|
+
from fastmcp.client.transports import StdioTransport
|
|
365
|
+
from mcp.types import CreateMessageResult, TextContent
|
|
366
|
+
|
|
367
|
+
call_count = 0
|
|
368
|
+
captured_calls = []
|
|
369
|
+
|
|
370
|
+
async def mock_sampling_handler(messages, params=None, context=None):
|
|
371
|
+
nonlocal call_count
|
|
372
|
+
call_count += 1
|
|
373
|
+
|
|
374
|
+
# Capture each call for verification
|
|
375
|
+
captured_calls.append({
|
|
376
|
+
'messages': messages,
|
|
377
|
+
'params': params
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
if call_count == 1:
|
|
381
|
+
# First call: break down question
|
|
382
|
+
return CreateMessageResult(
|
|
383
|
+
role="assistant",
|
|
384
|
+
content=TextContent(
|
|
385
|
+
type="text",
|
|
386
|
+
text="1. What are renewable energy sources?\n2. What are their benefits?\n3. What are the challenges?"
|
|
387
|
+
),
|
|
388
|
+
model="test-model"
|
|
389
|
+
)
|
|
390
|
+
else:
|
|
391
|
+
# Second call: comprehensive answer
|
|
392
|
+
return CreateMessageResult(
|
|
393
|
+
role="assistant",
|
|
394
|
+
content=TextContent(
|
|
395
|
+
type="text",
|
|
396
|
+
text="Renewable energy sources include solar, wind, and hydro. Benefits include sustainability, reduced emissions, and energy independence."
|
|
397
|
+
),
|
|
398
|
+
model="test-model"
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
server_path = Path(__file__).parent.parent / "mcp" / "sampling_demo" / "main.py"
|
|
402
|
+
transport = StdioTransport(
|
|
403
|
+
command=sys.executable,
|
|
404
|
+
args=[str(server_path)]
|
|
405
|
+
)
|
|
406
|
+
client = Client(transport, sampling_handler=mock_sampling_handler)
|
|
407
|
+
|
|
408
|
+
async with client:
|
|
409
|
+
result = await client.call_tool(
|
|
410
|
+
"research_question",
|
|
411
|
+
{"question": "What are the benefits of renewable energy?"}
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
# Verify two-step research process
|
|
415
|
+
assert call_count == 2
|
|
416
|
+
# First call should be about breaking down the question
|
|
417
|
+
assert "break down" in str(captured_calls[0]['messages']).lower()
|
|
418
|
+
# Second call should reference the breakdown
|
|
419
|
+
assert len(str(captured_calls[1]['messages'])) > len(str(captured_calls[0]['messages']))
|
|
420
|
+
# Result should contain analysis and answer
|
|
421
|
+
text = result.content[0].text
|
|
422
|
+
assert "Research Question" in text or "Analysis" in text or "Answer" in text
|
|
423
|
+
|
|
424
|
+
@pytest.mark.asyncio
|
|
425
|
+
async def test_translate_and_explain_tool(self):
|
|
426
|
+
"""Test translate_and_explain tool with sequential sampling workflow."""
|
|
427
|
+
import sys
|
|
428
|
+
from pathlib import Path
|
|
429
|
+
|
|
430
|
+
from fastmcp import Client
|
|
431
|
+
from fastmcp.client.transports import StdioTransport
|
|
432
|
+
from mcp.types import CreateMessageResult, TextContent
|
|
433
|
+
|
|
434
|
+
call_count = 0
|
|
435
|
+
|
|
436
|
+
async def mock_sampling_handler(messages, params=None, context=None):
|
|
437
|
+
nonlocal call_count
|
|
438
|
+
call_count += 1
|
|
439
|
+
|
|
440
|
+
if call_count == 1:
|
|
441
|
+
# First call: translation
|
|
442
|
+
return CreateMessageResult(
|
|
443
|
+
role="assistant",
|
|
444
|
+
content=TextContent(
|
|
445
|
+
type="text",
|
|
446
|
+
text="Hola, ¿cómo estás?"
|
|
447
|
+
),
|
|
448
|
+
model="test-model"
|
|
449
|
+
)
|
|
450
|
+
else:
|
|
451
|
+
# Second call: explanation
|
|
452
|
+
return CreateMessageResult(
|
|
453
|
+
role="assistant",
|
|
454
|
+
content=TextContent(
|
|
455
|
+
type="text",
|
|
456
|
+
text="Translation uses informal 'tú' form. 'Cómo estás' is the standard greeting in Spanish."
|
|
457
|
+
),
|
|
458
|
+
model="test-model"
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
server_path = Path(__file__).parent.parent / "mcp" / "sampling_demo" / "main.py"
|
|
462
|
+
transport = StdioTransport(
|
|
463
|
+
command=sys.executable,
|
|
464
|
+
args=[str(server_path)]
|
|
465
|
+
)
|
|
466
|
+
client = Client(transport, sampling_handler=mock_sampling_handler)
|
|
467
|
+
|
|
468
|
+
async with client:
|
|
469
|
+
result = await client.call_tool(
|
|
470
|
+
"translate_and_explain",
|
|
471
|
+
{
|
|
472
|
+
"text": "Hello, how are you?",
|
|
473
|
+
"target_language": "Spanish"
|
|
474
|
+
}
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
# Verify sequential workflow (two calls)
|
|
478
|
+
assert call_count == 2
|
|
479
|
+
# Result should contain both translation and explanation
|
|
480
|
+
text = result.content[0].text
|
|
481
|
+
assert "Translation" in text or "Hola" in text
|
|
482
|
+
assert "Notes" in text or "explain" in text.lower() or "form" in text.lower()
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from main import app
|
|
2
|
+
from starlette.testclient import TestClient
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_admin_routes_require_admin(monkeypatch):
|
|
6
|
+
client = TestClient(app)
|
|
7
|
+
|
|
8
|
+
# Non-admin user should be redirected/forbidden depending on middleware
|
|
9
|
+
# Provide a non-admin email
|
|
10
|
+
r = client.get("/admin/", headers={"X-User-Email": "user@example.com"})
|
|
11
|
+
assert r.status_code in (302, 403)
|
|
12
|
+
|
|
13
|
+
# Admin access when user is in admin group (mocked via config in core.auth)
|
|
14
|
+
r2 = client.get("/admin/", headers={"X-User-Email": "admin@example.com"})
|
|
15
|
+
# In debug mode off, should allow if auth module says admin@example.com is admin
|
|
16
|
+
assert r2.status_code == 200
|
|
17
|
+
data = r2.json()
|
|
18
|
+
assert data.get("available_endpoints") is not None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_system_status_endpoint():
|
|
22
|
+
"""Test the system status endpoint returns expected data structure."""
|
|
23
|
+
client = TestClient(app)
|
|
24
|
+
|
|
25
|
+
# Test with admin user
|
|
26
|
+
r = client.get("/admin/system-status", headers={"X-User-Email": "admin@example.com"})
|
|
27
|
+
assert r.status_code == 200
|
|
28
|
+
|
|
29
|
+
data = r.json()
|
|
30
|
+
|
|
31
|
+
# Check response structure
|
|
32
|
+
assert "overall_status" in data
|
|
33
|
+
assert "components" in data
|
|
34
|
+
assert "checked_by" in data
|
|
35
|
+
|
|
36
|
+
# Overall status should be "healthy" or "warning"
|
|
37
|
+
assert data["overall_status"] in ("healthy", "warning")
|
|
38
|
+
|
|
39
|
+
# Components should be a list
|
|
40
|
+
assert isinstance(data["components"], list)
|
|
41
|
+
|
|
42
|
+
# Check that expected components are present
|
|
43
|
+
component_names = [c["component"] for c in data["components"]]
|
|
44
|
+
assert "Configuration" in component_names
|
|
45
|
+
assert "Logging" in component_names
|
|
46
|
+
|
|
47
|
+
# Each component should have required fields
|
|
48
|
+
for component in data["components"]:
|
|
49
|
+
assert "component" in component
|
|
50
|
+
assert "status" in component
|
|
51
|
+
assert "details" in component
|
|
52
|
+
assert component["status"] in ("healthy", "warning", "error")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_system_status_requires_admin():
|
|
56
|
+
"""Test that system status endpoint requires admin access."""
|
|
57
|
+
client = TestClient(app)
|
|
58
|
+
|
|
59
|
+
# Non-admin user should be denied
|
|
60
|
+
r = client.get("/admin/system-status", headers={"X-User-Email": "user@example.com"})
|
|
61
|
+
assert r.status_code in (302, 403)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
|
|
3
|
+
from main import app
|
|
4
|
+
from starlette.testclient import TestClient
|
|
5
|
+
|
|
6
|
+
from atlas.core.capabilities import generate_file_token, verify_file_token
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_capability_token_roundtrip_and_tamper(monkeypatch):
|
|
10
|
+
# Basic generate/verify
|
|
11
|
+
token = generate_file_token("alice@example.com", "file123", ttl_seconds=60)
|
|
12
|
+
claims = verify_file_token(token)
|
|
13
|
+
assert claims and claims["u"] == "alice@example.com" and claims["k"] == "file123"
|
|
14
|
+
|
|
15
|
+
# Tamper body should fail
|
|
16
|
+
body, sig = token.split(".", 1)
|
|
17
|
+
tampered = body[:-1] + ("A" if body[-1] != "A" else "B")
|
|
18
|
+
bad = f"{tampered}.{sig}"
|
|
19
|
+
assert verify_file_token(bad) is None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_capability_token_expiry(monkeypatch):
|
|
23
|
+
# Create a token that is already expired
|
|
24
|
+
token = generate_file_token("bob@example.com", "file999", ttl_seconds=-1)
|
|
25
|
+
assert verify_file_token(token) is None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_download_rejects_invalid_or_expired_token(monkeypatch):
|
|
29
|
+
client = TestClient(app)
|
|
30
|
+
|
|
31
|
+
from atlas.infrastructure.app_factory import app_factory
|
|
32
|
+
s3 = app_factory.get_file_storage()
|
|
33
|
+
|
|
34
|
+
async def fake_get_file(user, key):
|
|
35
|
+
return {
|
|
36
|
+
"key": key,
|
|
37
|
+
"filename": "hello.txt",
|
|
38
|
+
"content_base64": base64.b64encode(b"secret").decode(),
|
|
39
|
+
"content_type": "text/plain",
|
|
40
|
+
"size": 6,
|
|
41
|
+
"last_modified": "",
|
|
42
|
+
"etag": "",
|
|
43
|
+
"tags": {},
|
|
44
|
+
"user_email": user,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Always return a file for these tests
|
|
48
|
+
monkeypatch.setattr(s3, "get_file", fake_get_file)
|
|
49
|
+
|
|
50
|
+
# Invalid token
|
|
51
|
+
resp = client.get(
|
|
52
|
+
"/api/files/download/k2",
|
|
53
|
+
params={"token": "not.a.valid.token"},
|
|
54
|
+
headers={"X-User-Email": "ignored@example.com"},
|
|
55
|
+
)
|
|
56
|
+
assert resp.status_code == 403
|
|
57
|
+
|
|
58
|
+
# Expired token
|
|
59
|
+
expired = generate_file_token("alice@example.com", "k2", ttl_seconds=-5)
|
|
60
|
+
resp2 = client.get(
|
|
61
|
+
"/api/files/download/k2",
|
|
62
|
+
params={"token": expired},
|
|
63
|
+
headers={"X-User-Email": "ignored@example.com"},
|
|
64
|
+
)
|
|
65
|
+
assert resp2.status_code == 403
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from main import app
|
|
2
|
+
from starlette.testclient import TestClient
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_user_stats_enforces_self_scope(monkeypatch):
|
|
6
|
+
client = TestClient(app)
|
|
7
|
+
|
|
8
|
+
# user stats for self should pass (even if backend returns arbitrary data)
|
|
9
|
+
r_ok = client.get(
|
|
10
|
+
"/api/users/alice@example.com/files/stats",
|
|
11
|
+
headers={"X-User-Email": "alice@example.com"},
|
|
12
|
+
)
|
|
13
|
+
# Endpoint may error if mock S3 down; allow 200 or 500, but importantly not 403
|
|
14
|
+
assert r_ok.status_code in (200, 500)
|
|
15
|
+
|
|
16
|
+
# user cannot view others
|
|
17
|
+
r_forbid = client.get(
|
|
18
|
+
"/api/users/bob@example.com/files/stats",
|
|
19
|
+
headers={"X-User-Email": "alice@example.com"},
|
|
20
|
+
)
|
|
21
|
+
assert r_forbid.status_code == 403
|