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,223 @@
|
|
|
1
|
+
"""MCP Authentication routes for per-user API key and token management.
|
|
2
|
+
|
|
3
|
+
This module provides user-facing endpoints for managing authentication tokens
|
|
4
|
+
with MCP servers. Each user's tokens are stored separately and securely encrypted.
|
|
5
|
+
|
|
6
|
+
Users can manually upload API keys, JWTs, or bearer tokens for MCP servers that
|
|
7
|
+
require authentication. This supports any type of token that can be passed as a
|
|
8
|
+
bearer token in the Authorization header.
|
|
9
|
+
|
|
10
|
+
Updated: 2025-01-21
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
17
|
+
from pydantic import BaseModel
|
|
18
|
+
|
|
19
|
+
from atlas.core.auth import is_user_in_group
|
|
20
|
+
from atlas.core.log_sanitizer import get_current_user, sanitize_for_logging
|
|
21
|
+
from atlas.infrastructure.app_factory import app_factory
|
|
22
|
+
from atlas.modules.mcp_tools.token_storage import get_token_storage
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
router = APIRouter(prefix="/api/mcp/auth", tags=["mcp-auth"])
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TokenUpload(BaseModel):
|
|
30
|
+
"""Request body for uploading an API key or token."""
|
|
31
|
+
token: str
|
|
32
|
+
expires_at: Optional[float] = None # Unix timestamp, or None for no expiry
|
|
33
|
+
scopes: Optional[str] = None # Space-separated scopes (optional)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# --- Routes ---
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@router.get("/status")
|
|
40
|
+
async def get_auth_status(current_user: str = Depends(get_current_user)):
|
|
41
|
+
"""Get authentication status for all MCP servers accessible to the user.
|
|
42
|
+
|
|
43
|
+
Returns information about which servers require authentication,
|
|
44
|
+
which the user has authenticated with, and token status.
|
|
45
|
+
"""
|
|
46
|
+
try:
|
|
47
|
+
mcp_manager = app_factory.get_mcp_manager()
|
|
48
|
+
token_storage = get_token_storage()
|
|
49
|
+
|
|
50
|
+
# Get servers the user is authorized to access
|
|
51
|
+
authorized_servers = await mcp_manager.get_authorized_servers(
|
|
52
|
+
current_user, is_user_in_group
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Get user's current auth status
|
|
56
|
+
user_auth_status = token_storage.get_user_auth_status(current_user)
|
|
57
|
+
|
|
58
|
+
# Build response with server auth requirements and user's status
|
|
59
|
+
servers_status = []
|
|
60
|
+
|
|
61
|
+
for server_name in authorized_servers:
|
|
62
|
+
server_config = mcp_manager.servers_config.get(server_name, {})
|
|
63
|
+
auth_type = server_config.get("auth_type", "none")
|
|
64
|
+
|
|
65
|
+
# Get user's token status for this server
|
|
66
|
+
token_status = user_auth_status.get(server_name)
|
|
67
|
+
|
|
68
|
+
server_info = {
|
|
69
|
+
"server_name": server_name,
|
|
70
|
+
"auth_type": auth_type,
|
|
71
|
+
"auth_required": auth_type != "none",
|
|
72
|
+
"authenticated": token_status is not None,
|
|
73
|
+
"description": server_config.get("description", ""),
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# Add token details if authenticated
|
|
77
|
+
if token_status:
|
|
78
|
+
server_info.update({
|
|
79
|
+
"token_type": token_status["token_type"],
|
|
80
|
+
"is_expired": token_status["is_expired"],
|
|
81
|
+
"expires_at": token_status["expires_at"],
|
|
82
|
+
"time_until_expiry": token_status["time_until_expiry"],
|
|
83
|
+
"has_refresh_token": token_status["has_refresh_token"],
|
|
84
|
+
"scopes": token_status["scopes"],
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
servers_status.append(server_info)
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
"servers": servers_status,
|
|
91
|
+
"user": current_user,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
except Exception as e:
|
|
95
|
+
logger.error(f"Error getting auth status: {e}", exc_info=True)
|
|
96
|
+
raise HTTPException(status_code=500, detail="Internal server error while fetching auth status")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@router.post("/{server_name}/token")
|
|
100
|
+
async def upload_token(
|
|
101
|
+
server_name: str,
|
|
102
|
+
token_data: TokenUpload,
|
|
103
|
+
current_user: str = Depends(get_current_user),
|
|
104
|
+
):
|
|
105
|
+
"""Upload an API key or bearer token for an MCP server.
|
|
106
|
+
|
|
107
|
+
This allows users to manually provide tokens for servers that require
|
|
108
|
+
authentication. Tokens can be:
|
|
109
|
+
- API keys
|
|
110
|
+
- JWT tokens
|
|
111
|
+
- Bearer tokens
|
|
112
|
+
- Any other string token that can be used in Authorization header
|
|
113
|
+
|
|
114
|
+
The token will be securely encrypted and stored per-user.
|
|
115
|
+
"""
|
|
116
|
+
try:
|
|
117
|
+
mcp_manager = app_factory.get_mcp_manager()
|
|
118
|
+
token_storage = get_token_storage()
|
|
119
|
+
|
|
120
|
+
# Verify server exists and user has access
|
|
121
|
+
authorized_servers = await mcp_manager.get_authorized_servers(
|
|
122
|
+
current_user, is_user_in_group
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
if server_name not in authorized_servers:
|
|
126
|
+
raise HTTPException(
|
|
127
|
+
status_code=403,
|
|
128
|
+
detail=f"Not authorized to access server '{server_name}'"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Verify server accepts token authentication
|
|
132
|
+
server_config = mcp_manager.servers_config.get(server_name, {})
|
|
133
|
+
auth_type = server_config.get("auth_type", "none")
|
|
134
|
+
|
|
135
|
+
if auth_type not in ("jwt", "bearer", "api_key", "oauth"):
|
|
136
|
+
raise HTTPException(
|
|
137
|
+
status_code=400,
|
|
138
|
+
detail=f"Server '{server_name}' does not accept token authentication (auth_type: {auth_type})"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Validate token is not empty
|
|
142
|
+
if not token_data.token or not token_data.token.strip():
|
|
143
|
+
raise HTTPException(
|
|
144
|
+
status_code=400,
|
|
145
|
+
detail="Token cannot be empty"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Determine token type based on server config
|
|
149
|
+
token_type = "bearer"
|
|
150
|
+
if auth_type == "jwt":
|
|
151
|
+
token_type = "jwt"
|
|
152
|
+
elif auth_type == "api_key":
|
|
153
|
+
token_type = "api_key"
|
|
154
|
+
|
|
155
|
+
# Store the token
|
|
156
|
+
stored_token = token_storage.store_token(
|
|
157
|
+
user_email=current_user,
|
|
158
|
+
server_name=server_name,
|
|
159
|
+
token_value=token_data.token.strip(),
|
|
160
|
+
token_type=token_type,
|
|
161
|
+
expires_at=token_data.expires_at,
|
|
162
|
+
scopes=token_data.scopes,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
sanitized_server = sanitize_for_logging(server_name)
|
|
166
|
+
logger.info(f"User uploaded token for MCP server '{sanitized_server}'")
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
"message": f"Token stored for server '{server_name}'",
|
|
170
|
+
"server_name": server_name,
|
|
171
|
+
"token_type": stored_token.token_type,
|
|
172
|
+
"expires_at": stored_token.expires_at,
|
|
173
|
+
"scopes": stored_token.scopes,
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
except HTTPException:
|
|
177
|
+
raise
|
|
178
|
+
except Exception as e:
|
|
179
|
+
logger.error(f"Error uploading token: {e}", exc_info=True)
|
|
180
|
+
raise HTTPException(status_code=500, detail="Internal server error while uploading token")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@router.delete("/{server_name}/token")
|
|
184
|
+
async def remove_token(
|
|
185
|
+
server_name: str,
|
|
186
|
+
current_user: str = Depends(get_current_user),
|
|
187
|
+
):
|
|
188
|
+
"""Remove stored token for an MCP server (disconnect).
|
|
189
|
+
|
|
190
|
+
This removes the user's authentication token for the specified server.
|
|
191
|
+
The user will need to re-authenticate to use the server's tools.
|
|
192
|
+
"""
|
|
193
|
+
try:
|
|
194
|
+
token_storage = get_token_storage()
|
|
195
|
+
|
|
196
|
+
# Remove the token
|
|
197
|
+
removed = token_storage.remove_token(current_user, server_name)
|
|
198
|
+
|
|
199
|
+
if not removed:
|
|
200
|
+
raise HTTPException(
|
|
201
|
+
status_code=404,
|
|
202
|
+
detail=f"No token found for server '{server_name}'"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Invalidate any cached client for this user/server combination
|
|
206
|
+
tool_manager = app_factory.get_mcp_manager()
|
|
207
|
+
if tool_manager is not None:
|
|
208
|
+
await tool_manager._invalidate_user_client(current_user, server_name)
|
|
209
|
+
logger.debug(f"Invalidated cached client for server '{server_name}'")
|
|
210
|
+
|
|
211
|
+
sanitized_server = sanitize_for_logging(server_name)
|
|
212
|
+
logger.info(f"User removed token for MCP server '{sanitized_server}'")
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
"message": f"Token removed for server '{server_name}'",
|
|
216
|
+
"server_name": server_name,
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
except HTTPException:
|
|
220
|
+
raise
|
|
221
|
+
except Exception as e:
|
|
222
|
+
logger.error(f"Error removing token: {e}", exc_info=True)
|
|
223
|
+
raise HTTPException(status_code=500, detail="Internal server error while removing token")
|
atlas/server_cli.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Atlas Server CLI - Start the Atlas backend server.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
atlas-server # Start with defaults
|
|
6
|
+
atlas-server --port 8000 # Custom port
|
|
7
|
+
atlas-server --env /path/to/.env # Custom env file
|
|
8
|
+
atlas-server --config-folder /path/to/config # Custom config folder
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _apply_env_file(env_path: Path) -> None:
|
|
18
|
+
"""Load environment variables from a .env file."""
|
|
19
|
+
from dotenv import load_dotenv
|
|
20
|
+
|
|
21
|
+
if not env_path.exists():
|
|
22
|
+
print(f"Error: env file not found: {env_path}", file=sys.stderr)
|
|
23
|
+
sys.exit(2)
|
|
24
|
+
|
|
25
|
+
load_dotenv(dotenv_path=str(env_path))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _apply_config_folder(config_folder: Path) -> None:
|
|
29
|
+
"""Set up config folder as the overrides directory."""
|
|
30
|
+
if not config_folder.exists():
|
|
31
|
+
print(f"Error: config folder not found: {config_folder}", file=sys.stderr)
|
|
32
|
+
sys.exit(2)
|
|
33
|
+
|
|
34
|
+
os.environ["APP_CONFIG_OVERRIDES"] = str(config_folder.resolve())
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
38
|
+
"""Build argument parser for atlas-server CLI."""
|
|
39
|
+
parser = argparse.ArgumentParser(
|
|
40
|
+
prog="atlas-server",
|
|
41
|
+
description="Start the Atlas backend server with MCP integration.",
|
|
42
|
+
)
|
|
43
|
+
parser.add_argument(
|
|
44
|
+
"--port",
|
|
45
|
+
type=int,
|
|
46
|
+
default=None,
|
|
47
|
+
help="Port to run the server on (default: 8000 or PORT env var).",
|
|
48
|
+
)
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"--host",
|
|
51
|
+
default=None,
|
|
52
|
+
help="Host to bind to (default: 127.0.0.1 or ATLAS_HOST env var).",
|
|
53
|
+
)
|
|
54
|
+
parser.add_argument(
|
|
55
|
+
"--env",
|
|
56
|
+
dest="env_file",
|
|
57
|
+
default=None,
|
|
58
|
+
help="Path to .env file (default: .env in current directory or package root).",
|
|
59
|
+
)
|
|
60
|
+
parser.add_argument(
|
|
61
|
+
"--config-folder",
|
|
62
|
+
dest="config_folder",
|
|
63
|
+
default=None,
|
|
64
|
+
help="Path to config folder for overrides (sets APP_CONFIG_OVERRIDES).",
|
|
65
|
+
)
|
|
66
|
+
parser.add_argument(
|
|
67
|
+
"--config-defaults",
|
|
68
|
+
dest="config_defaults",
|
|
69
|
+
default=None,
|
|
70
|
+
help="Path to config defaults folder (sets APP_CONFIG_DEFAULTS).",
|
|
71
|
+
)
|
|
72
|
+
parser.add_argument(
|
|
73
|
+
"--reload",
|
|
74
|
+
action="store_true",
|
|
75
|
+
help="Enable auto-reload for development (not recommended for production).",
|
|
76
|
+
)
|
|
77
|
+
parser.add_argument(
|
|
78
|
+
"--workers",
|
|
79
|
+
type=int,
|
|
80
|
+
default=1,
|
|
81
|
+
help="Number of worker processes (default: 1).",
|
|
82
|
+
)
|
|
83
|
+
parser.add_argument(
|
|
84
|
+
"--version",
|
|
85
|
+
action="store_true",
|
|
86
|
+
help="Print version and exit.",
|
|
87
|
+
)
|
|
88
|
+
return parser
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def run_server(args: argparse.Namespace) -> int:
|
|
92
|
+
"""Run the Atlas server with the given arguments."""
|
|
93
|
+
import uvicorn
|
|
94
|
+
|
|
95
|
+
# Determine host and port
|
|
96
|
+
host = args.host or os.getenv("ATLAS_HOST", "127.0.0.1")
|
|
97
|
+
port = args.port or int(os.getenv("PORT", "8000"))
|
|
98
|
+
|
|
99
|
+
# Import the FastAPI app
|
|
100
|
+
from atlas.main import app
|
|
101
|
+
|
|
102
|
+
print(f"Starting Atlas server on {host}:{port}")
|
|
103
|
+
|
|
104
|
+
if args.reload:
|
|
105
|
+
print("Warning: --reload is enabled. This is not recommended for production.")
|
|
106
|
+
uvicorn.run(
|
|
107
|
+
"atlas.main:app",
|
|
108
|
+
host=host,
|
|
109
|
+
port=port,
|
|
110
|
+
reload=True,
|
|
111
|
+
workers=1, # reload doesn't support multiple workers
|
|
112
|
+
)
|
|
113
|
+
else:
|
|
114
|
+
uvicorn.run(
|
|
115
|
+
app,
|
|
116
|
+
host=host,
|
|
117
|
+
port=port,
|
|
118
|
+
workers=args.workers,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
return 0
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def main() -> None:
|
|
125
|
+
"""Main entry point for atlas-server CLI."""
|
|
126
|
+
parser = build_parser()
|
|
127
|
+
args = parser.parse_args()
|
|
128
|
+
|
|
129
|
+
if args.version:
|
|
130
|
+
from atlas.version import VERSION
|
|
131
|
+
print(f"atlas-server version {VERSION}")
|
|
132
|
+
sys.exit(0)
|
|
133
|
+
|
|
134
|
+
# Apply env file first (before any other imports that might use env vars)
|
|
135
|
+
if args.env_file:
|
|
136
|
+
_apply_env_file(Path(args.env_file).expanduser())
|
|
137
|
+
else:
|
|
138
|
+
# Try to find .env in current directory or package root
|
|
139
|
+
cwd_env = Path.cwd() / ".env"
|
|
140
|
+
if cwd_env.exists():
|
|
141
|
+
_apply_env_file(cwd_env)
|
|
142
|
+
else:
|
|
143
|
+
# Check package root (parent of atlas/)
|
|
144
|
+
pkg_root_env = Path(__file__).resolve().parents[1] / ".env"
|
|
145
|
+
if pkg_root_env.exists():
|
|
146
|
+
_apply_env_file(pkg_root_env)
|
|
147
|
+
|
|
148
|
+
# Apply config folder if specified
|
|
149
|
+
if args.config_folder:
|
|
150
|
+
_apply_config_folder(Path(args.config_folder).expanduser())
|
|
151
|
+
|
|
152
|
+
# Apply config defaults if specified
|
|
153
|
+
if args.config_defaults:
|
|
154
|
+
defaults_path = Path(args.config_defaults).expanduser()
|
|
155
|
+
if not defaults_path.exists():
|
|
156
|
+
print(f"Error: config defaults folder not found: {defaults_path}", file=sys.stderr)
|
|
157
|
+
sys.exit(2)
|
|
158
|
+
os.environ["APP_CONFIG_DEFAULTS"] = str(defaults_path.resolve())
|
|
159
|
+
|
|
160
|
+
sys.exit(run_server(args))
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
if __name__ == "__main__":
|
|
164
|
+
main()
|
atlas/tests/conftest.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
# Ensure the backend root is on sys.path for absolute imports like 'infrastructure.*'
|
|
5
|
+
backend_root = Path(__file__).resolve().parents[1]
|
|
6
|
+
project_root = backend_root.parent
|
|
7
|
+
if str(backend_root) not in sys.path:
|
|
8
|
+
sys.path.insert(0, str(backend_root))
|
|
9
|
+
if str(project_root) not in sys.path:
|
|
10
|
+
sys.path.insert(0, str(project_root))
|
|
11
|
+
|
|
12
|
+
# Pre-import critical modules before any test files can replace them with fakes.
|
|
13
|
+
# This prevents test pollution where one test file patches sys.modules and other
|
|
14
|
+
# tests import the fake instead of the real module.
|
|
15
|
+
# See test_capability_tokens_and_injection.py which patches LiteLLMCaller.
|
|
16
|
+
import atlas.modules.llm.litellm_caller # noqa: E402, F401
|
|
17
|
+
|
|
18
|
+
# Explicitly reference the module to satisfy static analyzers that flag unused imports.
|
|
19
|
+
# The import above is intentional: it pre-populates sys.modules with the real module.
|
|
20
|
+
_ = atlas.modules.llm.litellm_caller.LiteLLMCaller # noqa: E402
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
from unittest.mock import ANY, AsyncMock, Mock, patch
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from atlas.modules.mcp_tools.client import MCPToolManager
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.mark.integration
|
|
9
|
+
class TestMCPAuthenticationIntegration:
|
|
10
|
+
"""Integration tests for MCP authentication."""
|
|
11
|
+
|
|
12
|
+
@pytest.mark.asyncio
|
|
13
|
+
async def test_authenticated_connection_success(self, monkeypatch):
|
|
14
|
+
"""Should successfully connect to authenticated MCP server."""
|
|
15
|
+
monkeypatch.setenv("MCP_TEST_TOKEN", "test-api-key-123")
|
|
16
|
+
|
|
17
|
+
# Configure test server with auth requirement
|
|
18
|
+
server_config = {
|
|
19
|
+
"url": "http://localhost:8001/mcp",
|
|
20
|
+
"transport": "http",
|
|
21
|
+
"auth_token": "${MCP_TEST_TOKEN}"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# Mock config_manager to return our test server config
|
|
25
|
+
with patch('atlas.modules.mcp_tools.client.config_manager') as mock_config_manager:
|
|
26
|
+
mock_config_manager.mcp_config.servers = {"mcp-http-mock": Mock()}
|
|
27
|
+
mock_config_manager.mcp_config.servers["mcp-http-mock"].model_dump.return_value = server_config
|
|
28
|
+
mock_config_manager.app_settings.mcp_call_timeout = 120
|
|
29
|
+
mock_config_manager.app_settings.mcp_discovery_timeout = 30
|
|
30
|
+
|
|
31
|
+
manager = MCPToolManager()
|
|
32
|
+
manager.servers_config = {"mcp-http-mock": server_config}
|
|
33
|
+
|
|
34
|
+
# Mock the fastmcp.Client to avoid actual network call for now
|
|
35
|
+
with patch('atlas.modules.mcp_tools.client.Client') as MockFastMCPClient:
|
|
36
|
+
mock_client_instance = MockFastMCPClient.return_value
|
|
37
|
+
mock_client_instance.__aenter__.return_value = mock_client_instance
|
|
38
|
+
mock_client_instance.list_tools.return_value = [] # Mock an empty list of tools
|
|
39
|
+
|
|
40
|
+
await manager.initialize_clients()
|
|
41
|
+
|
|
42
|
+
# Assert that the client was initialized and added to manager.clients
|
|
43
|
+
assert "mcp-http-mock" in manager.clients
|
|
44
|
+
MockFastMCPClient.assert_called_once_with(
|
|
45
|
+
"http://localhost:8001/mcp",
|
|
46
|
+
auth="test-api-key-123",
|
|
47
|
+
log_handler=ANY,
|
|
48
|
+
elicitation_handler=ANY,
|
|
49
|
+
sampling_handler=ANY,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
@pytest.mark.asyncio
|
|
53
|
+
async def test_authenticated_connection_failure_invalid_token(self, monkeypatch):
|
|
54
|
+
"""Should fail to connect with invalid token."""
|
|
55
|
+
monkeypatch.setenv("MCP_TEST_TOKEN", "invalid-token") # Set an invalid token
|
|
56
|
+
|
|
57
|
+
server_config = {
|
|
58
|
+
"url": "http://localhost:8001/mcp",
|
|
59
|
+
"transport": "http",
|
|
60
|
+
"auth_token": "${MCP_TEST_TOKEN}"
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
with patch('atlas.modules.mcp_tools.client.config_manager') as mock_config_manager:
|
|
64
|
+
mock_config_manager.mcp_config.servers = {"mcp-http-mock": Mock()}
|
|
65
|
+
mock_config_manager.mcp_config.servers["mcp-http-mock"].model_dump.return_value = server_config
|
|
66
|
+
mock_config_manager.app_settings.mcp_call_timeout = 120
|
|
67
|
+
mock_config_manager.app_settings.mcp_discovery_timeout = 30
|
|
68
|
+
|
|
69
|
+
manager = MCPToolManager()
|
|
70
|
+
manager.servers_config = {"mcp-http-mock": server_config}
|
|
71
|
+
|
|
72
|
+
# Mock the fastmcp.Client to simulate an authentication error
|
|
73
|
+
with patch('atlas.modules.mcp_tools.client.Client') as MockFastMCPClient:
|
|
74
|
+
mock_client_instance = MockFastMCPClient.return_value
|
|
75
|
+
mock_client_instance.__aenter__.side_effect = Exception("Authentication failed: 401 Unauthorized")
|
|
76
|
+
|
|
77
|
+
await manager.initialize_clients()
|
|
78
|
+
|
|
79
|
+
# Client object is created successfully (not connected yet)
|
|
80
|
+
assert "mcp-http-mock" in manager.clients
|
|
81
|
+
# Verify auth token was passed correctly
|
|
82
|
+
MockFastMCPClient.assert_called_once_with(
|
|
83
|
+
"http://localhost:8001/mcp",
|
|
84
|
+
auth="invalid-token",
|
|
85
|
+
log_handler=ANY,
|
|
86
|
+
elicitation_handler=ANY,
|
|
87
|
+
sampling_handler=ANY,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Now try to discover tools - this should fail due to auth error
|
|
91
|
+
await manager.discover_tools()
|
|
92
|
+
|
|
93
|
+
# After failed connection, tools should not be discovered
|
|
94
|
+
if not hasattr(manager, '_tool_index'):
|
|
95
|
+
manager._tool_index = {}
|
|
96
|
+
assert len([k for k in manager._tool_index.keys() if k.startswith("mcp-http-mock_")]) == 0
|
|
97
|
+
|
|
98
|
+
@pytest.mark.asyncio
|
|
99
|
+
async def test_authenticated_tool_execution(self, monkeypatch):
|
|
100
|
+
"""Should successfully execute tool after authentication."""
|
|
101
|
+
monkeypatch.setenv("MCP_TEST_TOKEN", "test-api-key-123")
|
|
102
|
+
|
|
103
|
+
server_config = {
|
|
104
|
+
"url": "http://localhost:8001/mcp",
|
|
105
|
+
"transport": "http",
|
|
106
|
+
"auth_token": "${MCP_TEST_TOKEN}"
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
with patch('atlas.modules.mcp_tools.client.config_manager') as mock_config_manager:
|
|
110
|
+
mock_config_manager.mcp_config.servers = {"mcp-http-mock": Mock()}
|
|
111
|
+
mock_config_manager.mcp_config.servers["mcp-http-mock"].model_dump.return_value = server_config
|
|
112
|
+
mock_config_manager.app_settings.mcp_call_timeout = 120
|
|
113
|
+
mock_config_manager.app_settings.mcp_discovery_timeout = 30
|
|
114
|
+
|
|
115
|
+
manager = MCPToolManager()
|
|
116
|
+
manager.servers_config = {"mcp-http-mock": server_config}
|
|
117
|
+
|
|
118
|
+
# Mock the fastmcp.Client and its call_tool method
|
|
119
|
+
with patch('atlas.modules.mcp_tools.client.Client') as MockFastMCPClient:
|
|
120
|
+
mock_client_instance = MockFastMCPClient.return_value
|
|
121
|
+
mock_client_instance.__aenter__.return_value = mock_client_instance
|
|
122
|
+
mock_client_instance.list_tools.return_value = [] # Mock an empty list of tools
|
|
123
|
+
|
|
124
|
+
# Make call_tool return an async result
|
|
125
|
+
class MockResult:
|
|
126
|
+
data = {"results": "tool_result"}
|
|
127
|
+
mock_client_instance.call_tool = AsyncMock(return_value=MockResult())
|
|
128
|
+
|
|
129
|
+
await manager.initialize_clients()
|
|
130
|
+
|
|
131
|
+
# Simulate tool call
|
|
132
|
+
from atlas.domain.messages.models import ToolCall
|
|
133
|
+
tool_call = ToolCall(id="call_1", name="mcp-http-mock_test_tool", arguments={})
|
|
134
|
+
|
|
135
|
+
# Need to mock the _tool_index for execute_tool to find the server
|
|
136
|
+
mock_tool = Mock()
|
|
137
|
+
mock_tool.name = "test_tool"
|
|
138
|
+
manager._tool_index = {
|
|
139
|
+
"mcp-http-mock_test_tool": {
|
|
140
|
+
'server': 'mcp-http-mock',
|
|
141
|
+
'tool': mock_tool
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
result = await manager.execute_tool(tool_call)
|
|
146
|
+
|
|
147
|
+
assert result.success is True
|
|
148
|
+
assert "tool_result" in result.content
|
|
149
|
+
mock_client_instance.call_tool.assert_called_once()
|
|
150
|
+
call_args = mock_client_instance.call_tool.call_args
|
|
151
|
+
assert call_args[0] == ("test_tool", {})
|
|
152
|
+
assert "progress_handler" in call_args[1]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Quick test script to verify sampling functionality works end-to-end.
|
|
4
|
+
This tests the sampling_demo server directly to ensure it can be initialized
|
|
5
|
+
and sampling works correctly.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import logging
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
# Add backend to path
|
|
14
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
15
|
+
|
|
16
|
+
logging.basicConfig(level=logging.INFO)
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def test_sampling_demo():
|
|
21
|
+
"""Test the sampling_demo server."""
|
|
22
|
+
try:
|
|
23
|
+
from fastmcp import Client
|
|
24
|
+
|
|
25
|
+
logger.info("Starting sampling demo test...")
|
|
26
|
+
|
|
27
|
+
# Create sampling handler that uses LiteLLM
|
|
28
|
+
async def sampling_handler(messages, params, context):
|
|
29
|
+
from mcp.types import CreateMessageResult, TextContent
|
|
30
|
+
|
|
31
|
+
logger.info(f"Sampling handler called with {len(messages)} messages")
|
|
32
|
+
|
|
33
|
+
# For testing purposes, we'll return a mock response
|
|
34
|
+
# In production, this would call the actual LLM
|
|
35
|
+
logger.info("Returning mock LLM response for testing")
|
|
36
|
+
|
|
37
|
+
# Return proper CreateMessageResult
|
|
38
|
+
return CreateMessageResult(
|
|
39
|
+
role="assistant",
|
|
40
|
+
content=TextContent(type="text", text="This is a mock LLM response for testing purposes."),
|
|
41
|
+
model="mock-model"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Create client to sampling_demo server
|
|
45
|
+
server_path = "backend/mcp/sampling_demo/main.py"
|
|
46
|
+
logger.info(f"Connecting to server: {server_path}")
|
|
47
|
+
|
|
48
|
+
client = Client(server_path, sampling_handler=sampling_handler)
|
|
49
|
+
|
|
50
|
+
async with client:
|
|
51
|
+
# List tools
|
|
52
|
+
tools = await client.list_tools()
|
|
53
|
+
logger.info(f"Available tools: {[t.name for t in tools]}")
|
|
54
|
+
|
|
55
|
+
# Test summarize_text tool
|
|
56
|
+
logger.info("\n=== Testing summarize_text ===")
|
|
57
|
+
result = await client.call_tool(
|
|
58
|
+
"summarize_text",
|
|
59
|
+
{
|
|
60
|
+
"text": "The quick brown fox jumps over the lazy dog. "
|
|
61
|
+
"This sentence contains every letter of the alphabet. "
|
|
62
|
+
"It is commonly used for testing fonts and keyboards."
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
logger.info(f"Result: {result}")
|
|
66
|
+
|
|
67
|
+
# Test analyze_sentiment tool
|
|
68
|
+
logger.info("\n=== Testing analyze_sentiment ===")
|
|
69
|
+
result = await client.call_tool(
|
|
70
|
+
"analyze_sentiment",
|
|
71
|
+
{
|
|
72
|
+
"text": "I absolutely love this product! It's amazing and works perfectly!"
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
logger.info(f"Result: {result}")
|
|
76
|
+
|
|
77
|
+
logger.info("\n✅ All tests passed!")
|
|
78
|
+
return True
|
|
79
|
+
|
|
80
|
+
except Exception as e:
|
|
81
|
+
logger.error(f"Test failed: {e}", exc_info=True)
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
if __name__ == "__main__":
|
|
86
|
+
success = asyncio.run(test_sampling_demo())
|
|
87
|
+
sys.exit(0 if success else 1)
|