mito-ai 0.1.33__py3-none-any.whl → 0.1.49__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.
- mito_ai/__init__.py +49 -9
- mito_ai/_version.py +1 -1
- mito_ai/anthropic_client.py +142 -67
- mito_ai/{app_builder → app_deploy}/__init__.py +1 -1
- mito_ai/app_deploy/app_deploy_utils.py +44 -0
- mito_ai/app_deploy/handlers.py +345 -0
- mito_ai/{app_builder → app_deploy}/models.py +35 -22
- mito_ai/app_manager/__init__.py +4 -0
- mito_ai/app_manager/handlers.py +167 -0
- mito_ai/app_manager/models.py +71 -0
- mito_ai/app_manager/utils.py +24 -0
- mito_ai/auth/README.md +18 -0
- mito_ai/auth/__init__.py +6 -0
- mito_ai/auth/handlers.py +96 -0
- mito_ai/auth/urls.py +13 -0
- mito_ai/chat_history/handlers.py +63 -0
- mito_ai/chat_history/urls.py +32 -0
- mito_ai/completions/completion_handlers/agent_execution_handler.py +1 -1
- mito_ai/completions/completion_handlers/chat_completion_handler.py +4 -4
- mito_ai/completions/completion_handlers/utils.py +99 -37
- mito_ai/completions/handlers.py +57 -20
- mito_ai/completions/message_history.py +9 -1
- mito_ai/completions/models.py +31 -7
- mito_ai/completions/prompt_builders/agent_execution_prompt.py +21 -2
- mito_ai/completions/prompt_builders/agent_smart_debug_prompt.py +8 -0
- mito_ai/completions/prompt_builders/agent_system_message.py +115 -42
- mito_ai/completions/prompt_builders/chat_name_prompt.py +6 -6
- mito_ai/completions/prompt_builders/chat_prompt.py +18 -11
- mito_ai/completions/prompt_builders/chat_system_message.py +4 -0
- mito_ai/completions/prompt_builders/prompt_constants.py +23 -4
- mito_ai/completions/prompt_builders/utils.py +72 -10
- mito_ai/completions/providers.py +81 -47
- mito_ai/constants.py +25 -24
- mito_ai/file_uploads/__init__.py +3 -0
- mito_ai/file_uploads/handlers.py +248 -0
- mito_ai/file_uploads/urls.py +21 -0
- mito_ai/gemini_client.py +44 -48
- mito_ai/log/handlers.py +10 -3
- mito_ai/log/urls.py +3 -3
- mito_ai/openai_client.py +30 -44
- mito_ai/path_utils.py +70 -0
- mito_ai/streamlit_conversion/agent_utils.py +37 -0
- mito_ai/streamlit_conversion/prompts/prompt_constants.py +172 -0
- mito_ai/streamlit_conversion/prompts/prompt_utils.py +10 -0
- mito_ai/streamlit_conversion/prompts/streamlit_app_creation_prompt.py +46 -0
- mito_ai/streamlit_conversion/prompts/streamlit_error_correction_prompt.py +28 -0
- mito_ai/streamlit_conversion/prompts/streamlit_finish_todo_prompt.py +45 -0
- mito_ai/streamlit_conversion/prompts/streamlit_system_prompt.py +56 -0
- mito_ai/streamlit_conversion/prompts/update_existing_app_prompt.py +50 -0
- mito_ai/streamlit_conversion/search_replace_utils.py +94 -0
- mito_ai/streamlit_conversion/streamlit_agent_handler.py +144 -0
- mito_ai/streamlit_conversion/streamlit_utils.py +85 -0
- mito_ai/streamlit_conversion/validate_streamlit_app.py +105 -0
- mito_ai/streamlit_preview/__init__.py +6 -0
- mito_ai/streamlit_preview/handlers.py +111 -0
- mito_ai/streamlit_preview/manager.py +152 -0
- mito_ai/streamlit_preview/urls.py +22 -0
- mito_ai/streamlit_preview/utils.py +29 -0
- mito_ai/tests/chat_history/test_chat_history.py +211 -0
- mito_ai/tests/completions/completion_handlers_utils_test.py +190 -0
- mito_ai/tests/deploy_app/test_app_deploy_utils.py +89 -0
- mito_ai/tests/file_uploads/__init__.py +2 -0
- mito_ai/tests/file_uploads/test_handlers.py +282 -0
- mito_ai/tests/message_history/test_generate_short_chat_name.py +0 -4
- mito_ai/tests/message_history/test_message_history_utils.py +103 -23
- mito_ai/tests/open_ai_utils_test.py +18 -22
- mito_ai/tests/providers/test_anthropic_client.py +447 -0
- mito_ai/tests/providers/test_azure.py +2 -6
- mito_ai/tests/providers/test_capabilities.py +120 -0
- mito_ai/tests/{test_gemini_client.py → providers/test_gemini_client.py} +40 -36
- mito_ai/tests/providers/test_mito_server_utils.py +448 -0
- mito_ai/tests/providers/test_model_resolution.py +130 -0
- mito_ai/tests/providers/test_openai_client.py +57 -0
- mito_ai/tests/providers/test_provider_completion_exception.py +66 -0
- mito_ai/tests/providers/test_provider_limits.py +42 -0
- mito_ai/tests/providers/test_providers.py +382 -0
- mito_ai/tests/providers/test_retry_logic.py +389 -0
- mito_ai/tests/providers/test_stream_mito_server_utils.py +140 -0
- mito_ai/tests/providers/utils.py +85 -0
- mito_ai/tests/streamlit_conversion/__init__.py +3 -0
- mito_ai/tests/streamlit_conversion/test_apply_search_replace.py +240 -0
- mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +246 -0
- mito_ai/tests/streamlit_conversion/test_streamlit_utils.py +193 -0
- mito_ai/tests/streamlit_conversion/test_validate_streamlit_app.py +112 -0
- mito_ai/tests/streamlit_preview/test_streamlit_preview_handler.py +118 -0
- mito_ai/tests/streamlit_preview/test_streamlit_preview_manager.py +292 -0
- mito_ai/tests/test_constants.py +31 -3
- mito_ai/tests/test_telemetry.py +12 -0
- mito_ai/tests/user/__init__.py +2 -0
- mito_ai/tests/user/test_user.py +120 -0
- mito_ai/tests/utils/test_anthropic_utils.py +6 -6
- mito_ai/user/handlers.py +45 -0
- mito_ai/user/urls.py +21 -0
- mito_ai/utils/anthropic_utils.py +55 -121
- mito_ai/utils/create.py +17 -1
- mito_ai/utils/error_classes.py +42 -0
- mito_ai/utils/gemini_utils.py +39 -94
- mito_ai/utils/message_history_utils.py +7 -4
- mito_ai/utils/mito_server_utils.py +242 -0
- mito_ai/utils/open_ai_utils.py +38 -155
- mito_ai/utils/provider_utils.py +49 -0
- mito_ai/utils/server_limits.py +1 -1
- mito_ai/utils/telemetry_utils.py +137 -5
- {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +102 -100
- {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/package.json +4 -2
- {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/package.json.orig +3 -1
- {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/toolbar-buttons.json +2 -2
- mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.281f4b9af60d620c6fb1.js → mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.8f1845da6bf2b128c049.js +15948 -8403
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.8f1845da6bf2b128c049.js.map +1 -0
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js +198 -0
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js.map +1 -0
- mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.4f1d00fd0c58fcc05d8d.js → mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.8b24b5b3b93f95205b56.js +58 -33
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.8b24b5b3b93f95205b56.js.map +1 -0
- mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.06083e515de4862df010.js → mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js +10 -2
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js.map +1 -0
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_apis_signOut_mjs-node_module-75790d.688c25857e7b81b1740f.js +533 -0
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_apis_signOut_mjs-node_module-75790d.688c25857e7b81b1740f.js.map +1 -0
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_tokenProvider_tokenProvider_-72f1c8.a917210f057fcfe224ad.js +6941 -0
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_tokenProvider_tokenProvider_-72f1c8.a917210f057fcfe224ad.js.map +1 -0
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js +1021 -0
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js.map +1 -0
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js +59698 -0
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js.map +1 -0
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_react-dom_client_js-node_modules_aws-amplify_ui-react_dist_styles_css.b43d4249e4d3dac9ad7b.js +7440 -0
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_react-dom_client_js-node_modules_aws-amplify_ui-react_dist_styles_css.b43d4249e4d3dac9ad7b.js.map +1 -0
- mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.9795f79265ddb416864b.js → mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js +2 -240
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js.map +1 -0
- {mito_ai-0.1.33.dist-info → mito_ai-0.1.49.dist-info}/METADATA +5 -2
- mito_ai-0.1.49.dist-info/RECORD +205 -0
- mito_ai/app_builder/handlers.py +0 -218
- mito_ai/tests/providers_test.py +0 -438
- mito_ai/tests/test_anthropic_client.py +0 -270
- mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.281f4b9af60d620c6fb1.js.map +0 -1
- mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.4f1d00fd0c58fcc05d8d.js.map +0 -1
- mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.06083e515de4862df010.js.map +0 -1
- mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_html2canvas_dist_html2canvas_js.ea47e8c8c906197f8d19.js +0 -7842
- mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_html2canvas_dist_html2canvas_js.ea47e8c8c906197f8d19.js.map +0 -1
- mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.9795f79265ddb416864b.js.map +0 -1
- mito_ai-0.1.33.dist-info/RECORD +0 -134
- {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
- {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
- {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js +0 -0
- {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js.map +0 -0
- {mito_ai-0.1.33.dist-info → mito_ai-0.1.49.dist-info}/WHEEL +0 -0
- {mito_ai-0.1.33.dist-info → mito_ai-0.1.49.dist-info}/entry_points.txt +0 -0
- {mito_ai-0.1.33.dist-info → mito_ai-0.1.49.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
# Copyright (c) Saga Inc.
|
|
2
|
+
# Distributed under the terms of the GNU Affero General Public License v3.0 License.
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import time
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Any, Union, List, Optional
|
|
8
|
+
import tempfile
|
|
9
|
+
from mito_ai.path_utils import AbsoluteNotebookDirPath, AppFileName, does_app_path_exist, get_absolute_app_path, get_absolute_notebook_dir_path, get_absolute_notebook_path, get_app_file_name
|
|
10
|
+
from mito_ai.utils.create import initialize_user
|
|
11
|
+
from mito_ai.utils.error_classes import StreamlitDeploymentError
|
|
12
|
+
from mito_ai.utils.version_utils import is_pro
|
|
13
|
+
from mito_ai.utils.websocket_base import BaseWebSocketHandler
|
|
14
|
+
from mito_ai.app_deploy.app_deploy_utils import add_files_to_zip
|
|
15
|
+
from mito_ai.app_deploy.models import (
|
|
16
|
+
DeployAppReply,
|
|
17
|
+
AppDeployError,
|
|
18
|
+
DeployAppRequest,
|
|
19
|
+
ErrorMessage,
|
|
20
|
+
)
|
|
21
|
+
from mito_ai.completions.models import MessageType
|
|
22
|
+
from mito_ai.logger import get_logger
|
|
23
|
+
from mito_ai.constants import ACTIVE_STREAMLIT_BASE_URL
|
|
24
|
+
from mito_ai.utils.telemetry_utils import log_streamlit_app_deployment_failure
|
|
25
|
+
import requests
|
|
26
|
+
import traceback
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AppDeployHandler(BaseWebSocketHandler):
|
|
30
|
+
"""Handler for app deploy requests."""
|
|
31
|
+
|
|
32
|
+
def initialize(self) -> None:
|
|
33
|
+
"""Initialize the WebSocket handler."""
|
|
34
|
+
super().initialize()
|
|
35
|
+
self.log.debug("Initializing app builder websocket connection %s", self.request.path)
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def log(self) -> logging.Logger:
|
|
39
|
+
"""Use Mito AI logger."""
|
|
40
|
+
return get_logger()
|
|
41
|
+
|
|
42
|
+
async def get(self, *args: Any, **kwargs: Any) -> None:
|
|
43
|
+
"""Get an event to open a socket or check service availability."""
|
|
44
|
+
# Check if this is just a service availability check
|
|
45
|
+
if self.get_query_argument('check_availability', None) == 'true':
|
|
46
|
+
self.set_status(200)
|
|
47
|
+
self.finish()
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
await super().pre_get() # Authenticate and authorize
|
|
51
|
+
initialize_user() # Initialize user directory structure
|
|
52
|
+
|
|
53
|
+
reply = super().get(*args, **kwargs)
|
|
54
|
+
if reply is not None:
|
|
55
|
+
await reply
|
|
56
|
+
|
|
57
|
+
async def on_message(self, message: Union[str, bytes]) -> None:
|
|
58
|
+
"""Handle incoming messages on the WebSocket.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
message: The message received on the WebSocket.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
start = time.time()
|
|
65
|
+
|
|
66
|
+
# Convert bytes to string if needed
|
|
67
|
+
if isinstance(message, bytes):
|
|
68
|
+
message = message.decode('utf-8')
|
|
69
|
+
|
|
70
|
+
self.log.debug("App builder message received: %s", message)
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
parsed_message = self.parse_message(message)
|
|
74
|
+
message_type = parsed_message.get('type')
|
|
75
|
+
|
|
76
|
+
if message_type == MessageType.DEPLOY_APP.value:
|
|
77
|
+
# Handle build app request
|
|
78
|
+
deploy_app_request = DeployAppRequest(**parsed_message)
|
|
79
|
+
response = await self._handle_deploy_app(deploy_app_request)
|
|
80
|
+
self.reply(response)
|
|
81
|
+
else:
|
|
82
|
+
self.log.error(f"Unknown message type: {message_type}")
|
|
83
|
+
error = AppDeployError(
|
|
84
|
+
error_type="InvalidRequest",
|
|
85
|
+
message=f"Unknown message type: {message_type}",
|
|
86
|
+
error_code=400
|
|
87
|
+
)
|
|
88
|
+
raise StreamlitDeploymentError(error)
|
|
89
|
+
|
|
90
|
+
except StreamlitDeploymentError as e:
|
|
91
|
+
self.log.error("Invalid app builder request", exc_info=e)
|
|
92
|
+
log_streamlit_app_deployment_failure('mito_server_key', MessageType.DEPLOY_APP, e.error.__dict__)
|
|
93
|
+
self.reply(
|
|
94
|
+
DeployAppReply(
|
|
95
|
+
parent_id=e.message_id,
|
|
96
|
+
url="",
|
|
97
|
+
error=ErrorMessage(**e.error.__dict__)
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
except Exception as e:
|
|
101
|
+
self.log.error("Error handling app builder message", exc_info=e)
|
|
102
|
+
error = AppDeployError.from_exception(
|
|
103
|
+
e,
|
|
104
|
+
hint="An error occurred while building the app. Please check the logs for details."
|
|
105
|
+
)
|
|
106
|
+
log_streamlit_app_deployment_failure('mito_server_key', MessageType.DEPLOY_APP, error.__dict__)
|
|
107
|
+
self.reply(ErrorMessage(**error.__dict__))
|
|
108
|
+
|
|
109
|
+
latency_ms = round((time.time() - start) * 1000)
|
|
110
|
+
self.log.info(f"App builder handler processed in {latency_ms} ms.")
|
|
111
|
+
|
|
112
|
+
async def _handle_deploy_app(self, message: DeployAppRequest) -> DeployAppReply:
|
|
113
|
+
"""Handle a build app request.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
message: The parsed message.
|
|
117
|
+
"""
|
|
118
|
+
message_id = message.message_id
|
|
119
|
+
notebook_path = message.notebook_path
|
|
120
|
+
notebook_id = message.notebook_id
|
|
121
|
+
jwt_token = message.jwt_token
|
|
122
|
+
files_to_upload = message.selected_files
|
|
123
|
+
|
|
124
|
+
# Validate parameters
|
|
125
|
+
missing_required_parameters = []
|
|
126
|
+
if not message_id:
|
|
127
|
+
missing_required_parameters.append('message_id')
|
|
128
|
+
if not notebook_id:
|
|
129
|
+
missing_required_parameters.append('notebook_id')
|
|
130
|
+
if not notebook_path:
|
|
131
|
+
missing_required_parameters.append('notebook_path')
|
|
132
|
+
|
|
133
|
+
if len(missing_required_parameters) > 0:
|
|
134
|
+
error_message = f"Missing required request parameters: {', '.join(missing_required_parameters)}"
|
|
135
|
+
self.log.error(error_message)
|
|
136
|
+
error = AppDeployError(
|
|
137
|
+
error_type="BadRequest",
|
|
138
|
+
message=error_message,
|
|
139
|
+
error_code=400,
|
|
140
|
+
message_id=message_id
|
|
141
|
+
)
|
|
142
|
+
raise StreamlitDeploymentError(error)
|
|
143
|
+
|
|
144
|
+
# Validate JWT token if provided
|
|
145
|
+
token_preview = jwt_token[:20] if jwt_token else "No token provided"
|
|
146
|
+
self.log.info(f"Validating JWT token: {token_preview}...")
|
|
147
|
+
is_valid = self._validate_jwt_token(jwt_token) if jwt_token else False
|
|
148
|
+
if not is_valid or not jwt_token:
|
|
149
|
+
self.log.error("JWT token validation failed")
|
|
150
|
+
error = AppDeployError(
|
|
151
|
+
error_type="Unauthorized",
|
|
152
|
+
message="Invalid authentication token",
|
|
153
|
+
hint="Please sign in again to deploy your app.",
|
|
154
|
+
error_code=401,
|
|
155
|
+
message_id=message_id
|
|
156
|
+
)
|
|
157
|
+
raise StreamlitDeploymentError(error)
|
|
158
|
+
else:
|
|
159
|
+
self.log.info("JWT token validation successful")
|
|
160
|
+
|
|
161
|
+
notebook_path = str(notebook_path) if notebook_path else ""
|
|
162
|
+
absolute_notebook_path = get_absolute_notebook_path(notebook_path)
|
|
163
|
+
absolute_app_directory = get_absolute_notebook_dir_path(absolute_notebook_path)
|
|
164
|
+
app_file_name = get_app_file_name(notebook_id)
|
|
165
|
+
app_path = get_absolute_app_path(absolute_app_directory, app_file_name)
|
|
166
|
+
|
|
167
|
+
# Check if the app.py file exists
|
|
168
|
+
app_path_exists = does_app_path_exist(app_path)
|
|
169
|
+
if not app_path_exists:
|
|
170
|
+
error = AppDeployError(
|
|
171
|
+
error_type="AppNotFound",
|
|
172
|
+
message="App not found",
|
|
173
|
+
hint=f"Please make sure the {app_file_name} file exists in the same directory as the notebook.",
|
|
174
|
+
error_code=400,
|
|
175
|
+
message_id=message_id
|
|
176
|
+
)
|
|
177
|
+
raise StreamlitDeploymentError(error)
|
|
178
|
+
|
|
179
|
+
# Finally, deploy the app
|
|
180
|
+
deploy_url = await self._deploy_app(
|
|
181
|
+
absolute_app_directory,
|
|
182
|
+
app_file_name,
|
|
183
|
+
files_to_upload,
|
|
184
|
+
message_id,
|
|
185
|
+
jwt_token
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Send the response
|
|
189
|
+
return DeployAppReply(
|
|
190
|
+
parent_id=message_id,
|
|
191
|
+
url=deploy_url if deploy_url else ""
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _validate_jwt_token(self, token: str) -> bool:
|
|
196
|
+
"""Basic JWT token validation logic.
|
|
197
|
+
|
|
198
|
+
In a production environment, you would:
|
|
199
|
+
1. Decode the JWT token
|
|
200
|
+
2. Verify the signature using AWS Cognito public keys
|
|
201
|
+
3. Check the expiration time
|
|
202
|
+
4. Validate the issuer and audience claims
|
|
203
|
+
|
|
204
|
+
For now, we'll do a basic check that the token exists and has a reasonable format.
|
|
205
|
+
"""
|
|
206
|
+
try:
|
|
207
|
+
# Basic JWT format validation (header.payload.signature)
|
|
208
|
+
if not token or '.' not in token:
|
|
209
|
+
self.log.error("Token is empty or missing dots")
|
|
210
|
+
return False
|
|
211
|
+
|
|
212
|
+
parts = token.split('.')
|
|
213
|
+
if len(parts) != 3:
|
|
214
|
+
self.log.error("Token does not have 3 parts")
|
|
215
|
+
return False
|
|
216
|
+
|
|
217
|
+
# Check for placeholder token
|
|
218
|
+
if token == 'placeholder-jwt-token':
|
|
219
|
+
self.log.error("Placeholder token detected")
|
|
220
|
+
return False
|
|
221
|
+
|
|
222
|
+
return True
|
|
223
|
+
|
|
224
|
+
except Exception as e:
|
|
225
|
+
self.log.error(f"Error validating JWT token: {e}")
|
|
226
|
+
return False
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
async def _deploy_app(
|
|
230
|
+
self,
|
|
231
|
+
absolute_notebook_dir_path: AbsoluteNotebookDirPath,
|
|
232
|
+
app_file_name: AppFileName,
|
|
233
|
+
files_to_upload:List[str],
|
|
234
|
+
message_id: str,
|
|
235
|
+
jwt_token: str = ''
|
|
236
|
+
) -> Optional[str]:
|
|
237
|
+
|
|
238
|
+
"""Deploy the app using pre-signed URLs.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
app_path: Path to the app file.
|
|
242
|
+
files_to_upload: Files the user selected to upload for the app to run
|
|
243
|
+
jwt_token: JWT token for authentication (optional)
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
The URL of the deployed app.
|
|
247
|
+
"""
|
|
248
|
+
# Get app name from the path without the file type ending
|
|
249
|
+
# ie: if the file is my-app.py, this variable is just my-app
|
|
250
|
+
# We use it in the app url
|
|
251
|
+
app_file_name_no_file_extension_ending = app_file_name.split('.')[0]
|
|
252
|
+
self.log.info(f"Deploying app: {app_file_name} from path: {absolute_notebook_dir_path}")
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
# Step 1: Get pre-signed URL from API
|
|
256
|
+
self.log.info("Getting pre-signed upload URL...")
|
|
257
|
+
|
|
258
|
+
# Prepare headers with JWT token if provided
|
|
259
|
+
headers = {}
|
|
260
|
+
if jwt_token and jwt_token != 'placeholder-jwt-token':
|
|
261
|
+
headers['Authorization'] = f'Bearer {jwt_token}'
|
|
262
|
+
else:
|
|
263
|
+
self.log.warning("No JWT token provided for API request")
|
|
264
|
+
|
|
265
|
+
headers["Subscription-Tier"] = 'Pro' if is_pro() else 'Standard'
|
|
266
|
+
|
|
267
|
+
url_response = requests.get(f"{ACTIVE_STREAMLIT_BASE_URL}/get-upload-url?app_name={app_file_name_no_file_extension_ending}", headers=headers)
|
|
268
|
+
url_response.raise_for_status()
|
|
269
|
+
|
|
270
|
+
url_data = url_response.json()
|
|
271
|
+
presigned_url = url_data['upload_url']
|
|
272
|
+
expected_app_url = url_data['expected_app_url']
|
|
273
|
+
|
|
274
|
+
self.log.info(f"Received pre-signed URL. App will be available at: {expected_app_url}")
|
|
275
|
+
|
|
276
|
+
# Step 2: Create a zip file of the app.
|
|
277
|
+
temp_zip_path = None
|
|
278
|
+
try:
|
|
279
|
+
# Create temp file
|
|
280
|
+
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as temp_zip:
|
|
281
|
+
temp_zip_path = temp_zip.name
|
|
282
|
+
|
|
283
|
+
self.log.info("Zipping application files...")
|
|
284
|
+
add_files_to_zip(temp_zip_path, absolute_notebook_dir_path, files_to_upload, app_file_name, self.log)
|
|
285
|
+
|
|
286
|
+
upload_response = await self._upload_app_to_s3(temp_zip_path, presigned_url)
|
|
287
|
+
except Exception as e:
|
|
288
|
+
self.log.error(f"Error zipping app: {e}")
|
|
289
|
+
error = AppDeployError(
|
|
290
|
+
error_type="ZippingError",
|
|
291
|
+
message=f"Error zipping app: {e}",
|
|
292
|
+
traceback=traceback.format_exc(),
|
|
293
|
+
error_code=500,
|
|
294
|
+
message_id=message_id
|
|
295
|
+
)
|
|
296
|
+
raise StreamlitDeploymentError(error)
|
|
297
|
+
finally:
|
|
298
|
+
# Clean up
|
|
299
|
+
if temp_zip_path is not None:
|
|
300
|
+
os.remove(temp_zip_path)
|
|
301
|
+
|
|
302
|
+
self.log.info(f"Upload successful! Status code: {upload_response.status_code}")
|
|
303
|
+
|
|
304
|
+
self.log.info(f"Deployment initiated. App will be available at: {expected_app_url}")
|
|
305
|
+
return str(expected_app_url)
|
|
306
|
+
|
|
307
|
+
except requests.exceptions.RequestException as e:
|
|
308
|
+
self.log.error(f"Error during API request: {e}")
|
|
309
|
+
if hasattr(e, 'response') and e.response is not None:
|
|
310
|
+
error_detail = e.response.json()
|
|
311
|
+
error_message = error_detail.get('error', "")
|
|
312
|
+
self.log.error(f"Server error details: {error_detail}")
|
|
313
|
+
else:
|
|
314
|
+
error_message = str(e)
|
|
315
|
+
|
|
316
|
+
error = AppDeployError(
|
|
317
|
+
error_type="APIException",
|
|
318
|
+
message=str(error_message),
|
|
319
|
+
traceback=traceback.format_exc(),
|
|
320
|
+
error_code=500,
|
|
321
|
+
message_id=message_id
|
|
322
|
+
)
|
|
323
|
+
raise StreamlitDeploymentError(error)
|
|
324
|
+
except Exception as e:
|
|
325
|
+
self.log.error(f"Error during deployment: {str(e)}")
|
|
326
|
+
error = AppDeployError(
|
|
327
|
+
error_type="DeploymentException",
|
|
328
|
+
message=str(e),
|
|
329
|
+
traceback=traceback.format_exc(),
|
|
330
|
+
error_code=500,
|
|
331
|
+
message_id=message_id
|
|
332
|
+
)
|
|
333
|
+
raise StreamlitDeploymentError(error)
|
|
334
|
+
|
|
335
|
+
async def _upload_app_to_s3(self, app_path: str, presigned_url: str) -> requests.Response:
|
|
336
|
+
"""Upload the app to S3 using the presigned URL."""
|
|
337
|
+
with open(app_path, 'rb') as file_data:
|
|
338
|
+
upload_response = requests.put(
|
|
339
|
+
presigned_url,
|
|
340
|
+
data=file_data,
|
|
341
|
+
headers={'Content-Type': 'application/zip'}
|
|
342
|
+
)
|
|
343
|
+
upload_response.raise_for_status()
|
|
344
|
+
|
|
345
|
+
return upload_response
|
|
@@ -2,24 +2,25 @@
|
|
|
2
2
|
# Distributed under the terms of the GNU Affero General Public License v3.0 License.
|
|
3
3
|
|
|
4
4
|
from dataclasses import dataclass
|
|
5
|
-
|
|
6
|
-
from typing import Literal, Optional
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class MessageType(str, Enum):
|
|
10
|
-
"""Types of app builder messages."""
|
|
11
|
-
BUILD_APP = "build-app"
|
|
5
|
+
import traceback
|
|
6
|
+
from typing import Literal, Optional, List
|
|
12
7
|
|
|
13
8
|
|
|
14
9
|
@dataclass(frozen=True)
|
|
15
|
-
class
|
|
16
|
-
"""Error information for app
|
|
10
|
+
class AppDeployError:
|
|
11
|
+
"""Error information for app deploy operations."""
|
|
17
12
|
|
|
18
13
|
# Error type.
|
|
19
14
|
error_type: str
|
|
20
15
|
|
|
21
16
|
# Error title.
|
|
22
|
-
|
|
17
|
+
message: str
|
|
18
|
+
|
|
19
|
+
#ID of parent to resolve response
|
|
20
|
+
message_id: Optional[str] = "InvalidMessageID"
|
|
21
|
+
|
|
22
|
+
# Error code
|
|
23
|
+
error_code: Optional[int] = 500
|
|
23
24
|
|
|
24
25
|
# Error traceback.
|
|
25
26
|
traceback: Optional[str] = None
|
|
@@ -28,26 +29,29 @@ class AppBuilderError:
|
|
|
28
29
|
hint: Optional[str] = None
|
|
29
30
|
|
|
30
31
|
@classmethod
|
|
31
|
-
def from_exception(cls, e: Exception, hint: Optional[str] = None) -> "
|
|
32
|
+
def from_exception(cls, e: Exception, hint: Optional[str] = None, error_code: Optional[int] = 500) -> "AppDeployError":
|
|
32
33
|
"""Create an error from an exception.
|
|
33
34
|
|
|
34
35
|
Args:
|
|
35
36
|
e: The exception.
|
|
36
37
|
hint: Optional hint to fix the error.
|
|
38
|
+
error_code: Optional error code which defaults to 500
|
|
37
39
|
|
|
38
40
|
Returns:
|
|
39
41
|
The app builder error.
|
|
40
42
|
"""
|
|
43
|
+
tb_str = "".join(traceback.format_exception(type(e), e, e.__traceback__))
|
|
41
44
|
return cls(
|
|
42
45
|
error_type=type(e).__name__,
|
|
43
|
-
|
|
44
|
-
traceback=
|
|
46
|
+
message=str(e),
|
|
47
|
+
traceback=tb_str,
|
|
45
48
|
hint=hint,
|
|
49
|
+
error_code=error_code
|
|
46
50
|
)
|
|
47
51
|
|
|
48
52
|
|
|
49
53
|
@dataclass(frozen=True)
|
|
50
|
-
class ErrorMessage(
|
|
54
|
+
class ErrorMessage(AppDeployError):
|
|
51
55
|
"""Error message."""
|
|
52
56
|
|
|
53
57
|
# Message type.
|
|
@@ -55,22 +59,31 @@ class ErrorMessage(AppBuilderError):
|
|
|
55
59
|
|
|
56
60
|
|
|
57
61
|
@dataclass(frozen=True)
|
|
58
|
-
class
|
|
59
|
-
"""Request to
|
|
62
|
+
class DeployAppRequest:
|
|
63
|
+
"""Request to deploy an app."""
|
|
60
64
|
|
|
61
65
|
# Request type.
|
|
62
|
-
type: Literal["
|
|
66
|
+
type: Literal["deploy_app"]
|
|
63
67
|
|
|
64
68
|
# Message ID.
|
|
65
69
|
message_id: str
|
|
66
70
|
|
|
67
71
|
# Path to the app file.
|
|
68
|
-
|
|
72
|
+
notebook_path: str
|
|
73
|
+
|
|
74
|
+
# Notebook ID
|
|
75
|
+
notebook_id: str
|
|
76
|
+
|
|
77
|
+
# Files to be uploaded for the app to run
|
|
78
|
+
selected_files: List[str]
|
|
79
|
+
|
|
80
|
+
# JWT token for authorization.
|
|
81
|
+
jwt_token: Optional[str] = None
|
|
69
82
|
|
|
70
83
|
|
|
71
84
|
@dataclass(frozen=True)
|
|
72
|
-
class
|
|
73
|
-
"""Reply to a
|
|
85
|
+
class DeployAppReply:
|
|
86
|
+
"""Reply to a deplpy app request."""
|
|
74
87
|
|
|
75
88
|
# ID of the request message this is replying to.
|
|
76
89
|
parent_id: str
|
|
@@ -79,7 +92,7 @@ class BuildAppReply:
|
|
|
79
92
|
url: str
|
|
80
93
|
|
|
81
94
|
# Optional error information.
|
|
82
|
-
error: Optional[
|
|
95
|
+
error: Optional[AppDeployError] = None
|
|
83
96
|
|
|
84
97
|
# Type of reply.
|
|
85
|
-
type: Literal["
|
|
98
|
+
type: Literal["deploy_app"] = "deploy_app"
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Copyright (c) Saga Inc.
|
|
2
|
+
# Distributed under the terms of the GNU Affero General Public License v3.0 License.
|
|
3
|
+
|
|
4
|
+
# app_manager/handlers.py
|
|
5
|
+
import os
|
|
6
|
+
import time
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Union
|
|
9
|
+
from mito_ai.utils.websocket_base import BaseWebSocketHandler
|
|
10
|
+
from mito_ai.app_manager.models import (
|
|
11
|
+
App,
|
|
12
|
+
AppManagerError,
|
|
13
|
+
ManageAppRequest,
|
|
14
|
+
ManageAppReply,
|
|
15
|
+
CheckAppStatusRequest,
|
|
16
|
+
CheckAppStatusReply,
|
|
17
|
+
ErrorMessage,
|
|
18
|
+
MessageType
|
|
19
|
+
)
|
|
20
|
+
from mito_ai.constants import ACTIVE_STREAMLIT_BASE_URL
|
|
21
|
+
from mito_ai.logger import get_logger
|
|
22
|
+
from mito_ai.app_manager.utils import convert_utc_to_local_time
|
|
23
|
+
import requests
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AppManagerHandler(BaseWebSocketHandler):
|
|
27
|
+
"""Handler for app management requests."""
|
|
28
|
+
|
|
29
|
+
def initialize(self) -> None:
|
|
30
|
+
"""Initialize the WebSocket handler."""
|
|
31
|
+
super().initialize()
|
|
32
|
+
self.log.debug("Initializing app manager websocket connection %s", self.request.path)
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def log(self) -> logging.Logger:
|
|
36
|
+
"""Use Mito AI logger."""
|
|
37
|
+
return get_logger()
|
|
38
|
+
|
|
39
|
+
async def on_message(self, message: Union[str, bytes]) -> None:
|
|
40
|
+
"""Handle incoming messages on the WebSocket."""
|
|
41
|
+
start = time.time()
|
|
42
|
+
|
|
43
|
+
# Convert bytes to string if needed
|
|
44
|
+
if isinstance(message, bytes):
|
|
45
|
+
message = message.decode('utf-8')
|
|
46
|
+
|
|
47
|
+
self.log.debug("App manager message received: %s", message)
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
# Ensure message is a string before parsing
|
|
51
|
+
if not isinstance(message, str):
|
|
52
|
+
raise ValueError("Message must be a string")
|
|
53
|
+
|
|
54
|
+
parsed_message = self.parse_message(message)
|
|
55
|
+
message_type = parsed_message.get('type')
|
|
56
|
+
message_id = parsed_message.get('message_id')
|
|
57
|
+
|
|
58
|
+
if message_type == MessageType.MANAGE_APP.value:
|
|
59
|
+
# Handle manage app request
|
|
60
|
+
manage_app_request = ManageAppRequest(**parsed_message)
|
|
61
|
+
await self._handle_manage_app(manage_app_request)
|
|
62
|
+
elif message_type == MessageType.CHECK_APP_STATUS.value:
|
|
63
|
+
# Handle check app status request
|
|
64
|
+
check_status_request = CheckAppStatusRequest(**parsed_message)
|
|
65
|
+
await self._handle_check_app_status(check_status_request)
|
|
66
|
+
else:
|
|
67
|
+
self.log.error(f"Unknown message type: {message_type}")
|
|
68
|
+
error_response = ErrorMessage(
|
|
69
|
+
error_type="InvalidRequest",
|
|
70
|
+
title=f"Unknown message type: {message_type}",
|
|
71
|
+
message_id=message_id
|
|
72
|
+
)
|
|
73
|
+
self.reply(error_response)
|
|
74
|
+
|
|
75
|
+
except ValueError as e:
|
|
76
|
+
self.log.error("Invalid app manager request", exc_info=e)
|
|
77
|
+
error_response = ErrorMessage(
|
|
78
|
+
error_type=type(e).__name__,
|
|
79
|
+
title=str(e),
|
|
80
|
+
message_id=parsed_message.get('message_id') if 'parsed_message' in locals() else None
|
|
81
|
+
)
|
|
82
|
+
self.reply(error_response)
|
|
83
|
+
except Exception as e:
|
|
84
|
+
self.log.error("Error handling app manager message", exc_info=e)
|
|
85
|
+
error_response = ErrorMessage(
|
|
86
|
+
error_type=type(e).__name__,
|
|
87
|
+
title=str(e),
|
|
88
|
+
message_id=parsed_message.get('message_id') if 'parsed_message' in locals() else None
|
|
89
|
+
)
|
|
90
|
+
self.reply(error_response)
|
|
91
|
+
|
|
92
|
+
latency_ms = round((time.time() - start) * 1000)
|
|
93
|
+
self.log.info(f"App manager handler processed in {latency_ms} ms.")
|
|
94
|
+
|
|
95
|
+
async def _handle_manage_app(self, request: ManageAppRequest) -> None:
|
|
96
|
+
"""Handle a manage app request with hardcoded data."""
|
|
97
|
+
try:
|
|
98
|
+
jwt_token = request.jwt_token
|
|
99
|
+
headers = {}
|
|
100
|
+
if jwt_token and jwt_token != 'placeholder-jwt-token':
|
|
101
|
+
headers['Authorization'] = f'Bearer {jwt_token}'
|
|
102
|
+
else:
|
|
103
|
+
self.log.warning("No JWT token provided for API request")
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
manage_apps_response = requests.get(f"{ACTIVE_STREAMLIT_BASE_URL}/manage-apps",
|
|
107
|
+
headers=headers)
|
|
108
|
+
manage_apps_response.raise_for_status()
|
|
109
|
+
|
|
110
|
+
apps_data = manage_apps_response.json()
|
|
111
|
+
|
|
112
|
+
for app in apps_data:
|
|
113
|
+
if 'last_deployed_at' in app:
|
|
114
|
+
app['last_deployed_at'] = convert_utc_to_local_time(app['last_deployed_at'])
|
|
115
|
+
|
|
116
|
+
# Create successful response
|
|
117
|
+
reply = ManageAppReply(
|
|
118
|
+
apps=apps_data,
|
|
119
|
+
message_id=request.message_id
|
|
120
|
+
)
|
|
121
|
+
self.reply(reply)
|
|
122
|
+
|
|
123
|
+
except Exception as e:
|
|
124
|
+
self.log.error(f"Error handling manage app request: {e}", exc_info=e)
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
error = AppManagerError.from_exception(e)
|
|
128
|
+
except Exception:
|
|
129
|
+
error = AppManagerError(
|
|
130
|
+
error_type=type(e).__name__,
|
|
131
|
+
title=str(e)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Return error response
|
|
135
|
+
error_reply = ManageAppReply(
|
|
136
|
+
apps=[],
|
|
137
|
+
error=error,
|
|
138
|
+
message_id=request.message_id
|
|
139
|
+
)
|
|
140
|
+
self.reply(error_reply)
|
|
141
|
+
|
|
142
|
+
async def _handle_check_app_status(self, request: CheckAppStatusRequest) -> None:
|
|
143
|
+
"""Handle a check app status request."""
|
|
144
|
+
self.log.info("In check app status")
|
|
145
|
+
try:
|
|
146
|
+
# Make a HEAD request to check if the app URL is accessible
|
|
147
|
+
response = requests.head(request.app_url, timeout=10, verify=False)
|
|
148
|
+
self.log.debug(f"Is app accessible: {response.status_code}")
|
|
149
|
+
is_accessible = response.status_code==200
|
|
150
|
+
|
|
151
|
+
# Create successful response
|
|
152
|
+
reply = CheckAppStatusReply(
|
|
153
|
+
is_accessible=is_accessible
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
self.reply(reply)
|
|
157
|
+
|
|
158
|
+
except Exception as e:
|
|
159
|
+
self.log.error(f"Error checking app status: {e}", exc_info=e)
|
|
160
|
+
error = AppManagerError.from_exception(e)
|
|
161
|
+
|
|
162
|
+
# Return error response
|
|
163
|
+
error_reply = CheckAppStatusReply(
|
|
164
|
+
is_accessible=False,
|
|
165
|
+
error=error
|
|
166
|
+
)
|
|
167
|
+
self.reply(error_reply)
|