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.
Files changed (146) hide show
  1. mito_ai/__init__.py +49 -9
  2. mito_ai/_version.py +1 -1
  3. mito_ai/anthropic_client.py +142 -67
  4. mito_ai/{app_builder → app_deploy}/__init__.py +1 -1
  5. mito_ai/app_deploy/app_deploy_utils.py +44 -0
  6. mito_ai/app_deploy/handlers.py +345 -0
  7. mito_ai/{app_builder → app_deploy}/models.py +35 -22
  8. mito_ai/app_manager/__init__.py +4 -0
  9. mito_ai/app_manager/handlers.py +167 -0
  10. mito_ai/app_manager/models.py +71 -0
  11. mito_ai/app_manager/utils.py +24 -0
  12. mito_ai/auth/README.md +18 -0
  13. mito_ai/auth/__init__.py +6 -0
  14. mito_ai/auth/handlers.py +96 -0
  15. mito_ai/auth/urls.py +13 -0
  16. mito_ai/chat_history/handlers.py +63 -0
  17. mito_ai/chat_history/urls.py +32 -0
  18. mito_ai/completions/completion_handlers/agent_execution_handler.py +1 -1
  19. mito_ai/completions/completion_handlers/chat_completion_handler.py +4 -4
  20. mito_ai/completions/completion_handlers/utils.py +99 -37
  21. mito_ai/completions/handlers.py +57 -20
  22. mito_ai/completions/message_history.py +9 -1
  23. mito_ai/completions/models.py +31 -7
  24. mito_ai/completions/prompt_builders/agent_execution_prompt.py +21 -2
  25. mito_ai/completions/prompt_builders/agent_smart_debug_prompt.py +8 -0
  26. mito_ai/completions/prompt_builders/agent_system_message.py +115 -42
  27. mito_ai/completions/prompt_builders/chat_name_prompt.py +6 -6
  28. mito_ai/completions/prompt_builders/chat_prompt.py +18 -11
  29. mito_ai/completions/prompt_builders/chat_system_message.py +4 -0
  30. mito_ai/completions/prompt_builders/prompt_constants.py +23 -4
  31. mito_ai/completions/prompt_builders/utils.py +72 -10
  32. mito_ai/completions/providers.py +81 -47
  33. mito_ai/constants.py +25 -24
  34. mito_ai/file_uploads/__init__.py +3 -0
  35. mito_ai/file_uploads/handlers.py +248 -0
  36. mito_ai/file_uploads/urls.py +21 -0
  37. mito_ai/gemini_client.py +44 -48
  38. mito_ai/log/handlers.py +10 -3
  39. mito_ai/log/urls.py +3 -3
  40. mito_ai/openai_client.py +30 -44
  41. mito_ai/path_utils.py +70 -0
  42. mito_ai/streamlit_conversion/agent_utils.py +37 -0
  43. mito_ai/streamlit_conversion/prompts/prompt_constants.py +172 -0
  44. mito_ai/streamlit_conversion/prompts/prompt_utils.py +10 -0
  45. mito_ai/streamlit_conversion/prompts/streamlit_app_creation_prompt.py +46 -0
  46. mito_ai/streamlit_conversion/prompts/streamlit_error_correction_prompt.py +28 -0
  47. mito_ai/streamlit_conversion/prompts/streamlit_finish_todo_prompt.py +45 -0
  48. mito_ai/streamlit_conversion/prompts/streamlit_system_prompt.py +56 -0
  49. mito_ai/streamlit_conversion/prompts/update_existing_app_prompt.py +50 -0
  50. mito_ai/streamlit_conversion/search_replace_utils.py +94 -0
  51. mito_ai/streamlit_conversion/streamlit_agent_handler.py +144 -0
  52. mito_ai/streamlit_conversion/streamlit_utils.py +85 -0
  53. mito_ai/streamlit_conversion/validate_streamlit_app.py +105 -0
  54. mito_ai/streamlit_preview/__init__.py +6 -0
  55. mito_ai/streamlit_preview/handlers.py +111 -0
  56. mito_ai/streamlit_preview/manager.py +152 -0
  57. mito_ai/streamlit_preview/urls.py +22 -0
  58. mito_ai/streamlit_preview/utils.py +29 -0
  59. mito_ai/tests/chat_history/test_chat_history.py +211 -0
  60. mito_ai/tests/completions/completion_handlers_utils_test.py +190 -0
  61. mito_ai/tests/deploy_app/test_app_deploy_utils.py +89 -0
  62. mito_ai/tests/file_uploads/__init__.py +2 -0
  63. mito_ai/tests/file_uploads/test_handlers.py +282 -0
  64. mito_ai/tests/message_history/test_generate_short_chat_name.py +0 -4
  65. mito_ai/tests/message_history/test_message_history_utils.py +103 -23
  66. mito_ai/tests/open_ai_utils_test.py +18 -22
  67. mito_ai/tests/providers/test_anthropic_client.py +447 -0
  68. mito_ai/tests/providers/test_azure.py +2 -6
  69. mito_ai/tests/providers/test_capabilities.py +120 -0
  70. mito_ai/tests/{test_gemini_client.py → providers/test_gemini_client.py} +40 -36
  71. mito_ai/tests/providers/test_mito_server_utils.py +448 -0
  72. mito_ai/tests/providers/test_model_resolution.py +130 -0
  73. mito_ai/tests/providers/test_openai_client.py +57 -0
  74. mito_ai/tests/providers/test_provider_completion_exception.py +66 -0
  75. mito_ai/tests/providers/test_provider_limits.py +42 -0
  76. mito_ai/tests/providers/test_providers.py +382 -0
  77. mito_ai/tests/providers/test_retry_logic.py +389 -0
  78. mito_ai/tests/providers/test_stream_mito_server_utils.py +140 -0
  79. mito_ai/tests/providers/utils.py +85 -0
  80. mito_ai/tests/streamlit_conversion/__init__.py +3 -0
  81. mito_ai/tests/streamlit_conversion/test_apply_search_replace.py +240 -0
  82. mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +246 -0
  83. mito_ai/tests/streamlit_conversion/test_streamlit_utils.py +193 -0
  84. mito_ai/tests/streamlit_conversion/test_validate_streamlit_app.py +112 -0
  85. mito_ai/tests/streamlit_preview/test_streamlit_preview_handler.py +118 -0
  86. mito_ai/tests/streamlit_preview/test_streamlit_preview_manager.py +292 -0
  87. mito_ai/tests/test_constants.py +31 -3
  88. mito_ai/tests/test_telemetry.py +12 -0
  89. mito_ai/tests/user/__init__.py +2 -0
  90. mito_ai/tests/user/test_user.py +120 -0
  91. mito_ai/tests/utils/test_anthropic_utils.py +6 -6
  92. mito_ai/user/handlers.py +45 -0
  93. mito_ai/user/urls.py +21 -0
  94. mito_ai/utils/anthropic_utils.py +55 -121
  95. mito_ai/utils/create.py +17 -1
  96. mito_ai/utils/error_classes.py +42 -0
  97. mito_ai/utils/gemini_utils.py +39 -94
  98. mito_ai/utils/message_history_utils.py +7 -4
  99. mito_ai/utils/mito_server_utils.py +242 -0
  100. mito_ai/utils/open_ai_utils.py +38 -155
  101. mito_ai/utils/provider_utils.py +49 -0
  102. mito_ai/utils/server_limits.py +1 -1
  103. mito_ai/utils/telemetry_utils.py +137 -5
  104. {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +102 -100
  105. {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/package.json +4 -2
  106. {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
  107. {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
  108. 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
  109. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.8f1845da6bf2b128c049.js.map +1 -0
  110. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js +198 -0
  111. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js.map +1 -0
  112. 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
  113. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.8b24b5b3b93f95205b56.js.map +1 -0
  114. 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
  115. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js.map +1 -0
  116. 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
  117. 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
  118. 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
  119. 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
  120. 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
  121. 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
  122. 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
  123. 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
  124. 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
  125. 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
  126. 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
  127. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js.map +1 -0
  128. {mito_ai-0.1.33.dist-info → mito_ai-0.1.49.dist-info}/METADATA +5 -2
  129. mito_ai-0.1.49.dist-info/RECORD +205 -0
  130. mito_ai/app_builder/handlers.py +0 -218
  131. mito_ai/tests/providers_test.py +0 -438
  132. mito_ai/tests/test_anthropic_client.py +0 -270
  133. mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.281f4b9af60d620c6fb1.js.map +0 -1
  134. mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.4f1d00fd0c58fcc05d8d.js.map +0 -1
  135. mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.06083e515de4862df010.js.map +0 -1
  136. mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_html2canvas_dist_html2canvas_js.ea47e8c8c906197f8d19.js +0 -7842
  137. 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
  138. mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.9795f79265ddb416864b.js.map +0 -1
  139. mito_ai-0.1.33.dist-info/RECORD +0 -134
  140. {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
  141. {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
  142. {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
  143. {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
  144. {mito_ai-0.1.33.dist-info → mito_ai-0.1.49.dist-info}/WHEEL +0 -0
  145. {mito_ai-0.1.33.dist-info → mito_ai-0.1.49.dist-info}/entry_points.txt +0 -0
  146. {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
- from enum import Enum
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 AppBuilderError:
16
- """Error information for app builder operations."""
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
- title: str
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) -> "AppBuilderError":
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
- title=str(e),
44
- traceback=getattr(e, "__traceback__", None) and str(e.__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(AppBuilderError):
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 BuildAppRequest:
59
- """Request to build an app."""
62
+ class DeployAppRequest:
63
+ """Request to deploy an app."""
60
64
 
61
65
  # Request type.
62
- type: Literal["build-app"]
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
- path: str
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 BuildAppReply:
73
- """Reply to a build app request."""
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[AppBuilderError] = None
95
+ error: Optional[AppDeployError] = None
83
96
 
84
97
  # Type of reply.
85
- type: Literal["build-app"] = "build-app"
98
+ type: Literal["deploy_app"] = "deploy_app"
@@ -0,0 +1,4 @@
1
+ # Copyright (c) Saga Inc.
2
+ # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
+
4
+ """App manager module for Mito AI."""
@@ -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)