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,295 @@
|
|
|
1
|
+
"""
|
|
2
|
+
File management utilities for handling files across the application.
|
|
3
|
+
|
|
4
|
+
This module provides utilities for:
|
|
5
|
+
- Content type detection
|
|
6
|
+
- File categorization
|
|
7
|
+
- File metadata management
|
|
8
|
+
- Integration with S3 storage
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import re
|
|
13
|
+
from typing import Any, Dict, List, Optional
|
|
14
|
+
|
|
15
|
+
from .s3_client import S3StorageClient
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FileManager:
|
|
21
|
+
"""Centralized file management with S3 integration."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, s3_client: Optional[S3StorageClient] = None):
|
|
24
|
+
"""Initialize with optional S3 client dependency injection."""
|
|
25
|
+
self.s3_client = s3_client or S3StorageClient()
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def sanitize_filename(filename: str) -> str:
|
|
29
|
+
"""Replace whitespace in a filename with underscores."""
|
|
30
|
+
return re.sub(r"\s+", "_", filename)
|
|
31
|
+
|
|
32
|
+
def get_content_type(self, filename: str) -> str:
|
|
33
|
+
"""Determine content type based on filename."""
|
|
34
|
+
extension = filename.lower().split('.')[-1] if '.' in filename else ''
|
|
35
|
+
|
|
36
|
+
content_types = {
|
|
37
|
+
'txt': 'text/plain',
|
|
38
|
+
'md': 'text/markdown',
|
|
39
|
+
'json': 'application/json',
|
|
40
|
+
'csv': 'text/csv',
|
|
41
|
+
'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
42
|
+
'pdf': 'application/pdf',
|
|
43
|
+
'png': 'image/png',
|
|
44
|
+
'jpg': 'image/jpeg',
|
|
45
|
+
'jpeg': 'image/jpeg',
|
|
46
|
+
'gif': 'image/gif',
|
|
47
|
+
'py': 'text/x-python',
|
|
48
|
+
'js': 'application/javascript',
|
|
49
|
+
'html': 'text/html',
|
|
50
|
+
'css': 'text/css'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return content_types.get(extension, 'application/octet-stream')
|
|
54
|
+
|
|
55
|
+
def categorize_file_type(self, filename: str) -> str:
|
|
56
|
+
"""Categorize file based on extension."""
|
|
57
|
+
extension = filename.lower().split('.')[-1] if '.' in filename else ''
|
|
58
|
+
|
|
59
|
+
code_extensions = {'py', 'js', 'jsx', 'ts', 'tsx', 'html', 'css', 'java', 'cpp', 'c', 'rs', 'go', 'php', 'rb', 'swift'}
|
|
60
|
+
image_extensions = {'png', 'jpg', 'jpeg', 'gif', 'bmp', 'svg', 'webp'}
|
|
61
|
+
data_extensions = {'csv', 'json', 'xlsx', 'xls', 'xml'}
|
|
62
|
+
document_extensions = {'pdf', 'doc', 'docx', 'txt', 'md', 'rtf'}
|
|
63
|
+
|
|
64
|
+
if extension in code_extensions:
|
|
65
|
+
return 'code'
|
|
66
|
+
elif extension in image_extensions:
|
|
67
|
+
return 'image'
|
|
68
|
+
elif extension in data_extensions:
|
|
69
|
+
return 'data'
|
|
70
|
+
elif extension in document_extensions:
|
|
71
|
+
return 'document'
|
|
72
|
+
else:
|
|
73
|
+
return 'other'
|
|
74
|
+
|
|
75
|
+
def get_file_extension(self, filename: str) -> str:
|
|
76
|
+
"""Extract file extension from filename."""
|
|
77
|
+
return '.' + filename.split('.')[-1] if '.' in filename else ''
|
|
78
|
+
|
|
79
|
+
def get_canvas_file_type(self, file_ext: str) -> str:
|
|
80
|
+
"""Determine canvas display type based on file extension."""
|
|
81
|
+
image_exts = {'.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.bmp', '.ico'}
|
|
82
|
+
text_exts = {'.txt', '.md', '.rst', '.csv', '.json', '.xml', '.yaml', '.yml',
|
|
83
|
+
'.py', '.js', '.css', '.ts', '.jsx', '.tsx', '.vue', '.sql'}
|
|
84
|
+
|
|
85
|
+
if file_ext in image_exts:
|
|
86
|
+
return 'image'
|
|
87
|
+
elif file_ext == '.pdf':
|
|
88
|
+
return 'pdf'
|
|
89
|
+
elif file_ext in {'.html', '.htm'}:
|
|
90
|
+
return 'html'
|
|
91
|
+
elif file_ext in text_exts:
|
|
92
|
+
return 'text'
|
|
93
|
+
else:
|
|
94
|
+
return 'other'
|
|
95
|
+
|
|
96
|
+
def should_display_in_canvas(self, filename: str) -> bool:
|
|
97
|
+
"""Check if file should be displayed in canvas based on file type."""
|
|
98
|
+
canvas_extensions = {
|
|
99
|
+
# Images
|
|
100
|
+
'.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.bmp', '.ico',
|
|
101
|
+
# Documents
|
|
102
|
+
'.pdf', '.html', '.htm',
|
|
103
|
+
# Text/code files
|
|
104
|
+
'.txt', '.md', '.rst', '.csv', '.json', '.xml', '.yaml', '.yml',
|
|
105
|
+
'.py', '.js', '.css', '.ts', '.jsx', '.tsx', '.vue', '.sql'
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
file_ext = self.get_file_extension(filename).lower()
|
|
109
|
+
return file_ext in canvas_extensions
|
|
110
|
+
|
|
111
|
+
async def upload_file(
|
|
112
|
+
self,
|
|
113
|
+
user_email: str,
|
|
114
|
+
filename: str,
|
|
115
|
+
content_base64: str,
|
|
116
|
+
source_type: str = "user",
|
|
117
|
+
tags: Optional[Dict[str, str]] = None
|
|
118
|
+
) -> Dict[str, Any]:
|
|
119
|
+
"""Upload a file with automatic content type detection."""
|
|
120
|
+
filename = self.sanitize_filename(filename)
|
|
121
|
+
content_type = self.get_content_type(filename)
|
|
122
|
+
|
|
123
|
+
return await self.s3_client.upload_file(
|
|
124
|
+
user_email=user_email,
|
|
125
|
+
filename=filename,
|
|
126
|
+
content_base64=content_base64,
|
|
127
|
+
content_type=content_type,
|
|
128
|
+
tags=tags,
|
|
129
|
+
source_type=source_type
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
async def upload_multiple_files(
|
|
133
|
+
self,
|
|
134
|
+
user_email: str,
|
|
135
|
+
files: Dict[str, str],
|
|
136
|
+
source_type: str = "user"
|
|
137
|
+
) -> Dict[str, str]:
|
|
138
|
+
"""Upload multiple files and return filename -> s3_key mapping."""
|
|
139
|
+
uploaded_files = {}
|
|
140
|
+
|
|
141
|
+
for original_name, base64_content in files.items():
|
|
142
|
+
safe_name = self.sanitize_filename(original_name)
|
|
143
|
+
try:
|
|
144
|
+
file_metadata = await self.upload_file(
|
|
145
|
+
user_email=user_email,
|
|
146
|
+
filename=safe_name,
|
|
147
|
+
content_base64=base64_content,
|
|
148
|
+
source_type=source_type
|
|
149
|
+
)
|
|
150
|
+
uploaded_files[safe_name] = file_metadata["key"]
|
|
151
|
+
logger.info(f"File uploaded: {safe_name} -> {file_metadata['key']}")
|
|
152
|
+
except Exception as exc:
|
|
153
|
+
logger.error(f"Failed to upload file {safe_name}: {exc}")
|
|
154
|
+
raise
|
|
155
|
+
|
|
156
|
+
return uploaded_files
|
|
157
|
+
|
|
158
|
+
def organize_files_metadata(self, file_references: Dict[str, Dict[str, Any]]) -> Dict[str, Any]:
|
|
159
|
+
"""Organize files metadata by category for UI display."""
|
|
160
|
+
files_metadata = []
|
|
161
|
+
|
|
162
|
+
for filename, file_metadata in file_references.items():
|
|
163
|
+
# Determine source type from tags or metadata
|
|
164
|
+
tags = file_metadata.get("tags", {})
|
|
165
|
+
source_type = tags.get("source", "uploaded")
|
|
166
|
+
source_tool = tags.get("source_tool", None)
|
|
167
|
+
|
|
168
|
+
file_info = {
|
|
169
|
+
'filename': filename,
|
|
170
|
+
's3_key': file_metadata.get("key", ""),
|
|
171
|
+
'size': file_metadata.get("size", 0),
|
|
172
|
+
'type': self.categorize_file_type(filename),
|
|
173
|
+
'source': source_type,
|
|
174
|
+
'source_tool': source_tool,
|
|
175
|
+
'extension': filename.split('.')[-1] if '.' in filename else '',
|
|
176
|
+
'last_modified': file_metadata.get("last_modified").isoformat() if file_metadata.get("last_modified") else None,
|
|
177
|
+
'content_type': file_metadata.get("content_type", "application/octet-stream"),
|
|
178
|
+
'can_display_in_canvas': self.should_display_in_canvas(filename)
|
|
179
|
+
}
|
|
180
|
+
files_metadata.append(file_info)
|
|
181
|
+
|
|
182
|
+
# Group by category
|
|
183
|
+
categorized = {
|
|
184
|
+
'code': [f for f in files_metadata if f['type'] == 'code'],
|
|
185
|
+
'image': [f for f in files_metadata if f['type'] == 'image'],
|
|
186
|
+
'data': [f for f in files_metadata if f['type'] == 'data'],
|
|
187
|
+
'document': [f for f in files_metadata if f['type'] == 'document'],
|
|
188
|
+
'other': [f for f in files_metadata if f['type'] == 'other']
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
'total_files': len(files_metadata),
|
|
193
|
+
'files': files_metadata,
|
|
194
|
+
'categories': categorized
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async def upload_files_from_base64(
|
|
198
|
+
self,
|
|
199
|
+
files: List[Dict[str, Any]],
|
|
200
|
+
user_email: str,
|
|
201
|
+
source_type: str = "tool"
|
|
202
|
+
) -> Dict[str, Dict[str, Any]]:
|
|
203
|
+
"""Upload multiple base64 files and return filename -> metadata mapping.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
files: List of dicts { filename, content, mime_type? }
|
|
207
|
+
user_email: Owner for auth/partitioning
|
|
208
|
+
source_type: "user" or "tool"
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Dict mapping filename -> metadata dict compatible with organize_files_metadata
|
|
212
|
+
"""
|
|
213
|
+
uploaded_refs: Dict[str, Dict[str, Any]] = {}
|
|
214
|
+
for f in files:
|
|
215
|
+
try:
|
|
216
|
+
filename = f.get("filename")
|
|
217
|
+
if filename:
|
|
218
|
+
filename = self.sanitize_filename(filename)
|
|
219
|
+
content_b64 = f.get("content")
|
|
220
|
+
mime_type = f.get("mime_type") or self.get_content_type(filename or "")
|
|
221
|
+
if not filename or not content_b64:
|
|
222
|
+
logger.warning("Skipping upload: missing filename or content")
|
|
223
|
+
continue
|
|
224
|
+
meta = await self.s3_client.upload_file(
|
|
225
|
+
user_email=user_email,
|
|
226
|
+
filename=filename,
|
|
227
|
+
content_base64=content_b64,
|
|
228
|
+
content_type=mime_type,
|
|
229
|
+
tags={"source": source_type},
|
|
230
|
+
source_type=source_type,
|
|
231
|
+
)
|
|
232
|
+
# Normalize minimal reference for session context
|
|
233
|
+
uploaded_refs[filename] = {
|
|
234
|
+
"key": meta.get("key"),
|
|
235
|
+
"content_type": meta.get("content_type", mime_type),
|
|
236
|
+
"size": meta.get("size", 0),
|
|
237
|
+
"source": source_type,
|
|
238
|
+
"last_modified": meta.get("last_modified"),
|
|
239
|
+
"tags": {"source": source_type},
|
|
240
|
+
}
|
|
241
|
+
except Exception as e:
|
|
242
|
+
logger.error(f"Failed to upload artifact {f.get('filename')}: {e}")
|
|
243
|
+
return uploaded_refs
|
|
244
|
+
|
|
245
|
+
def get_canvas_displayable_files(
|
|
246
|
+
self,
|
|
247
|
+
result_dict: Dict[str, Any],
|
|
248
|
+
uploaded_files: Dict[str, str]
|
|
249
|
+
) -> List[Dict]:
|
|
250
|
+
"""Extract files from tool result that should be displayed in canvas."""
|
|
251
|
+
canvas_files = []
|
|
252
|
+
|
|
253
|
+
# Check returned_files array (preferred format)
|
|
254
|
+
if "returned_files" in result_dict and isinstance(result_dict["returned_files"], list):
|
|
255
|
+
for file_info in result_dict["returned_files"]:
|
|
256
|
+
if isinstance(file_info, dict) and "filename" in file_info:
|
|
257
|
+
filename = file_info["filename"]
|
|
258
|
+
|
|
259
|
+
if self.should_display_in_canvas(filename) and filename in uploaded_files:
|
|
260
|
+
canvas_files.append({
|
|
261
|
+
"filename": filename,
|
|
262
|
+
"type": self.get_canvas_file_type(self.get_file_extension(filename).lower()),
|
|
263
|
+
"s3_key": uploaded_files[filename],
|
|
264
|
+
"size": file_info.get("size", 0),
|
|
265
|
+
"source": "tool_generated"
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
# Check legacy single file format
|
|
269
|
+
elif "returned_file_name" in result_dict and "returned_file_base64" in result_dict:
|
|
270
|
+
filename = result_dict["returned_file_name"]
|
|
271
|
+
|
|
272
|
+
if self.should_display_in_canvas(filename) and filename in uploaded_files:
|
|
273
|
+
canvas_files.append({
|
|
274
|
+
"filename": filename,
|
|
275
|
+
"type": self.get_canvas_file_type(self.get_file_extension(filename).lower()),
|
|
276
|
+
"s3_key": uploaded_files[filename],
|
|
277
|
+
"size": 0, # Size not available in legacy format
|
|
278
|
+
"source": "tool_generated"
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
logger.info(f"Found {len(canvas_files)} canvas-displayable files: {[f['filename'] for f in canvas_files]}")
|
|
282
|
+
return canvas_files
|
|
283
|
+
|
|
284
|
+
async def get_file_content(self, user_email: str, filename: str, s3_key: str) -> Optional[str]:
|
|
285
|
+
"""Get base64 content of a file by S3 key."""
|
|
286
|
+
try:
|
|
287
|
+
file_data = await self.s3_client.get_file(user_email, s3_key)
|
|
288
|
+
if file_data:
|
|
289
|
+
return file_data["content_base64"]
|
|
290
|
+
else:
|
|
291
|
+
logger.warning(f"File not found in S3: {s3_key}")
|
|
292
|
+
return None
|
|
293
|
+
except Exception as exc:
|
|
294
|
+
logger.error(f"Error getting file content for {filename}: {exc}")
|
|
295
|
+
return None
|