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,417 @@
1
+ """
2
+ S3 Client for file storage operations.
3
+
4
+ This module provides a client interface to interact with S3-compatible storage
5
+ (MinIO or AWS S3) using boto3.
6
+ """
7
+
8
+ import base64
9
+ import logging
10
+ import time
11
+ import uuid
12
+ from typing import Any, Dict, List, Optional
13
+ from urllib.parse import quote
14
+
15
+ import boto3
16
+ from botocore.client import Config
17
+ from botocore.exceptions import ClientError
18
+
19
+ from atlas.core.log_sanitizer import sanitize_for_logging
20
+ from atlas.core.metrics_logger import log_metric
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ class S3StorageClient:
26
+ """Client for interacting with S3-compatible storage (MinIO/AWS S3)."""
27
+
28
+ def __init__(
29
+ self,
30
+ s3_endpoint: str = None,
31
+ s3_bucket_name: str = None,
32
+ s3_access_key: str = None,
33
+ s3_secret_key: str = None,
34
+ s3_region: str = None,
35
+ s3_timeout: int = None,
36
+ s3_use_ssl: bool = None
37
+ ):
38
+ """Initialize the S3 client with configuration."""
39
+ # Allow dependency injection for testing
40
+ if any(param is None for param in [s3_endpoint, s3_bucket_name, s3_access_key, s3_secret_key, s3_region, s3_timeout, s3_use_ssl]):
41
+ from atlas.modules.config import config_manager
42
+ config = config_manager.app_settings
43
+ s3_endpoint = s3_endpoint or config.s3_endpoint
44
+ s3_bucket_name = s3_bucket_name or config.s3_bucket_name
45
+ s3_access_key = s3_access_key or config.s3_access_key
46
+ s3_secret_key = s3_secret_key or config.s3_secret_key
47
+ s3_region = s3_region or config.s3_region
48
+ s3_timeout = s3_timeout or config.s3_timeout
49
+ s3_use_ssl = s3_use_ssl if s3_use_ssl is not None else config.s3_use_ssl
50
+
51
+ self.endpoint_url = s3_endpoint
52
+ self.bucket_name = s3_bucket_name
53
+ self.region = s3_region
54
+ self.timeout = s3_timeout
55
+
56
+ # Create boto3 S3 client
57
+ self.s3_client = boto3.client(
58
+ 's3',
59
+ endpoint_url=self.endpoint_url,
60
+ aws_access_key_id=s3_access_key,
61
+ aws_secret_access_key=s3_secret_key,
62
+ region_name=self.region,
63
+ use_ssl=s3_use_ssl,
64
+ config=Config(
65
+ signature_version='s3v4',
66
+ connect_timeout=s3_timeout,
67
+ read_timeout=s3_timeout,
68
+ retries={'max_attempts': 3}
69
+ )
70
+ )
71
+
72
+ logger.info(f"S3Client initialized with endpoint: {self.endpoint_url}, bucket: {self.bucket_name}")
73
+
74
+ def _generate_s3_key(self, user_email: str, filename: str, source_type: str = "user") -> str:
75
+ """Generate an S3-style key with user isolation."""
76
+ timestamp = int(time.time())
77
+ unique_id = str(uuid.uuid4())[:8]
78
+ safe_filename = filename.replace(" ", "_").replace("/", "_")
79
+
80
+ if source_type == "tool":
81
+ # Tool-generated files go in a special directory
82
+ return f"users/{user_email}/generated/{timestamp}_{unique_id}_{safe_filename}"
83
+ else:
84
+ # User-uploaded files
85
+ return f"users/{user_email}/uploads/{timestamp}_{unique_id}_{safe_filename}"
86
+
87
+
88
+
89
+ async def upload_file(
90
+ self,
91
+ user_email: str,
92
+ filename: str,
93
+ content_base64: str,
94
+ content_type: str = "application/octet-stream",
95
+ tags: Optional[Dict[str, str]] = None,
96
+ source_type: str = "user"
97
+ ) -> Dict[str, Any]:
98
+ """
99
+ Upload a file to S3 storage.
100
+
101
+ Args:
102
+ user_email: Email of the user uploading the file
103
+ filename: Original filename
104
+ content_base64: Base64 encoded file content
105
+ content_type: MIME type of the file
106
+ tags: Additional metadata tags
107
+ source_type: Type of file ("user" or "tool")
108
+
109
+ Returns:
110
+ Dictionary containing file metadata including the S3 key
111
+ """
112
+ try:
113
+ # Decode base64 content
114
+ content_bytes = base64.b64decode(content_base64)
115
+
116
+ # Generate S3 key
117
+ s3_key = self._generate_s3_key(user_email, filename, source_type)
118
+
119
+ # Prepare tags
120
+ file_tags = tags or {}
121
+ file_tags["source"] = source_type
122
+ file_tags["user_email"] = user_email
123
+ file_tags["original_filename"] = filename
124
+
125
+ # Convert tags to S3 tag format (URL-encode values for safety)
126
+ tag_set = "&".join([f"{quote(k, safe='')}={quote(v, safe='')}" for k, v in file_tags.items()])
127
+
128
+ # Upload to S3
129
+ self.s3_client.put_object(
130
+ Bucket=self.bucket_name,
131
+ Key=s3_key,
132
+ Body=content_bytes,
133
+ ContentType=content_type,
134
+ Tagging=tag_set,
135
+ Metadata={
136
+ "user_email": user_email,
137
+ "original_filename": filename,
138
+ "source_type": source_type
139
+ }
140
+ )
141
+
142
+ # Get object metadata for response
143
+ response = self.s3_client.head_object(
144
+ Bucket=self.bucket_name,
145
+ Key=s3_key
146
+ )
147
+
148
+ result = {
149
+ "key": s3_key,
150
+ "filename": filename,
151
+ "size": len(content_bytes),
152
+ "content_type": content_type,
153
+ "last_modified": response['LastModified'],
154
+ "etag": response['ETag'].strip('"'),
155
+ "tags": file_tags,
156
+ "user_email": user_email
157
+ }
158
+
159
+ category = "generated" if "/generated/" in s3_key else ("uploads" if "/uploads/" in s3_key else "other")
160
+ logger.info(
161
+ "File uploaded successfully: category=%s, size=%d bytes, content_type=%s, user=%s",
162
+ category,
163
+ len(content_bytes),
164
+ sanitize_for_logging(content_type),
165
+ sanitize_for_logging(user_email),
166
+ )
167
+ logger.debug("Uploaded file key (sanitized): %s", sanitize_for_logging(s3_key))
168
+
169
+ log_metric("file_stored", user_email, file_size=len(content_bytes), content_type=content_type, category=category)
170
+
171
+ return result
172
+
173
+ except ClientError as e:
174
+ error_msg = f"S3 upload failed: {e.response['Error']['Message']}"
175
+ logger.error(error_msg)
176
+ raise Exception(error_msg)
177
+ except Exception as e:
178
+ logger.error(f"Error uploading file to S3: {str(e)}")
179
+ raise
180
+
181
+ async def get_file(self, user_email: str, file_key: str) -> Dict[str, Any]:
182
+ """
183
+ Get a file from S3 storage.
184
+
185
+ Args:
186
+ user_email: Email of the user requesting the file
187
+ file_key: S3 key of the file to retrieve
188
+
189
+ Returns:
190
+ Dictionary containing file data and metadata
191
+ """
192
+ try:
193
+ # Verify user has access to this file (check if key starts with user's prefix)
194
+ if not file_key.startswith(f"users/{user_email}/"):
195
+ logger.warning(
196
+ "Access denied: user=%s attempted to access key=%s",
197
+ sanitize_for_logging(user_email),
198
+ sanitize_for_logging(file_key.split('/')[-1]),
199
+ )
200
+ raise Exception("Access denied to file")
201
+
202
+ # Get object from S3
203
+ response = self.s3_client.get_object(
204
+ Bucket=self.bucket_name,
205
+ Key=file_key
206
+ )
207
+
208
+ # Read file content
209
+ content_bytes = response['Body'].read()
210
+ content_base64 = base64.b64encode(content_bytes).decode()
211
+
212
+ # Get tags
213
+ try:
214
+ tags_response = self.s3_client.get_object_tagging(
215
+ Bucket=self.bucket_name,
216
+ Key=file_key
217
+ )
218
+ tags = {tag['Key']: tag['Value'] for tag in tags_response.get('TagSet', [])}
219
+ except Exception:
220
+ tags = {}
221
+
222
+ # Extract filename from metadata or key
223
+ metadata = response.get('Metadata', {})
224
+ filename = metadata.get('original_filename', file_key.split('/')[-1])
225
+
226
+ result = {
227
+ "key": file_key,
228
+ "filename": filename,
229
+ "content_base64": content_base64,
230
+ "content_type": response['ContentType'],
231
+ "size": len(content_bytes),
232
+ "last_modified": response['LastModified'],
233
+ "etag": response['ETag'].strip('"'),
234
+ "tags": tags
235
+ }
236
+
237
+ category = "generated" if "/generated/" in file_key else ("uploads" if "/uploads/" in file_key else "other")
238
+ logger.info(
239
+ "File retrieved successfully: category=%s, size=%d bytes, content_type=%s, user=%s",
240
+ category,
241
+ len(content_bytes),
242
+ sanitize_for_logging(response['ContentType']),
243
+ sanitize_for_logging(user_email),
244
+ )
245
+ logger.debug("Retrieved file key (sanitized): %s", sanitize_for_logging(file_key))
246
+ return result
247
+
248
+ except ClientError as e:
249
+ if e.response['Error']['Code'] == 'NoSuchKey':
250
+ logger.warning(f"File not found: {sanitize_for_logging(file_key)} for user {sanitize_for_logging(user_email)}")
251
+ return None
252
+ else:
253
+ error_msg = f"S3 get failed: {e.response['Error']['Message']}"
254
+ logger.error(error_msg)
255
+ raise Exception(error_msg)
256
+ except Exception as e:
257
+ logger.error(f"Error getting file from S3: {str(e)}")
258
+ raise
259
+
260
+ async def list_files(
261
+ self,
262
+ user_email: str,
263
+ file_type: Optional[str] = None,
264
+ limit: int = 100
265
+ ) -> List[Dict[str, Any]]:
266
+ """
267
+ List files for a user.
268
+
269
+ Args:
270
+ user_email: Email of the user
271
+ file_type: Optional filter by file type ("user" or "tool")
272
+ limit: Maximum number of files to return
273
+
274
+ Returns:
275
+ List of file metadata dictionaries
276
+ """
277
+ try:
278
+ # List objects with user's prefix
279
+ prefix = f"users/{user_email}/"
280
+ if file_type == "tool":
281
+ prefix = f"users/{user_email}/generated/"
282
+ elif file_type == "user":
283
+ prefix = f"users/{user_email}/uploads/"
284
+
285
+ response = self.s3_client.list_objects_v2(
286
+ Bucket=self.bucket_name,
287
+ Prefix=prefix,
288
+ MaxKeys=limit
289
+ )
290
+
291
+ files = []
292
+ for obj in response.get('Contents', []):
293
+ # Get tags for each object
294
+ try:
295
+ tags_response = self.s3_client.get_object_tagging(
296
+ Bucket=self.bucket_name,
297
+ Key=obj['Key']
298
+ )
299
+ tags = {tag['Key']: tag['Value'] for tag in tags_response.get('TagSet', [])}
300
+ except Exception:
301
+ tags = {}
302
+
303
+ # Get metadata
304
+ try:
305
+ head_response = self.s3_client.head_object(
306
+ Bucket=self.bucket_name,
307
+ Key=obj['Key']
308
+ )
309
+ metadata = head_response.get('Metadata', {})
310
+ content_type = head_response.get('ContentType', 'application/octet-stream')
311
+ filename = metadata.get('original_filename', obj['Key'].split('/')[-1])
312
+ except Exception:
313
+ content_type = 'application/octet-stream'
314
+ filename = obj['Key'].split('/')[-1]
315
+
316
+ files.append({
317
+ "key": obj['Key'],
318
+ "filename": filename,
319
+ "size": obj['Size'],
320
+ "content_type": content_type,
321
+ "last_modified": obj['LastModified'],
322
+ "etag": obj['ETag'].strip('"'),
323
+ "tags": tags,
324
+ "user_email": user_email
325
+ })
326
+
327
+ # Sort by last modified, newest first
328
+ files.sort(key=lambda f: f['last_modified'], reverse=True)
329
+
330
+ logger.info(f"Listed {len(files)} files for user {sanitize_for_logging(user_email)}")
331
+ return files
332
+
333
+ except ClientError as e:
334
+ error_msg = f"S3 list failed: {e.response['Error']['Message']}"
335
+ logger.error(error_msg)
336
+ raise Exception(error_msg)
337
+ except Exception as e:
338
+ logger.error(f"Error listing files from S3: {str(e)}")
339
+ raise
340
+
341
+ async def delete_file(self, user_email: str, file_key: str) -> bool:
342
+ """
343
+ Delete a file from S3 storage.
344
+
345
+ Args:
346
+ user_email: Email of the user deleting the file
347
+ file_key: S3 key of the file to delete
348
+
349
+ Returns:
350
+ True if deletion was successful
351
+ """
352
+ try:
353
+ # Verify user has access to this file
354
+ if not file_key.startswith(f"users/{user_email}/"):
355
+ logger.warning(f"Access denied for deletion: {sanitize_for_logging(user_email)} attempted to delete {sanitize_for_logging(file_key)}")
356
+ raise Exception("Access denied to delete file")
357
+
358
+ # Delete object from S3
359
+ self.s3_client.delete_object(
360
+ Bucket=self.bucket_name,
361
+ Key=file_key
362
+ )
363
+
364
+ logger.info(f"File deleted successfully: {sanitize_for_logging(file_key)} for user {sanitize_for_logging(user_email)}")
365
+ return True
366
+
367
+ except ClientError as e:
368
+ if e.response['Error']['Code'] == 'NoSuchKey':
369
+ logger.warning(f"File not found for deletion: {sanitize_for_logging(file_key)} for user {sanitize_for_logging(user_email)}")
370
+ return False
371
+ else:
372
+ error_msg = f"S3 delete failed: {e.response['Error']['Message']}"
373
+ logger.error(error_msg)
374
+ raise Exception(error_msg)
375
+ except Exception as e:
376
+ logger.error(f"Error deleting file from S3: {str(e)}")
377
+ raise
378
+
379
+ async def get_user_stats(self, user_email: str) -> Dict[str, Any]:
380
+ """
381
+ Get file statistics for a user.
382
+
383
+ Args:
384
+ user_email: Email of the user
385
+
386
+ Returns:
387
+ Dictionary containing file statistics
388
+ """
389
+ try:
390
+ # List all user files
391
+ files = await self.list_files(user_email, limit=1000)
392
+
393
+ total_size = 0
394
+ upload_count = 0
395
+ generated_count = 0
396
+
397
+ for file_data in files:
398
+ total_size += file_data['size']
399
+
400
+ if file_data.get('tags', {}).get('source') == 'tool':
401
+ generated_count += 1
402
+ else:
403
+ upload_count += 1
404
+
405
+ result = {
406
+ "total_files": len(files),
407
+ "total_size": total_size,
408
+ "upload_count": upload_count,
409
+ "generated_count": generated_count
410
+ }
411
+
412
+ logger.info(f"Got file stats for user {sanitize_for_logging(user_email)}: {result}")
413
+ return result
414
+
415
+ except Exception as e:
416
+ logger.error(f"Error getting user stats from S3: {str(e)}")
417
+ raise
@@ -0,0 +1,19 @@
1
+ """LLM module for the chat backend.
2
+
3
+ This module provides:
4
+ - LLM calling interface for various interaction modes
5
+ - Response models and data structures
6
+ - CLI tools for testing LLM interactions
7
+ """
8
+
9
+ from .litellm_caller import LiteLLMCaller
10
+ from .models import LLMResponse
11
+
12
+ # Create default instance
13
+ llm_caller = LiteLLMCaller()
14
+
15
+ __all__ = [
16
+ "LiteLLMCaller",
17
+ "LLMResponse",
18
+ "llm_caller",
19
+ ]