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,276 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Result processing module for code executor.
|
|
4
|
+
Handles output processing, visualization, and file encoding.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import base64
|
|
8
|
+
import logging
|
|
9
|
+
import traceback
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Dict, List
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def detect_matplotlib_plots(exec_dir: Path) -> List[str]:
|
|
17
|
+
"""
|
|
18
|
+
Detect if matplotlib plots were created and convert to base64.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
exec_dir: Execution directory to scan for plot files
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
List of base64-encoded plot images
|
|
25
|
+
"""
|
|
26
|
+
try:
|
|
27
|
+
plot_files = []
|
|
28
|
+
for ext in ['*.png', '*.jpg', '*.jpeg', '*.svg']:
|
|
29
|
+
plot_files.extend(exec_dir.glob(ext))
|
|
30
|
+
|
|
31
|
+
base64_plots = []
|
|
32
|
+
for plot_file in plot_files:
|
|
33
|
+
try:
|
|
34
|
+
with open(plot_file, 'rb') as f:
|
|
35
|
+
image_data = base64.b64encode(f.read()).decode('utf-8')
|
|
36
|
+
file_ext = plot_file.suffix.lower()
|
|
37
|
+
mime_type = {
|
|
38
|
+
'.png': 'image/png',
|
|
39
|
+
'.jpg': 'image/jpeg',
|
|
40
|
+
'.jpeg': 'image/jpeg',
|
|
41
|
+
'.svg': 'image/svg+xml'
|
|
42
|
+
}.get(file_ext, 'image/png')
|
|
43
|
+
|
|
44
|
+
base64_plots.append(f"data:{mime_type};base64,{image_data}")
|
|
45
|
+
logger.info(f"Successfully encoded plot: {plot_file.name}")
|
|
46
|
+
except Exception as e:
|
|
47
|
+
logger.warning(f"Failed to encode plot {plot_file}: {str(e)}")
|
|
48
|
+
logger.warning(f"Traceback: {traceback.format_exc()}")
|
|
49
|
+
continue
|
|
50
|
+
|
|
51
|
+
logger.info(f"Detected {len(base64_plots)} plots in {exec_dir}")
|
|
52
|
+
return base64_plots
|
|
53
|
+
|
|
54
|
+
except Exception as e:
|
|
55
|
+
logger.error(f"Error detecting matplotlib plots: {str(e)}")
|
|
56
|
+
logger.error(f"Traceback: {traceback.format_exc()}")
|
|
57
|
+
return []
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def create_visualization_html(plots: List[str], output_text: str) -> str:
|
|
61
|
+
"""
|
|
62
|
+
Create HTML for displaying plots and output matching the frontend dark theme.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
plots: List of base64-encoded plot images
|
|
66
|
+
output_text: Text output from code execution
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
HTML string for display
|
|
70
|
+
"""
|
|
71
|
+
html_content = """
|
|
72
|
+
<div style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
|
73
|
+
max-width: 100%;
|
|
74
|
+
padding: 20px;
|
|
75
|
+
background-color: #111827;
|
|
76
|
+
color: #e5e7eb;
|
|
77
|
+
line-height: 1.6;">
|
|
78
|
+
<h3 style="color: #e5e7eb; margin: 0 0 16px 0; font-weight: 600;">Code Execution Results</h3>
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
if output_text.strip():
|
|
82
|
+
html_content += f"""
|
|
83
|
+
<div style="background-color: #1f2937;
|
|
84
|
+
border: 1px solid #374151;
|
|
85
|
+
padding: 16px;
|
|
86
|
+
border-radius: 8px;
|
|
87
|
+
margin-bottom: 20px;">
|
|
88
|
+
<h4 style="color: #e5e7eb; margin: 0 0 12px 0; font-weight: 500; font-size: 14px;">Output:</h4>
|
|
89
|
+
<pre style="white-space: pre-wrap;
|
|
90
|
+
margin: 0;
|
|
91
|
+
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
|
92
|
+
color: #d1d5db;
|
|
93
|
+
background-color: #111827;
|
|
94
|
+
padding: 12px;
|
|
95
|
+
border-radius: 6px;
|
|
96
|
+
border: 1px solid #4b5563;
|
|
97
|
+
overflow-x: auto;">{output_text}</pre>
|
|
98
|
+
</div>
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
if plots:
|
|
102
|
+
html_content += '<h4 style="color: #e5e7eb; margin: 20px 0 16px 0; font-weight: 500;">Generated Visualizations:</h4>'
|
|
103
|
+
for i, plot in enumerate(plots):
|
|
104
|
+
html_content += f"""
|
|
105
|
+
<div style="margin-bottom: 20px;
|
|
106
|
+
text-align: center;
|
|
107
|
+
background-color: #1f2937;
|
|
108
|
+
border: 1px solid #374151;
|
|
109
|
+
border-radius: 8px;
|
|
110
|
+
padding: 16px;">
|
|
111
|
+
<img src="{plot}"
|
|
112
|
+
alt="Plot {i+1}"
|
|
113
|
+
style="max-width: 100%;
|
|
114
|
+
height: auto;
|
|
115
|
+
border: 1px solid #4b5563;
|
|
116
|
+
border-radius: 6px;
|
|
117
|
+
background-color: white;">
|
|
118
|
+
</div>
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
html_content += "</div>"
|
|
122
|
+
return html_content
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def list_generated_files(exec_dir: Path) -> List[str]:
|
|
126
|
+
"""
|
|
127
|
+
List files generated during code execution.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
exec_dir: Execution directory
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
List of generated file names
|
|
134
|
+
"""
|
|
135
|
+
try:
|
|
136
|
+
generated_files = []
|
|
137
|
+
for file_path in exec_dir.iterdir():
|
|
138
|
+
if file_path.is_file() and file_path.name != "exec_script.py":
|
|
139
|
+
generated_files.append(file_path.name)
|
|
140
|
+
|
|
141
|
+
logger.info(f"Generated files: {generated_files}")
|
|
142
|
+
return generated_files
|
|
143
|
+
|
|
144
|
+
except Exception as e:
|
|
145
|
+
logger.error(f"Error listing generated files: {str(e)}")
|
|
146
|
+
logger.error(f"Traceback: {traceback.format_exc()}")
|
|
147
|
+
return []
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def encode_generated_files(exec_dir: Path) -> List[Dict[str, str]]:
|
|
151
|
+
"""
|
|
152
|
+
Encode generated files to base64 for download.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
exec_dir: Execution directory
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
List of dictionaries with 'filename' and 'content_base64' keys
|
|
159
|
+
"""
|
|
160
|
+
try:
|
|
161
|
+
encoded_files = []
|
|
162
|
+
for file_path in exec_dir.iterdir():
|
|
163
|
+
if file_path.is_file() and file_path.name != "exec_script.py":
|
|
164
|
+
try:
|
|
165
|
+
with open(file_path, 'rb') as f:
|
|
166
|
+
file_content = f.read()
|
|
167
|
+
|
|
168
|
+
content_base64 = base64.b64encode(file_content).decode('utf-8')
|
|
169
|
+
encoded_files.append({
|
|
170
|
+
'filename': file_path.name,
|
|
171
|
+
'content_base64': content_base64
|
|
172
|
+
})
|
|
173
|
+
logger.info(f"Encoded file: {file_path.name} ({len(file_content)} bytes)")
|
|
174
|
+
except Exception as e:
|
|
175
|
+
logger.warning(f"Failed to encode file {file_path.name}: {str(e)}")
|
|
176
|
+
continue
|
|
177
|
+
|
|
178
|
+
logger.info(f"Encoded {len(encoded_files)} generated files")
|
|
179
|
+
return encoded_files
|
|
180
|
+
|
|
181
|
+
except Exception as e:
|
|
182
|
+
logger.error(f"Error encoding generated files: {str(e)}")
|
|
183
|
+
logger.error(f"Traceback: {traceback.format_exc()}")
|
|
184
|
+
return []
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def truncate_output_for_llm(output: str, max_chars: int = 2000) -> tuple[str, bool]:
|
|
188
|
+
"""
|
|
189
|
+
Smart truncation that preserves important context around key terms.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
output: The output text to potentially truncate
|
|
193
|
+
max_chars: Maximum characters to allow (default: 2000)
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Tuple of (truncated_output, was_truncated)
|
|
197
|
+
"""
|
|
198
|
+
if len(output) <= max_chars:
|
|
199
|
+
return output, False
|
|
200
|
+
|
|
201
|
+
# Key terms that indicate important context
|
|
202
|
+
key_terms = [
|
|
203
|
+
'error', 'Error', 'ERROR', 'exception', 'Exception', 'EXCEPTION',
|
|
204
|
+
'traceback', 'Traceback', 'TRACEBACK', 'failed', 'Failed', 'FAILED',
|
|
205
|
+
'warning', 'Warning', 'WARNING', 'success', 'Success', 'SUCCESS',
|
|
206
|
+
'completed', 'Completed', 'COMPLETED', 'result', 'Result', 'RESULT',
|
|
207
|
+
'summary', 'Summary', 'SUMMARY', 'total', 'Total', 'TOTAL',
|
|
208
|
+
'shape:', 'dtype:', 'columns:', 'index:', 'memory usage:', 'non-null'
|
|
209
|
+
]
|
|
210
|
+
|
|
211
|
+
# Find all important sections
|
|
212
|
+
important_sections = []
|
|
213
|
+
context_chars = 150 # Characters around each key term
|
|
214
|
+
|
|
215
|
+
for term in key_terms:
|
|
216
|
+
start_pos = 0
|
|
217
|
+
while True:
|
|
218
|
+
pos = output.find(term, start_pos)
|
|
219
|
+
if pos == -1:
|
|
220
|
+
break
|
|
221
|
+
|
|
222
|
+
# Extract context around the term
|
|
223
|
+
section_start = max(0, pos - context_chars)
|
|
224
|
+
section_end = min(len(output), pos + len(term) + context_chars)
|
|
225
|
+
|
|
226
|
+
# Expand to word boundaries if possible
|
|
227
|
+
while section_start > 0 and not output[section_start].isspace():
|
|
228
|
+
section_start -= 1
|
|
229
|
+
while section_end < len(output) and not output[section_end].isspace():
|
|
230
|
+
section_end += 1
|
|
231
|
+
|
|
232
|
+
important_sections.append((section_start, section_end, term))
|
|
233
|
+
start_pos = pos + 1
|
|
234
|
+
|
|
235
|
+
if important_sections:
|
|
236
|
+
# Sort sections by position and merge overlapping ones
|
|
237
|
+
important_sections.sort(key=lambda x: x[0])
|
|
238
|
+
merged_sections = []
|
|
239
|
+
|
|
240
|
+
for start, end, term in important_sections:
|
|
241
|
+
if merged_sections and start <= merged_sections[-1][1] + 50: # Merge if close
|
|
242
|
+
merged_sections[-1] = (merged_sections[-1][0], max(end, merged_sections[-1][1]), merged_sections[-1][2] + f", {term}")
|
|
243
|
+
else:
|
|
244
|
+
merged_sections.append((start, end, term))
|
|
245
|
+
|
|
246
|
+
# Build truncated output with important sections
|
|
247
|
+
result_parts = []
|
|
248
|
+
total_chars = 0
|
|
249
|
+
|
|
250
|
+
# Always include the beginning (first 300 chars)
|
|
251
|
+
beginning = output[:300]
|
|
252
|
+
result_parts.append(beginning)
|
|
253
|
+
total_chars += len(beginning)
|
|
254
|
+
|
|
255
|
+
# Add important sections
|
|
256
|
+
for start, end, terms in merged_sections:
|
|
257
|
+
section = output[start:end]
|
|
258
|
+
if total_chars + len(section) + 100 > max_chars: # Reserve space for truncation message
|
|
259
|
+
break
|
|
260
|
+
|
|
261
|
+
if start > 300: # Don't duplicate beginning
|
|
262
|
+
result_parts.append(f"\n\n[... content around: {terms} ...]\n")
|
|
263
|
+
result_parts.append(section)
|
|
264
|
+
total_chars += len(section) + 50
|
|
265
|
+
|
|
266
|
+
truncated = ''.join(result_parts)
|
|
267
|
+
else:
|
|
268
|
+
# No key terms found, use simple truncation
|
|
269
|
+
truncated = output[:max_chars - 200] # Reserve space for message
|
|
270
|
+
# Try to break at a reasonable point (newline) near the limit
|
|
271
|
+
last_newline = truncated.rfind('\n')
|
|
272
|
+
if last_newline > len(truncated) * 0.8: # If newline is in last 20%, use it
|
|
273
|
+
truncated = truncated[:last_newline]
|
|
274
|
+
|
|
275
|
+
truncation_msg = f"\n\n[OUTPUT TRUNCATED - Original length: {len(output)} characters. Full output preserved in downloaded files and visualizations.]"
|
|
276
|
+
return truncated + truncation_msg, True
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Script generation module for code executor.
|
|
4
|
+
Handles creation of safe execution scripts with security overrides.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import traceback
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Import CodeExecutionError class definition locally to avoid circular imports
|
|
13
|
+
class CodeExecutionError(Exception):
|
|
14
|
+
"""Raised when code execution fails."""
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def create_safe_execution_script(code: str, exec_dir: Path) -> Path:
|
|
21
|
+
"""
|
|
22
|
+
Create a Python script with the user code wrapped in safety measures.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
code: User's Python code
|
|
26
|
+
exec_dir: Execution directory
|
|
27
|
+
Returns:
|
|
28
|
+
Path to the created script
|
|
29
|
+
"""
|
|
30
|
+
try:
|
|
31
|
+
# Indent each line of user code to fit inside the try block
|
|
32
|
+
indented_code = '\n'.join(' ' + line for line in code.split('\n'))
|
|
33
|
+
|
|
34
|
+
script_content = f'''#!/usr/bin/env python3
|
|
35
|
+
import sys
|
|
36
|
+
import os
|
|
37
|
+
import json
|
|
38
|
+
import traceback
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
|
|
41
|
+
# Change to execution directory
|
|
42
|
+
os.chdir(r"{exec_dir}")
|
|
43
|
+
|
|
44
|
+
# Configure matplotlib for safe plotting
|
|
45
|
+
import matplotlib
|
|
46
|
+
matplotlib.use('Agg') # Use non-interactive backend that saves to files
|
|
47
|
+
matplotlib.rcParams['savefig.directory'] = r"{exec_dir}" # Default save directory
|
|
48
|
+
|
|
49
|
+
# Restrict file operations to current directory only
|
|
50
|
+
original_open = open
|
|
51
|
+
|
|
52
|
+
def safe_open(file, mode='r', **kwargs):
|
|
53
|
+
"""Override open to restrict file access to execution directory, with exceptions for safe plotting libraries."""
|
|
54
|
+
file_path = Path(file).resolve()
|
|
55
|
+
exec_path = Path(r"{exec_dir}").resolve()
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
file_path.relative_to(exec_path)
|
|
59
|
+
# File is in execution directory - always allow
|
|
60
|
+
return original_open(file, mode, **kwargs)
|
|
61
|
+
except ValueError:
|
|
62
|
+
# File is outside execution directory - check if it's an allowed library file
|
|
63
|
+
file_str = str(file_path)
|
|
64
|
+
|
|
65
|
+
# Allow matplotlib and seaborn configuration and data files (read-only)
|
|
66
|
+
allowed_paths = [
|
|
67
|
+
'/matplotlib/',
|
|
68
|
+
'/seaborn/',
|
|
69
|
+
'/site-packages/matplotlib/',
|
|
70
|
+
'/site-packages/seaborn/',
|
|
71
|
+
'matplotlib/mpl-data/',
|
|
72
|
+
'matplotlib/backends/',
|
|
73
|
+
'matplotlib/font_manager.py',
|
|
74
|
+
'seaborn/data/',
|
|
75
|
+
'seaborn/_core/',
|
|
76
|
+
'numpy/core/',
|
|
77
|
+
'pandas/io/',
|
|
78
|
+
'/usr/share/fonts/',
|
|
79
|
+
'/usr/local/share/fonts/',
|
|
80
|
+
'fontconfig/',
|
|
81
|
+
'.cache/matplotlib/',
|
|
82
|
+
'/tmp/matplotlib-',
|
|
83
|
+
'/home/.matplotlib/',
|
|
84
|
+
'/.matplotlib/',
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
# Check if file path contains any allowed library paths
|
|
88
|
+
is_allowed_path = any(allowed_path in file_str for allowed_path in allowed_paths)
|
|
89
|
+
|
|
90
|
+
if not is_allowed_path:
|
|
91
|
+
raise PermissionError(f"File access outside execution directory not allowed: {{file}}")
|
|
92
|
+
|
|
93
|
+
# Allow read access to all allowed paths
|
|
94
|
+
if 'r' in mode and 'w' not in mode and 'a' not in mode and '+' not in mode:
|
|
95
|
+
return original_open(file, mode, **kwargs)
|
|
96
|
+
|
|
97
|
+
# Allow write access only to matplotlib cache directories
|
|
98
|
+
if ('.cache/matplotlib/' in file_str or
|
|
99
|
+
'matplotlib/fontList.cache' in file_str or
|
|
100
|
+
'matplotlib/tex.cache' in file_str or
|
|
101
|
+
'/tmp/matplotlib-' in file_str or
|
|
102
|
+
'/.matplotlib/' in file_str):
|
|
103
|
+
return original_open(file, mode, **kwargs)
|
|
104
|
+
|
|
105
|
+
# Deny write access to other external files
|
|
106
|
+
if 'w' in mode or 'a' in mode or '+' in mode:
|
|
107
|
+
raise PermissionError(f"Write access outside execution directory not allowed: {{file}}")
|
|
108
|
+
|
|
109
|
+
return original_open(file, mode, **kwargs)
|
|
110
|
+
|
|
111
|
+
# Override built-in open
|
|
112
|
+
if isinstance(__builtins__, dict):
|
|
113
|
+
__builtins__['open'] = safe_open
|
|
114
|
+
else:
|
|
115
|
+
__builtins__.open = safe_open
|
|
116
|
+
|
|
117
|
+
# Capture output
|
|
118
|
+
import io
|
|
119
|
+
import sys
|
|
120
|
+
|
|
121
|
+
stdout_buffer = io.StringIO()
|
|
122
|
+
stderr_buffer = io.StringIO()
|
|
123
|
+
|
|
124
|
+
# Redirect stdout and stderr
|
|
125
|
+
old_stdout = sys.stdout
|
|
126
|
+
old_stderr = sys.stderr
|
|
127
|
+
sys.stdout = stdout_buffer
|
|
128
|
+
sys.stderr = stderr_buffer
|
|
129
|
+
|
|
130
|
+
execution_error = None
|
|
131
|
+
error_traceback = None
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
# User code starts here (matplotlib/seaborn should now work with plotting)
|
|
135
|
+
{indented_code}
|
|
136
|
+
# User code ends here
|
|
137
|
+
|
|
138
|
+
# Auto-save any open matplotlib figures so they surface in UI even if user didn't call plt.savefig()
|
|
139
|
+
try:
|
|
140
|
+
# Only run if matplotlib/pyplot is available
|
|
141
|
+
if 'matplotlib' in sys.modules:
|
|
142
|
+
import matplotlib.pyplot as plt # type: ignore
|
|
143
|
+
fig_nums = list(plt.get_fignums())
|
|
144
|
+
if fig_nums:
|
|
145
|
+
for _fig_num in fig_nums:
|
|
146
|
+
try:
|
|
147
|
+
fig = plt.figure(_fig_num)
|
|
148
|
+
out_name = "plot_" + str(_fig_num) + ".png"
|
|
149
|
+
fig.savefig(out_name)
|
|
150
|
+
except Exception:
|
|
151
|
+
# Don't fail user code on save issues
|
|
152
|
+
pass
|
|
153
|
+
try:
|
|
154
|
+
plt.close('all')
|
|
155
|
+
except Exception:
|
|
156
|
+
pass
|
|
157
|
+
except Exception:
|
|
158
|
+
# Silent best-effort; plotting is optional
|
|
159
|
+
pass
|
|
160
|
+
|
|
161
|
+
except Exception as e:
|
|
162
|
+
execution_error = e
|
|
163
|
+
error_traceback = traceback.format_exc()
|
|
164
|
+
print(f"Execution error: {{type(e).__name__}}: {{str(e)}}", file=sys.stderr)
|
|
165
|
+
print(f"Traceback:\\n{{error_traceback}}", file=sys.stderr)
|
|
166
|
+
|
|
167
|
+
finally:
|
|
168
|
+
# Restore stdout and stderr
|
|
169
|
+
sys.stdout = old_stdout
|
|
170
|
+
sys.stderr = old_stderr
|
|
171
|
+
|
|
172
|
+
# Output results
|
|
173
|
+
result = {{
|
|
174
|
+
"stdout": stdout_buffer.getvalue(),
|
|
175
|
+
"stderr": stderr_buffer.getvalue(),
|
|
176
|
+
"success": execution_error is None,
|
|
177
|
+
"error_type": type(execution_error).__name__ if execution_error else None,
|
|
178
|
+
"error_traceback": error_traceback
|
|
179
|
+
}}
|
|
180
|
+
|
|
181
|
+
print(json.dumps(result))
|
|
182
|
+
'''
|
|
183
|
+
|
|
184
|
+
script_path = exec_dir / "exec_script.py"
|
|
185
|
+
with open(script_path, 'w') as f:
|
|
186
|
+
f.write(script_content)
|
|
187
|
+
|
|
188
|
+
logger.info(f"Created execution script: {script_path}")
|
|
189
|
+
return script_path
|
|
190
|
+
|
|
191
|
+
except Exception as e:
|
|
192
|
+
error_msg = f"Failed to create execution script: {str(e)}"
|
|
193
|
+
logger.error(error_msg)
|
|
194
|
+
logger.error(f"Traceback: {traceback.format_exc()}")
|
|
195
|
+
raise CodeExecutionError(error_msg)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Security checking module for code executor.
|
|
4
|
+
Contains AST-based security analysis for Python code.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import ast
|
|
8
|
+
import logging
|
|
9
|
+
import traceback
|
|
10
|
+
from typing import List
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CodeSecurityError(Exception):
|
|
16
|
+
"""Raised when code fails security checks."""
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SecurityChecker(ast.NodeVisitor):
|
|
21
|
+
"""AST visitor to check for dangerous code patterns."""
|
|
22
|
+
|
|
23
|
+
def __init__(self):
|
|
24
|
+
self.violations = []
|
|
25
|
+
self.imported_modules = set()
|
|
26
|
+
|
|
27
|
+
# Dangerous modules that should never be imported
|
|
28
|
+
self.forbidden_modules = {
|
|
29
|
+
'os', 'sys', 'subprocess', 'socket', 'urllib', 'urllib2', 'urllib3',
|
|
30
|
+
'requests', 'http', 'ftplib', 'smtplib', 'telnetlib', 'webbrowser',
|
|
31
|
+
'ctypes', 'multiprocessing', 'threading', 'asyncio', 'concurrent',
|
|
32
|
+
'pickle', 'dill', 'shelve', 'dbm', 'sqlite3', 'pymongo',
|
|
33
|
+
'paramiko', 'fabric', 'pexpect', 'pty', 'tty',
|
|
34
|
+
'importlib', '__builtin__', 'builtins', 'imp'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# Allowed safe modules for data analysis
|
|
38
|
+
self.allowed_modules = {
|
|
39
|
+
'numpy', 'np', 'pandas', 'pd', 'matplotlib', 'plt', 'seaborn', 'sns',
|
|
40
|
+
'scipy', 'sklearn', 'PIL', 'pillow', 'openpyxl',
|
|
41
|
+
'json', 'csv', 'datetime', 'math', 'statistics', 'random', 're',
|
|
42
|
+
'collections', 'itertools', 'functools', 'operator', 'copy',
|
|
43
|
+
'decimal', 'fractions', 'pathlib', 'typing'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Dangerous function names
|
|
47
|
+
self.forbidden_functions = {
|
|
48
|
+
'eval', 'exec', 'compile', '__import__', 'getattr', 'setattr', 'delattr',
|
|
49
|
+
'hasattr', 'callable', 'isinstance', 'issubclass', 'super', 'globals',
|
|
50
|
+
'locals', 'vars', 'dir', 'help', 'input', 'raw_input', 'exit', 'quit'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
def visit_Import(self, node):
|
|
54
|
+
"""Check import statements."""
|
|
55
|
+
for alias in node.names:
|
|
56
|
+
module_name = alias.name.split('.')[0]
|
|
57
|
+
self.imported_modules.add(module_name)
|
|
58
|
+
|
|
59
|
+
if module_name in self.forbidden_modules:
|
|
60
|
+
self.violations.append(f"Forbidden module import: {module_name}")
|
|
61
|
+
elif module_name not in self.allowed_modules:
|
|
62
|
+
self.violations.append(f"Unauthorized module import: {module_name}")
|
|
63
|
+
|
|
64
|
+
self.generic_visit(node)
|
|
65
|
+
|
|
66
|
+
def visit_ImportFrom(self, node):
|
|
67
|
+
"""Check from...import statements."""
|
|
68
|
+
if node.module:
|
|
69
|
+
module_name = node.module.split('.')[0]
|
|
70
|
+
self.imported_modules.add(module_name)
|
|
71
|
+
|
|
72
|
+
if module_name in self.forbidden_modules:
|
|
73
|
+
self.violations.append(f"Forbidden module import: {module_name}")
|
|
74
|
+
elif module_name not in self.allowed_modules:
|
|
75
|
+
self.violations.append(f"Unauthorized module import: {module_name}")
|
|
76
|
+
|
|
77
|
+
self.generic_visit(node)
|
|
78
|
+
|
|
79
|
+
def visit_Call(self, node):
|
|
80
|
+
"""Check function calls."""
|
|
81
|
+
# Check for dangerous built-in functions
|
|
82
|
+
if isinstance(node.func, ast.Name):
|
|
83
|
+
if node.func.id in self.forbidden_functions:
|
|
84
|
+
self.violations.append(f"Forbidden function call: {node.func.id}")
|
|
85
|
+
|
|
86
|
+
# Check for file operations outside working directory
|
|
87
|
+
elif isinstance(node.func, ast.Attribute):
|
|
88
|
+
if (isinstance(node.func.value, ast.Name) and
|
|
89
|
+
node.func.value.id == 'open' or node.func.attr == 'open'):
|
|
90
|
+
# Allow open() but we'll validate paths at runtime
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
self.generic_visit(node)
|
|
94
|
+
|
|
95
|
+
def visit_With(self, node):
|
|
96
|
+
"""Check with statements (often used for file operations)."""
|
|
97
|
+
for item in node.items:
|
|
98
|
+
if isinstance(item.context_expr, ast.Call):
|
|
99
|
+
if (isinstance(item.context_expr.func, ast.Name) and
|
|
100
|
+
item.context_expr.func.id == 'open'):
|
|
101
|
+
# Allow open() but validate paths at runtime
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
self.generic_visit(node)
|
|
105
|
+
|
|
106
|
+
def visit_Attribute(self, node):
|
|
107
|
+
"""Check attribute access."""
|
|
108
|
+
# Check for dangerous attribute access patterns
|
|
109
|
+
if isinstance(node.value, ast.Name):
|
|
110
|
+
if (node.value.id == '__builtins__' or
|
|
111
|
+
node.attr.startswith('__') and node.attr.endswith('__')):
|
|
112
|
+
self.violations.append(f"Forbidden attribute access: {node.value.id}.{node.attr}")
|
|
113
|
+
|
|
114
|
+
self.generic_visit(node)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def check_code_security(code: str) -> List[str]:
|
|
118
|
+
"""
|
|
119
|
+
Check Python code for security violations using AST parsing.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
code: Python code to check
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
List of security violations (empty if safe)
|
|
126
|
+
"""
|
|
127
|
+
try:
|
|
128
|
+
tree = ast.parse(code)
|
|
129
|
+
checker = SecurityChecker()
|
|
130
|
+
checker.visit(tree)
|
|
131
|
+
return checker.violations
|
|
132
|
+
except SyntaxError as e:
|
|
133
|
+
error_msg = f"Syntax error: {str(e)}"
|
|
134
|
+
logger.warning(f"Code syntax error: {error_msg}")
|
|
135
|
+
return [error_msg]
|
|
136
|
+
except Exception as e:
|
|
137
|
+
error_msg = f"Security check error: {str(e)}"
|
|
138
|
+
logger.error(f"Unexpected error during security check: {error_msg}")
|
|
139
|
+
logger.error(f"Traceback: {traceback.format_exc()}")
|
|
140
|
+
return [error_msg]
|