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.
Files changed (250) hide show
  1. atlas/__init__.py +40 -0
  2. atlas/application/__init__.py +7 -0
  3. atlas/application/chat/__init__.py +7 -0
  4. atlas/application/chat/agent/__init__.py +10 -0
  5. atlas/application/chat/agent/act_loop.py +179 -0
  6. atlas/application/chat/agent/factory.py +142 -0
  7. atlas/application/chat/agent/protocols.py +46 -0
  8. atlas/application/chat/agent/react_loop.py +338 -0
  9. atlas/application/chat/agent/think_act_loop.py +171 -0
  10. atlas/application/chat/approval_manager.py +151 -0
  11. atlas/application/chat/elicitation_manager.py +191 -0
  12. atlas/application/chat/events/__init__.py +1 -0
  13. atlas/application/chat/events/agent_event_relay.py +112 -0
  14. atlas/application/chat/modes/__init__.py +1 -0
  15. atlas/application/chat/modes/agent.py +125 -0
  16. atlas/application/chat/modes/plain.py +74 -0
  17. atlas/application/chat/modes/rag.py +81 -0
  18. atlas/application/chat/modes/tools.py +179 -0
  19. atlas/application/chat/orchestrator.py +213 -0
  20. atlas/application/chat/policies/__init__.py +1 -0
  21. atlas/application/chat/policies/tool_authorization.py +99 -0
  22. atlas/application/chat/preprocessors/__init__.py +1 -0
  23. atlas/application/chat/preprocessors/message_builder.py +92 -0
  24. atlas/application/chat/preprocessors/prompt_override_service.py +104 -0
  25. atlas/application/chat/service.py +454 -0
  26. atlas/application/chat/utilities/__init__.py +6 -0
  27. atlas/application/chat/utilities/error_handler.py +367 -0
  28. atlas/application/chat/utilities/event_notifier.py +546 -0
  29. atlas/application/chat/utilities/file_processor.py +613 -0
  30. atlas/application/chat/utilities/tool_executor.py +789 -0
  31. atlas/atlas_chat_cli.py +347 -0
  32. atlas/atlas_client.py +238 -0
  33. atlas/core/__init__.py +0 -0
  34. atlas/core/auth.py +205 -0
  35. atlas/core/authorization_manager.py +27 -0
  36. atlas/core/capabilities.py +123 -0
  37. atlas/core/compliance.py +215 -0
  38. atlas/core/domain_whitelist.py +147 -0
  39. atlas/core/domain_whitelist_middleware.py +82 -0
  40. atlas/core/http_client.py +28 -0
  41. atlas/core/log_sanitizer.py +102 -0
  42. atlas/core/metrics_logger.py +59 -0
  43. atlas/core/middleware.py +131 -0
  44. atlas/core/otel_config.py +242 -0
  45. atlas/core/prompt_risk.py +200 -0
  46. atlas/core/rate_limit.py +0 -0
  47. atlas/core/rate_limit_middleware.py +64 -0
  48. atlas/core/security_headers_middleware.py +51 -0
  49. atlas/domain/__init__.py +37 -0
  50. atlas/domain/chat/__init__.py +1 -0
  51. atlas/domain/chat/dtos.py +85 -0
  52. atlas/domain/errors.py +96 -0
  53. atlas/domain/messages/__init__.py +12 -0
  54. atlas/domain/messages/models.py +160 -0
  55. atlas/domain/rag_mcp_service.py +664 -0
  56. atlas/domain/sessions/__init__.py +7 -0
  57. atlas/domain/sessions/models.py +36 -0
  58. atlas/domain/unified_rag_service.py +371 -0
  59. atlas/infrastructure/__init__.py +10 -0
  60. atlas/infrastructure/app_factory.py +135 -0
  61. atlas/infrastructure/events/__init__.py +1 -0
  62. atlas/infrastructure/events/cli_event_publisher.py +140 -0
  63. atlas/infrastructure/events/websocket_publisher.py +140 -0
  64. atlas/infrastructure/sessions/in_memory_repository.py +56 -0
  65. atlas/infrastructure/transport/__init__.py +7 -0
  66. atlas/infrastructure/transport/websocket_connection_adapter.py +33 -0
  67. atlas/init_cli.py +226 -0
  68. atlas/interfaces/__init__.py +15 -0
  69. atlas/interfaces/events.py +134 -0
  70. atlas/interfaces/llm.py +54 -0
  71. atlas/interfaces/rag.py +40 -0
  72. atlas/interfaces/sessions.py +75 -0
  73. atlas/interfaces/tools.py +57 -0
  74. atlas/interfaces/transport.py +24 -0
  75. atlas/main.py +564 -0
  76. atlas/mcp/api_key_demo/README.md +76 -0
  77. atlas/mcp/api_key_demo/main.py +172 -0
  78. atlas/mcp/api_key_demo/run.sh +56 -0
  79. atlas/mcp/basictable/main.py +147 -0
  80. atlas/mcp/calculator/main.py +149 -0
  81. atlas/mcp/code-executor/execution_engine.py +98 -0
  82. atlas/mcp/code-executor/execution_environment.py +95 -0
  83. atlas/mcp/code-executor/main.py +528 -0
  84. atlas/mcp/code-executor/result_processing.py +276 -0
  85. atlas/mcp/code-executor/script_generation.py +195 -0
  86. atlas/mcp/code-executor/security_checker.py +140 -0
  87. atlas/mcp/corporate_cars/main.py +437 -0
  88. atlas/mcp/csv_reporter/main.py +545 -0
  89. atlas/mcp/duckduckgo/main.py +182 -0
  90. atlas/mcp/elicitation_demo/README.md +171 -0
  91. atlas/mcp/elicitation_demo/main.py +262 -0
  92. atlas/mcp/env-demo/README.md +158 -0
  93. atlas/mcp/env-demo/main.py +199 -0
  94. atlas/mcp/file_size_test/main.py +284 -0
  95. atlas/mcp/filesystem/main.py +348 -0
  96. atlas/mcp/image_demo/main.py +113 -0
  97. atlas/mcp/image_demo/requirements.txt +4 -0
  98. atlas/mcp/logging_demo/README.md +72 -0
  99. atlas/mcp/logging_demo/main.py +103 -0
  100. atlas/mcp/many_tools_demo/main.py +50 -0
  101. atlas/mcp/order_database/__init__.py +0 -0
  102. atlas/mcp/order_database/main.py +369 -0
  103. atlas/mcp/order_database/signal_data.csv +1001 -0
  104. atlas/mcp/pdfbasic/main.py +394 -0
  105. atlas/mcp/pptx_generator/main.py +760 -0
  106. atlas/mcp/pptx_generator/requirements.txt +13 -0
  107. atlas/mcp/pptx_generator/run_test.sh +1 -0
  108. atlas/mcp/pptx_generator/test_pptx_generator_security.py +169 -0
  109. atlas/mcp/progress_demo/main.py +167 -0
  110. atlas/mcp/progress_updates_demo/QUICKSTART.md +273 -0
  111. atlas/mcp/progress_updates_demo/README.md +120 -0
  112. atlas/mcp/progress_updates_demo/main.py +497 -0
  113. atlas/mcp/prompts/main.py +222 -0
  114. atlas/mcp/public_demo/main.py +189 -0
  115. atlas/mcp/sampling_demo/README.md +169 -0
  116. atlas/mcp/sampling_demo/main.py +234 -0
  117. atlas/mcp/thinking/main.py +77 -0
  118. atlas/mcp/tool_planner/main.py +240 -0
  119. atlas/mcp/ui-demo/badmesh.png +0 -0
  120. atlas/mcp/ui-demo/main.py +383 -0
  121. atlas/mcp/ui-demo/templates/button_demo.html +32 -0
  122. atlas/mcp/ui-demo/templates/data_visualization.html +32 -0
  123. atlas/mcp/ui-demo/templates/form_demo.html +28 -0
  124. atlas/mcp/username-override-demo/README.md +320 -0
  125. atlas/mcp/username-override-demo/main.py +308 -0
  126. atlas/modules/__init__.py +0 -0
  127. atlas/modules/config/__init__.py +34 -0
  128. atlas/modules/config/cli.py +231 -0
  129. atlas/modules/config/config_manager.py +1096 -0
  130. atlas/modules/file_storage/__init__.py +22 -0
  131. atlas/modules/file_storage/cli.py +330 -0
  132. atlas/modules/file_storage/content_extractor.py +290 -0
  133. atlas/modules/file_storage/manager.py +295 -0
  134. atlas/modules/file_storage/mock_s3_client.py +402 -0
  135. atlas/modules/file_storage/s3_client.py +417 -0
  136. atlas/modules/llm/__init__.py +19 -0
  137. atlas/modules/llm/caller.py +287 -0
  138. atlas/modules/llm/litellm_caller.py +675 -0
  139. atlas/modules/llm/models.py +19 -0
  140. atlas/modules/mcp_tools/__init__.py +17 -0
  141. atlas/modules/mcp_tools/client.py +2123 -0
  142. atlas/modules/mcp_tools/token_storage.py +556 -0
  143. atlas/modules/prompts/prompt_provider.py +130 -0
  144. atlas/modules/rag/__init__.py +24 -0
  145. atlas/modules/rag/atlas_rag_client.py +336 -0
  146. atlas/modules/rag/client.py +129 -0
  147. atlas/routes/admin_routes.py +865 -0
  148. atlas/routes/config_routes.py +484 -0
  149. atlas/routes/feedback_routes.py +361 -0
  150. atlas/routes/files_routes.py +274 -0
  151. atlas/routes/health_routes.py +40 -0
  152. atlas/routes/mcp_auth_routes.py +223 -0
  153. atlas/server_cli.py +164 -0
  154. atlas/tests/conftest.py +20 -0
  155. atlas/tests/integration/test_mcp_auth_integration.py +152 -0
  156. atlas/tests/manual_test_sampling.py +87 -0
  157. atlas/tests/modules/mcp_tools/test_client_auth.py +226 -0
  158. atlas/tests/modules/mcp_tools/test_client_env.py +191 -0
  159. atlas/tests/test_admin_mcp_server_management_routes.py +141 -0
  160. atlas/tests/test_agent_roa.py +135 -0
  161. atlas/tests/test_app_factory_smoke.py +47 -0
  162. atlas/tests/test_approval_manager.py +439 -0
  163. atlas/tests/test_atlas_client.py +188 -0
  164. atlas/tests/test_atlas_rag_client.py +447 -0
  165. atlas/tests/test_atlas_rag_integration.py +224 -0
  166. atlas/tests/test_attach_file_flow.py +287 -0
  167. atlas/tests/test_auth_utils.py +165 -0
  168. atlas/tests/test_backend_public_url.py +185 -0
  169. atlas/tests/test_banner_logging.py +287 -0
  170. atlas/tests/test_capability_tokens_and_injection.py +203 -0
  171. atlas/tests/test_compliance_level.py +54 -0
  172. atlas/tests/test_compliance_manager.py +253 -0
  173. atlas/tests/test_config_manager.py +617 -0
  174. atlas/tests/test_config_manager_paths.py +12 -0
  175. atlas/tests/test_core_auth.py +18 -0
  176. atlas/tests/test_core_utils.py +190 -0
  177. atlas/tests/test_docker_env_sync.py +202 -0
  178. atlas/tests/test_domain_errors.py +329 -0
  179. atlas/tests/test_domain_whitelist.py +359 -0
  180. atlas/tests/test_elicitation_manager.py +408 -0
  181. atlas/tests/test_elicitation_routing.py +296 -0
  182. atlas/tests/test_env_demo_server.py +88 -0
  183. atlas/tests/test_error_classification.py +113 -0
  184. atlas/tests/test_error_flow_integration.py +116 -0
  185. atlas/tests/test_feedback_routes.py +333 -0
  186. atlas/tests/test_file_content_extraction.py +1134 -0
  187. atlas/tests/test_file_extraction_routes.py +158 -0
  188. atlas/tests/test_file_library.py +107 -0
  189. atlas/tests/test_file_manager_unit.py +18 -0
  190. atlas/tests/test_health_route.py +49 -0
  191. atlas/tests/test_http_client_stub.py +8 -0
  192. atlas/tests/test_imports_smoke.py +30 -0
  193. atlas/tests/test_interfaces_llm_response.py +9 -0
  194. atlas/tests/test_issue_access_denied_fix.py +136 -0
  195. atlas/tests/test_llm_env_expansion.py +836 -0
  196. atlas/tests/test_log_level_sensitive_data.py +285 -0
  197. atlas/tests/test_mcp_auth_routes.py +341 -0
  198. atlas/tests/test_mcp_client_auth.py +331 -0
  199. atlas/tests/test_mcp_data_injection.py +270 -0
  200. atlas/tests/test_mcp_get_authorized_servers.py +95 -0
  201. atlas/tests/test_mcp_hot_reload.py +512 -0
  202. atlas/tests/test_mcp_image_content.py +424 -0
  203. atlas/tests/test_mcp_logging.py +172 -0
  204. atlas/tests/test_mcp_progress_updates.py +313 -0
  205. atlas/tests/test_mcp_prompt_override_system_prompt.py +102 -0
  206. atlas/tests/test_mcp_prompts_server.py +39 -0
  207. atlas/tests/test_mcp_tool_result_parsing.py +296 -0
  208. atlas/tests/test_metrics_logger.py +56 -0
  209. atlas/tests/test_middleware_auth.py +379 -0
  210. atlas/tests/test_prompt_risk_and_acl.py +141 -0
  211. atlas/tests/test_rag_mcp_aggregator.py +204 -0
  212. atlas/tests/test_rag_mcp_service.py +224 -0
  213. atlas/tests/test_rate_limit_middleware.py +45 -0
  214. atlas/tests/test_routes_config_smoke.py +60 -0
  215. atlas/tests/test_routes_files_download_token.py +41 -0
  216. atlas/tests/test_routes_files_health.py +18 -0
  217. atlas/tests/test_runtime_imports.py +53 -0
  218. atlas/tests/test_sampling_integration.py +482 -0
  219. atlas/tests/test_security_admin_routes.py +61 -0
  220. atlas/tests/test_security_capability_tokens.py +65 -0
  221. atlas/tests/test_security_file_stats_scope.py +21 -0
  222. atlas/tests/test_security_header_injection.py +191 -0
  223. atlas/tests/test_security_headers_and_filename.py +63 -0
  224. atlas/tests/test_shared_session_repository.py +101 -0
  225. atlas/tests/test_system_prompt_loading.py +181 -0
  226. atlas/tests/test_token_storage.py +505 -0
  227. atlas/tests/test_tool_approval_config.py +93 -0
  228. atlas/tests/test_tool_approval_utils.py +356 -0
  229. atlas/tests/test_tool_authorization_group_filtering.py +223 -0
  230. atlas/tests/test_tool_details_in_config.py +108 -0
  231. atlas/tests/test_tool_planner.py +300 -0
  232. atlas/tests/test_unified_rag_service.py +398 -0
  233. atlas/tests/test_username_override_in_approval.py +258 -0
  234. atlas/tests/test_websocket_auth_header.py +168 -0
  235. atlas/version.py +6 -0
  236. atlas_chat-0.1.0.data/data/.env.example +253 -0
  237. atlas_chat-0.1.0.data/data/config/defaults/compliance-levels.json +44 -0
  238. atlas_chat-0.1.0.data/data/config/defaults/domain-whitelist.json +123 -0
  239. atlas_chat-0.1.0.data/data/config/defaults/file-extractors.json +74 -0
  240. atlas_chat-0.1.0.data/data/config/defaults/help-config.json +198 -0
  241. atlas_chat-0.1.0.data/data/config/defaults/llmconfig-buggy.yml +11 -0
  242. atlas_chat-0.1.0.data/data/config/defaults/llmconfig.yml +19 -0
  243. atlas_chat-0.1.0.data/data/config/defaults/mcp.json +138 -0
  244. atlas_chat-0.1.0.data/data/config/defaults/rag-sources.json +17 -0
  245. atlas_chat-0.1.0.data/data/config/defaults/splash-config.json +16 -0
  246. atlas_chat-0.1.0.dist-info/METADATA +236 -0
  247. atlas_chat-0.1.0.dist-info/RECORD +250 -0
  248. atlas_chat-0.1.0.dist-info/WHEEL +5 -0
  249. atlas_chat-0.1.0.dist-info/entry_points.txt +4 -0
  250. 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