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,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
|
+
]
|