mito-ai 0.1.46__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 (74) hide show
  1. mito_ai/_version.py +1 -1
  2. mito_ai/app_deploy/app_deploy_utils.py +28 -9
  3. mito_ai/app_deploy/handlers.py +123 -84
  4. mito_ai/app_deploy/models.py +19 -12
  5. mito_ai/completions/models.py +6 -1
  6. mito_ai/completions/prompt_builders/agent_execution_prompt.py +13 -1
  7. mito_ai/completions/prompt_builders/agent_system_message.py +63 -4
  8. mito_ai/completions/prompt_builders/chat_system_message.py +4 -0
  9. mito_ai/completions/prompt_builders/prompt_constants.py +1 -0
  10. mito_ai/completions/prompt_builders/utils.py +13 -0
  11. mito_ai/path_utils.py +70 -0
  12. mito_ai/streamlit_conversion/agent_utils.py +4 -201
  13. mito_ai/streamlit_conversion/prompts/prompt_constants.py +142 -152
  14. mito_ai/streamlit_conversion/prompts/streamlit_error_correction_prompt.py +3 -3
  15. mito_ai/streamlit_conversion/prompts/streamlit_finish_todo_prompt.py +2 -2
  16. mito_ai/streamlit_conversion/prompts/update_existing_app_prompt.py +2 -2
  17. mito_ai/streamlit_conversion/search_replace_utils.py +94 -0
  18. mito_ai/streamlit_conversion/streamlit_agent_handler.py +35 -46
  19. mito_ai/streamlit_conversion/streamlit_utils.py +13 -75
  20. mito_ai/streamlit_conversion/validate_streamlit_app.py +6 -21
  21. mito_ai/streamlit_preview/__init__.py +1 -2
  22. mito_ai/streamlit_preview/handlers.py +54 -85
  23. mito_ai/streamlit_preview/manager.py +11 -18
  24. mito_ai/streamlit_preview/utils.py +12 -28
  25. mito_ai/tests/deploy_app/test_app_deploy_utils.py +22 -4
  26. mito_ai/tests/message_history/test_message_history_utils.py +3 -0
  27. mito_ai/tests/streamlit_conversion/test_apply_search_replace.py +240 -0
  28. mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +40 -60
  29. mito_ai/tests/streamlit_conversion/test_streamlit_utils.py +26 -29
  30. mito_ai/tests/streamlit_conversion/test_validate_streamlit_app.py +25 -20
  31. mito_ai/tests/streamlit_preview/test_streamlit_preview_handler.py +87 -57
  32. mito_ai/tests/streamlit_preview/test_streamlit_preview_manager.py +27 -40
  33. mito_ai/user/handlers.py +15 -3
  34. mito_ai/utils/create.py +17 -1
  35. mito_ai/utils/error_classes.py +42 -0
  36. mito_ai/utils/message_history_utils.py +3 -1
  37. mito_ai/utils/telemetry_utils.py +78 -13
  38. {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +100 -100
  39. {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/package.json +2 -2
  40. {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/package.json.orig +1 -1
  41. mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.20f12766ecd3d430568e.js → mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.8f1845da6bf2b128c049.js +3571 -1442
  42. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.8f1845da6bf2b128c049.js.map +1 -0
  43. mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.54126ab6511271265443.js → mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.8b24b5b3b93f95205b56.js +24 -24
  44. mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.54126ab6511271265443.js.map → mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.8b24b5b3b93f95205b56.js.map +1 -1
  45. {mito_ai-0.1.46.dist-info → mito_ai-0.1.49.dist-info}/METADATA +1 -1
  46. {mito_ai-0.1.46.dist-info → mito_ai-0.1.49.dist-info}/RECORD +71 -69
  47. mito_ai/tests/streamlit_conversion/test_apply_patch_to_text.py +0 -368
  48. mito_ai/tests/streamlit_conversion/test_fix_diff_headers.py +0 -533
  49. mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.20f12766ecd3d430568e.js.map +0 -1
  50. /mito_ai/streamlit_conversion/{streamlit_system_prompt.py → prompts/streamlit_system_prompt.py} +0 -0
  51. {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
  52. {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/toolbar-buttons.json +0 -0
  53. {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js +0 -0
  54. {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js.map +0 -0
  55. {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
  56. {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js +0 -0
  57. {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js.map +0 -0
  58. {mito_ai-0.1.46.data → 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 +0 -0
  59. {mito_ai-0.1.46.data → 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 +0 -0
  60. {mito_ai-0.1.46.data → 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 +0 -0
  61. {mito_ai-0.1.46.data → 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 +0 -0
  62. {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js +0 -0
  63. {mito_ai-0.1.46.data → 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 +0 -0
  64. {mito_ai-0.1.46.data → 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 +0 -0
  65. {mito_ai-0.1.46.data → 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 +0 -0
  66. {mito_ai-0.1.46.data → 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 +0 -0
  67. {mito_ai-0.1.46.data → 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 +0 -0
  68. {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js +0 -0
  69. {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js.map +0 -0
  70. {mito_ai-0.1.46.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
  71. {mito_ai-0.1.46.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
  72. {mito_ai-0.1.46.dist-info → mito_ai-0.1.49.dist-info}/WHEEL +0 -0
  73. {mito_ai-0.1.46.dist-info → mito_ai-0.1.49.dist-info}/entry_points.txt +0 -0
  74. {mito_ai-0.1.46.dist-info → mito_ai-0.1.49.dist-info}/licenses/LICENSE +0 -0
mito_ai/_version.py CHANGED
@@ -1,4 +1,4 @@
1
1
  # This file is auto-generated by Hatchling. As such, do not:
2
2
  # - modify
3
3
  # - track in version control e.g. be sure to add to .gitignore
4
- __version__ = VERSION = '0.1.46'
4
+ __version__ = VERSION = '0.1.49'
@@ -6,20 +6,39 @@ import zipfile
6
6
  import logging
7
7
  from typing import List, Optional
8
8
 
9
- def add_files_to_zip(zip_path: str, base_path: str, files_to_add: List[str], logger: Optional[logging.Logger] = None) -> None:
9
+ from mito_ai.path_utils import AbsoluteNotebookDirPath
10
+
11
+ def add_files_to_zip(
12
+ zip_path: str,
13
+ notebook_dir_path: AbsoluteNotebookDirPath,
14
+ files_to_add: List[str],
15
+ app_file_name: str,
16
+ logger: Optional[logging.Logger] = None
17
+ ) -> None:
10
18
  """Create a zip file at zip_path and add the selected files/folders."""
11
19
  with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zipf:
12
- for rel_path in files_to_add:
13
- abs_path = os.path.join(base_path, rel_path)
20
+ for file_to_add_rel_path in files_to_add:
21
+
22
+ file_to_add_abs_path = os.path.join(notebook_dir_path, file_to_add_rel_path)
14
23
 
15
- if os.path.isfile(abs_path):
16
- zipf.write(abs_path, arcname=rel_path)
17
- elif os.path.isdir(abs_path):
18
- for root, _, files in os.walk(abs_path):
24
+ if os.path.isfile(file_to_add_abs_path):
25
+ basename = os.path.basename(file_to_add_abs_path)
26
+
27
+ if basename == app_file_name:
28
+ # For the actual app file, we want to write it just as app.py
29
+ # so our infra can always deploy using `streamlit run app.py`
30
+ # without having to account for different app names
31
+ zipf.write(file_to_add_abs_path, arcname='app.py')
32
+ else:
33
+ # otherwise we want to keep the name as is so all references
34
+ # to it from the app are correct
35
+ zipf.write(file_to_add_abs_path, arcname=file_to_add_rel_path)
36
+ elif os.path.isdir(file_to_add_abs_path):
37
+ for root, _, files in os.walk(file_to_add_abs_path):
19
38
  for file in files:
20
39
  file_abs = os.path.join(root, file)
21
- arcname = os.path.relpath(file_abs, base_path)
40
+ arcname = os.path.relpath(file_abs, notebook_dir_path)
22
41
  zipf.write(file_abs, arcname=arcname)
23
42
  else:
24
43
  if logger:
25
- logger.warning(f"Skipping missing file: {abs_path}")
44
+ logger.warning(f"Skipping missing file: {file_to_add_abs_path}")
@@ -4,10 +4,11 @@
4
4
  import os
5
5
  import time
6
6
  import logging
7
- from typing import Any, Union, List
7
+ from typing import Any, Union, List, Optional
8
8
  import tempfile
9
- from mito_ai.streamlit_conversion.streamlit_utils import get_app_path
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
10
  from mito_ai.utils.create import initialize_user
11
+ from mito_ai.utils.error_classes import StreamlitDeploymentError
11
12
  from mito_ai.utils.version_utils import is_pro
12
13
  from mito_ai.utils.websocket_base import BaseWebSocketHandler
13
14
  from mito_ai.app_deploy.app_deploy_utils import add_files_to_zip
@@ -16,11 +17,13 @@ from mito_ai.app_deploy.models import (
16
17
  AppDeployError,
17
18
  DeployAppRequest,
18
19
  ErrorMessage,
19
- MessageType
20
20
  )
21
+ from mito_ai.completions.models import MessageType
21
22
  from mito_ai.logger import get_logger
22
23
  from mito_ai.constants import ACTIVE_STREAMLIT_BASE_URL
24
+ from mito_ai.utils.telemetry_utils import log_streamlit_app_deployment_failure
23
25
  import requests
26
+ import traceback
24
27
 
25
28
 
26
29
  class AppDeployHandler(BaseWebSocketHandler):
@@ -67,41 +70,46 @@ class AppDeployHandler(BaseWebSocketHandler):
67
70
  self.log.debug("App builder message received: %s", message)
68
71
 
69
72
  try:
70
- # Ensure message is a string before parsing
71
- if not isinstance(message, str):
72
- raise ValueError("Message must be a string")
73
-
74
73
  parsed_message = self.parse_message(message)
75
74
  message_type = parsed_message.get('type')
76
75
 
77
76
  if message_type == MessageType.DEPLOY_APP.value:
78
77
  # Handle build app request
79
78
  deploy_app_request = DeployAppRequest(**parsed_message)
80
- await self._handle_deploy_app(deploy_app_request)
79
+ response = await self._handle_deploy_app(deploy_app_request)
80
+ self.reply(response)
81
81
  else:
82
82
  self.log.error(f"Unknown message type: {message_type}")
83
83
  error = AppDeployError(
84
84
  error_type="InvalidRequest",
85
- title=f"Unknown message type: {message_type}"
85
+ message=f"Unknown message type: {message_type}",
86
+ error_code=400
86
87
  )
87
- self.reply(ErrorMessage(**error.__dict__))
88
-
89
- except ValueError as e:
88
+ raise StreamlitDeploymentError(error)
89
+
90
+ except StreamlitDeploymentError as e:
90
91
  self.log.error("Invalid app builder request", exc_info=e)
91
- error = AppDeployError.from_exception(e)
92
- self.reply(ErrorMessage(**error.__dict__))
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
+ )
93
100
  except Exception as e:
94
101
  self.log.error("Error handling app builder message", exc_info=e)
95
102
  error = AppDeployError.from_exception(
96
103
  e,
97
104
  hint="An error occurred while building the app. Please check the logs for details."
98
105
  )
106
+ log_streamlit_app_deployment_failure('mito_server_key', MessageType.DEPLOY_APP, error.__dict__)
99
107
  self.reply(ErrorMessage(**error.__dict__))
100
108
 
101
109
  latency_ms = round((time.time() - start) * 1000)
102
110
  self.log.info(f"App builder handler processed in {latency_ms} ms.")
103
111
 
104
- async def _handle_deploy_app(self, message: DeployAppRequest) -> None:
112
+ async def _handle_deploy_app(self, message: DeployAppRequest) -> DeployAppReply:
105
113
  """Handle a build app request.
106
114
 
107
115
  Args:
@@ -109,24 +117,29 @@ class AppDeployHandler(BaseWebSocketHandler):
109
117
  """
110
118
  message_id = message.message_id
111
119
  notebook_path = message.notebook_path
120
+ notebook_id = message.notebook_id
112
121
  jwt_token = message.jwt_token
113
122
  files_to_upload = message.selected_files
114
123
 
124
+ # Validate parameters
125
+ missing_required_parameters = []
115
126
  if not message_id:
116
- self.log.error("Missing message_id in request")
117
- return
118
-
127
+ missing_required_parameters.append('message_id')
128
+ if not notebook_id:
129
+ missing_required_parameters.append('notebook_id')
119
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)
120
136
  error = AppDeployError(
121
- error_type="InvalidRequest",
122
- title="Missing 'notebook_path' parameter"
137
+ error_type="BadRequest",
138
+ message=error_message,
139
+ error_code=400,
140
+ message_id=message_id
123
141
  )
124
- self.reply(DeployAppReply(
125
- parent_id=message_id,
126
- url="",
127
- error=error
128
- ))
129
- return
142
+ raise StreamlitDeploymentError(error)
130
143
 
131
144
  # Validate JWT token if provided
132
145
  token_preview = jwt_token[:20] if jwt_token else "No token provided"
@@ -136,55 +149,49 @@ class AppDeployHandler(BaseWebSocketHandler):
136
149
  self.log.error("JWT token validation failed")
137
150
  error = AppDeployError(
138
151
  error_type="Unauthorized",
139
- title="Invalid authentication token",
140
- hint="Please sign in again to deploy your app."
152
+ message="Invalid authentication token",
153
+ hint="Please sign in again to deploy your app.",
154
+ error_code=401,
155
+ message_id=message_id
141
156
  )
142
- self.reply(DeployAppReply(
143
- parent_id=message_id,
144
- url="",
145
- error=error
146
- ))
147
- return
157
+ raise StreamlitDeploymentError(error)
148
158
  else:
149
159
  self.log.info("JWT token validation successful")
150
-
151
- try:
152
- notebook_path = str(notebook_path) if notebook_path else ""
153
160
 
154
- app_directory = os.path.dirname(notebook_path)
155
-
156
- # Check if the app.py file exists
157
- app_path = get_app_path(app_directory)
158
- if app_path is None:
159
- error = AppDeployError(
160
- error_type="AppNotFound",
161
- title="App not found",
162
- hint="Please make sure the app.py file exists in the same directory as the notebook."
163
- )
164
- self.reply(DeployAppReply(
165
- parent_id=message_id,
166
- url="",
167
- error=error
168
- ))
169
-
170
- # Finally, deploy the app
171
- deploy_url = await self._deploy_app(app_directory, files_to_upload, jwt_token)
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
+
172
194
 
173
- # Send the response
174
- self.reply(DeployAppReply(
175
- parent_id=message_id,
176
- url=deploy_url
177
- ))
178
-
179
- except Exception as e:
180
- self.log.error(f"Error building app: {e}", exc_info=e)
181
- error = AppDeployError.from_exception(e)
182
- self.reply(DeployAppReply(
183
- parent_id=message_id,
184
- url="",
185
- error=error
186
- ))
187
-
188
195
  def _validate_jwt_token(self, token: str) -> bool:
189
196
  """Basic JWT token validation logic.
190
197
 
@@ -219,7 +226,15 @@ class AppDeployHandler(BaseWebSocketHandler):
219
226
  return False
220
227
 
221
228
 
222
- async def _deploy_app(self, app_path: str, files_to_upload:List[str], jwt_token: str = '') -> str:
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
+
223
238
  """Deploy the app using pre-signed URLs.
224
239
 
225
240
  Args:
@@ -230,9 +245,11 @@ class AppDeployHandler(BaseWebSocketHandler):
230
245
  Returns:
231
246
  The URL of the deployed app.
232
247
  """
233
- # Get app name from the path
234
- app_name = os.path.basename(app_path).split('.')[0]
235
- self.log.info(f"Deploying app: {app_name} from path: {app_path}")
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}")
236
253
 
237
254
  try:
238
255
  # Step 1: Get pre-signed URL from API
@@ -247,7 +264,7 @@ class AppDeployHandler(BaseWebSocketHandler):
247
264
 
248
265
  headers["Subscription-Tier"] = 'Pro' if is_pro() else 'Standard'
249
266
 
250
- url_response = requests.get(f"{ACTIVE_STREAMLIT_BASE_URL}/get-upload-url?app_name={app_name}", headers=headers)
267
+ url_response = requests.get(f"{ACTIVE_STREAMLIT_BASE_URL}/get-upload-url?app_name={app_file_name_no_file_extension_ending}", headers=headers)
251
268
  url_response.raise_for_status()
252
269
 
253
270
  url_data = url_response.json()
@@ -255,7 +272,7 @@ class AppDeployHandler(BaseWebSocketHandler):
255
272
  expected_app_url = url_data['expected_app_url']
256
273
 
257
274
  self.log.info(f"Received pre-signed URL. App will be available at: {expected_app_url}")
258
-
275
+
259
276
  # Step 2: Create a zip file of the app.
260
277
  temp_zip_path = None
261
278
  try:
@@ -264,12 +281,19 @@ class AppDeployHandler(BaseWebSocketHandler):
264
281
  temp_zip_path = temp_zip.name
265
282
 
266
283
  self.log.info("Zipping application files...")
267
- add_files_to_zip(temp_zip_path, app_path, files_to_upload, self.log)
284
+ add_files_to_zip(temp_zip_path, absolute_notebook_dir_path, files_to_upload, app_file_name, self.log)
268
285
 
269
286
  upload_response = await self._upload_app_to_s3(temp_zip_path, presigned_url)
270
287
  except Exception as e:
271
288
  self.log.error(f"Error zipping app: {e}")
272
- raise
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)
273
297
  finally:
274
298
  # Clean up
275
299
  if temp_zip_path is not None:
@@ -278,20 +302,35 @@ class AppDeployHandler(BaseWebSocketHandler):
278
302
  self.log.info(f"Upload successful! Status code: {upload_response.status_code}")
279
303
 
280
304
  self.log.info(f"Deployment initiated. App will be available at: {expected_app_url}")
281
- return expected_app_url # type: ignore
305
+ return str(expected_app_url)
282
306
 
283
307
  except requests.exceptions.RequestException as e:
284
308
  self.log.error(f"Error during API request: {e}")
285
309
  if hasattr(e, 'response') and e.response is not None:
286
310
  error_detail = e.response.json()
311
+ error_message = error_detail.get('error', "")
287
312
  self.log.error(f"Server error details: {error_detail}")
288
- if 'error' in error_detail:
289
- raise Exception(error_detail['error'])
290
- raise
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)
291
324
  except Exception as e:
292
325
  self.log.error(f"Error during deployment: {str(e)}")
293
- raise
294
- raise RuntimeError("Unexpected error in _deploy_app")
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)
295
334
 
296
335
  async def _upload_app_to_s3(self, app_path: str, presigned_url: str) -> requests.Response:
297
336
  """Upload the app to S3 using the presigned URL."""
@@ -2,15 +2,10 @@
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
5
+ import traceback
6
6
  from typing import Literal, Optional, List
7
7
 
8
8
 
9
- class MessageType(str, Enum):
10
- """Types of app deploy messages."""
11
- DEPLOY_APP = "deploy-app"
12
-
13
-
14
9
  @dataclass(frozen=True)
15
10
  class AppDeployError:
16
11
  """Error information for app deploy operations."""
@@ -19,7 +14,13 @@ class AppDeployError:
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,21 +29,24 @@ class AppDeployError:
28
29
  hint: Optional[str] = None
29
30
 
30
31
  @classmethod
31
- def from_exception(cls, e: Exception, hint: Optional[str] = None) -> "AppDeployError":
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
 
@@ -59,13 +63,16 @@ class DeployAppRequest:
59
63
  """Request to deploy an app."""
60
64
 
61
65
  # Request type.
62
- type: Literal["deploy-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
72
  notebook_path: str
73
+
74
+ # Notebook ID
75
+ notebook_id: str
69
76
 
70
77
  # Files to be uploaded for the app to run
71
78
  selected_files: List[str]
@@ -88,4 +95,4 @@ class DeployAppReply:
88
95
  error: Optional[AppDeployError] = None
89
96
 
90
97
  # Type of reply.
91
- type: Literal["deploy-app"] = "deploy-app"
98
+ type: Literal["deploy_app"] = "deploy_app"
@@ -29,12 +29,13 @@ class CellUpdate(BaseModel):
29
29
  # for now and rely on the AI to respond with the correct types, following the format
30
30
  # that we show it in the system prompt.
31
31
  class AgentResponse(BaseModel):
32
- type: Literal['cell_update', 'get_cell_output', 'run_all_cells', 'finished_task']
32
+ type: Literal['cell_update', 'get_cell_output', 'run_all_cells', 'finished_task', 'create_streamlit_app', 'edit_streamlit_app']
33
33
  message: str
34
34
  cell_update: Optional[CellUpdate]
35
35
  get_cell_output_cell_id: Optional[str]
36
36
  next_steps: Optional[List[str]]
37
37
  analysis_assumptions: Optional[List[str]]
38
+ edit_streamlit_app_prompt: Optional[str]
38
39
 
39
40
 
40
41
  @dataclass(frozen=True)
@@ -65,6 +66,7 @@ class MessageType(Enum):
65
66
  UPDATE_MODEL_CONFIG = "update_model_config"
66
67
  STREAMLIT_CONVERSION = "streamlit_conversion"
67
68
  STOP_AGENT = "stop_agent"
69
+ DEPLOY_APP = "deploy_app"
68
70
 
69
71
 
70
72
  @dataclass(frozen=True)
@@ -95,7 +97,10 @@ class AgentExecutionMetadata():
95
97
  threadId: ThreadID
96
98
  input: str
97
99
  aiOptimizedCells: List[AIOptimizedCell]
100
+ activeCellId: str
98
101
  isChromeBrowser: bool
102
+ notebookPath: str
103
+ notebookID: str
99
104
  base64EncodedActiveCellOutput: Optional[str] = None
100
105
  variables: Optional[List[str]] = None
101
106
  files: Optional[List[str]] = None
@@ -3,14 +3,17 @@
3
3
 
4
4
  from mito_ai.completions.models import AgentExecutionMetadata
5
5
  from mito_ai.completions.prompt_builders.prompt_constants import (
6
+ ACTIVE_CELL_ID_SECTION_HEADING,
6
7
  FILES_SECTION_HEADING,
7
8
  JUPYTER_NOTEBOOK_SECTION_HEADING,
9
+ STREAMLIT_APP_STATUS_SECTION_HEADING,
8
10
  VARIABLES_SECTION_HEADING,
9
11
  cell_update_output_str
10
12
  )
11
13
  from mito_ai.completions.prompt_builders.utils import (
12
14
  get_rules_str,
13
15
  get_selected_context_str,
16
+ get_streamlit_app_status_str
14
17
  )
15
18
 
16
19
 
@@ -20,7 +23,10 @@ def create_agent_execution_prompt(md: AgentExecutionMetadata) -> str:
20
23
  ai_optimized_cells_str = '\n'.join([f"{cell}" for cell in md.aiOptimizedCells or []])
21
24
  rules_str = get_rules_str(md.additionalContext)
22
25
  selected_context_str = get_selected_context_str(md.additionalContext)
23
-
26
+
27
+
28
+ streamlit_status_str = get_streamlit_app_status_str(md.notebookID, md.notebookPath)
29
+
24
30
  context_str = f"""Remember to choose the correct tool to respond with.
25
31
 
26
32
  {rules_str}
@@ -35,6 +41,12 @@ def create_agent_execution_prompt(md: AgentExecutionMetadata) -> str:
35
41
  {FILES_SECTION_HEADING}
36
42
  {files_str}
37
43
 
44
+ {STREAMLIT_APP_STATUS_SECTION_HEADING}
45
+ {streamlit_status_str}
46
+
47
+ {ACTIVE_CELL_ID_SECTION_HEADING}
48
+ {md.activeCellId}
49
+
38
50
  {selected_context_str}
39
51
 
40
52
  {cell_update_output_str(md.base64EncodedActiveCellOutput is not None)}"""
@@ -225,6 +225,57 @@ Important information:
225
225
  5. Do not use this tool repeatedly if it continues to produce errors - instead, focus on fixing the specific error that occurred.
226
226
  ====
227
227
 
228
+ TOOL: CREATE_STREAMLIT_APP
229
+
230
+ When you want to create a new Streamlit app from the current notebook, respond with this format:
231
+
232
+ {{
233
+ type: 'create_streamlit_app',
234
+ message: str
235
+ }}
236
+
237
+ Important information:
238
+ 1. The message is a short summary of why you're creating the Streamlit app.
239
+ 2. Only use this tool when the user explicitly asks to create or preview a Streamlit app AND no Streamlit app is currently open.
240
+ 3. This tool creates a new app from scratch - use EDIT_STREAMLIT_APP tool if the user is asking you to edit, update, or modify an app that already exists.
241
+ 4. Using this tool will automatically open the app so the user can see a preview of the app.
242
+ 5. When you use this tool, assume that it successfully created the Streamlit app unless the user explicitly tells you otherwise. The app will remain open so that the user can view it until the user decides to close it. You do not need to continually use the create_streamlit_app tool to keep the app open.
243
+
244
+ <Example>
245
+
246
+ Your task: Show me my notebook as an app.
247
+
248
+ Output:
249
+ {{
250
+ type: 'create_streamlit_app',
251
+ message: "I'll convert your notebook into an app."
252
+ }}
253
+
254
+ The user will see a preview of the app and because you fulfilled your task, you can next respond with a FINISHED_TASK tool message.
255
+
256
+ <Example>
257
+
258
+ ====
259
+
260
+ TOOL: EDIT_STREAMLIT_APP
261
+
262
+ When you want to edit an existing Streamlit app, respond with this format:
263
+
264
+ {{
265
+ type: 'edit_streamlit_app',
266
+ message: str,
267
+ edit_streamlit_app_prompt: str
268
+ }}
269
+
270
+ Important information:
271
+ 1. The message is a short summary of why you're editing the Streamlit app.
272
+ 2. The edit_streamlit_app_prompt is REQUIRED and must contain specific instructions for the edit (e.g., "Make the title text larger", "Change the chart colors to blue", "Add a sidebar with filters").
273
+ 3. Only use this tool when the user asks to edit, update, or modify a Streamlit app.
274
+ 4. The app does not need to already be open for you to use the tool. Using this tool will automatically open the streamlit app after applying the changes so the user can view it. You do not need to call the create_streamlit_app tool first.
275
+ 5. When you use this tool, assume that it successfully edited the Streamlit app unless the user explicitly tells you otherwise. The app will remain open so that the user can view it until the user decides to close it.
276
+
277
+ ====
278
+
228
279
  TOOL: FINISHED_TASK
229
280
 
230
281
  When you have completed the user's task, respond with a message in this format:
@@ -238,8 +289,8 @@ When you have completed the user's task, respond with a message in this format:
238
289
  Important information:
239
290
  1. The message is a short summary of the ALL the work that you've completed on this task. It should not just refer to the final message. It could be something like "I've completed the sales strategy analysis by exploring key relationships in the data and summarizing creating a report with three recommendations to boost sales.""
240
291
  2. The message should include citations for any insights that you shared with the user.
241
- 3. The next_steps is an optional list of 2 or 3 suggested follow-up tasks or analyses that the user might want to perform next. These should be concise, actionable suggestions that build on the work you've just completed. For example: ["Visualize the results", "Export the cleaned data to CSV", "Perform statistical analysis on the key metrics"].
242
- 4. The next_steps should be as relevant to the user's actual task as possible. Try your best not to make generic suggestions like "Analyze the data" or "Visualize the results". For example, if the user just asked you to calculate LTV of their customers, you might suggest the following next steps: ["Graph key LTV drivers: churn and average transaction value", "Visualize LTV per customer age group"].
292
+ 3. The next_steps is an optional list of 2 or 3 suggested follow-up tasks or analyses that the user might want to perform next. These should be concise, actionable suggestions that build on the work you've just completed. For example: ["Export the cleaned data to CSV", "Analyze revenue per customer", "Convert notebook into an app"].
293
+ 4. The next_steps should be as relevant to the user's actual task as possible. Try your best not to make generic suggestions like "Analyze the data" or "Visualize the results". For example, if the user just asked you to calculate LTV of their customers, you might suggest the following next steps: ["Graph key LTV drivers: churn and average transaction value", "Visualize LTV per age group"].
243
294
  5. If you are not sure what the user might want to do next, err on the side of suggesting next steps instead of making an assumption and using more CELL_UPDATES.
244
295
  6. If the user's task doesn't involve creating or modifying a code cell, you should respond with a FINISHED_TASK response.
245
296
  7. If the user is just sending a friendly greeting (like "Hello", "Hi", "Hey", "How are you?", "What can you help me with?", etc.), you must respond with a FINISHED_TASK response message with a friendly message like this: "Hello! I'm Mito AI, your AI assistant for data analysis and Python programming in Jupyter notebooks. I can help you analyze datasets, create visualizations, clean data, and much more. What would you like to work on today?"
@@ -391,7 +442,9 @@ As you are guiding the user through the process of completing the task, send the
391
442
 
392
443
  The user is a beginning Python user, so you will need to be careful to send them only small steps to complete. Don't try to complete the task in a single response to the user. Instead, each message you send to the user should only contain a single, small step towards the end goal. When the user has completed the step, they will let you know that they are ready for the next step.
393
444
 
394
- You will keep working in the following iterative format until you have decided that you have finished the user's request. When you decide that you have finished the user's request, respond with a FINISHED_TASK tool message. Otherwise, if you have not finished the user's request, respond with a CELL_UPDATE {OR_GET_CELL_OUTPUT} tool message. When you respond with a CELL_UPDATE, the user will apply the CELL_UPDATE to the notebook and run the new code cell. The user will then send you a message with an updated version of the variables defined in the kernel, code in the notebook, and files in the current directory. In addition, the user will check if the code you provided produced an errored when executed. If it did produce an error, the user will share the error message with you.
445
+ You will keep working in the following iterative format until you have decided that you have finished the user's request. When you decide that you have finished the user's request, respond with a FINISHED_TASK tool message. Otherwise, if you have not finished the user's request, respond with one of your other tools.
446
+
447
+ When you respond with a CELL_UPDATE, the user will apply the CELL_UPDATE to the notebook and run the new code cell. The user will then send you a message with an updated version of the variables defined in the kernel, code in the notebook, and files in the current directory. In addition, the user will check if the code you provided produced an errored when executed. If it did produce an error, the user will share the error message with you.
395
448
 
396
449
  Whenever you get a message back from the user, you should:
397
450
  1. Ask yourself if the previous message you sent to the user was correct. You can answer this question by reviewing the updated code, variables, or output of the cell if you requested it.
@@ -410,4 +463,10 @@ REMEMBER, YOU ARE GOING TO COMPLETE THE USER'S TASK OVER THE COURSE OF THE ENTIR
410
463
  - If you are happy with the analysis, refer back to the original task provided by the user to decide your next steps. In this example, it is to graph the results, so you will send a CellAddition to construct the graph.
411
464
  - Wait for the user to send you back the updated variables and notebook state.
412
465
  {'' if not isChromeBrowser else '- Send a GET_CELL_OUTPUT tool message to get the output of the cell you just created and check if you can improve the graph to make it more readable, informative, or professional.'}
413
- - If after reviewing the updates you decide that you've completed the task, send a FINISHED_TASK tool message."""
466
+ - If after reviewing the updates you decide that you've completed the task, send a FINISHED_TASK tool message.
467
+
468
+ ====
469
+
470
+ OTHER USEFUL INFORMATION:
471
+ 1. When importing matplotlib, write the code `%matplotlib inline` to make sure the graphs render in Jupyter
472
+ 2. The active cell ID is shared with you so that when the user refers to "this cell" or similar phrases, you know which cell they mean. However, you are free to edit any cell that you see fit."""
@@ -21,6 +21,10 @@ There are three possible types of responses you might give:
21
21
  2. Explanation/Analysis: If the task does not require a code update, it might instead require you to provide an explanation of existing code or data, provide an analysis of the the data or chart.
22
22
  3. Friendly Response: If the user is just asking a question, saying hi, or you're just chatting, respond with a friendly response and do not return any code.
23
23
 
24
+ Other useful information:
25
+ 1. The user has two types of modes that they can collaborate with you in: Chat Mode (this mode) and agent mode. Chat mode gives the user more control over the edits made to the notebook and only edits the active cell. Agent mode gives you more autonomy over completing the user's task across mulitple messages. In agent mode, you can edit or create new cells, see the entire notebook, automatically run the code you write, and more.
26
+ 2. If the user asks you to generate a dashboard, app, or streamlit app for them, you should tell them that they must use Agent mode to complete the task. You are not able to automatically switch the user to agent mode, but they can switch to it themselves by using the Chat/Agent mode toggle in the bottom left corner of the Ai taskpane.
27
+
24
28
  ====
25
29
  {CITATION_RULES}
26
30