mito-ai 0.1.46__py3-none-any.whl → 0.1.47__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.

Potentially problematic release.


This version of mito-ai might be problematic. Click here for more details.

Files changed (70) hide show
  1. mito_ai/_version.py +1 -1
  2. mito_ai/app_deploy/handlers.py +97 -77
  3. mito_ai/app_deploy/models.py +16 -12
  4. mito_ai/completions/models.py +4 -1
  5. mito_ai/completions/prompt_builders/agent_execution_prompt.py +6 -1
  6. mito_ai/completions/prompt_builders/agent_system_message.py +63 -4
  7. mito_ai/completions/prompt_builders/chat_system_message.py +4 -0
  8. mito_ai/completions/prompt_builders/prompt_constants.py +1 -0
  9. mito_ai/completions/prompt_builders/utils.py +14 -0
  10. mito_ai/path_utils.py +56 -0
  11. mito_ai/streamlit_conversion/agent_utils.py +4 -201
  12. mito_ai/streamlit_conversion/prompts/prompt_constants.py +142 -152
  13. mito_ai/streamlit_conversion/prompts/streamlit_error_correction_prompt.py +3 -3
  14. mito_ai/streamlit_conversion/prompts/streamlit_finish_todo_prompt.py +2 -2
  15. mito_ai/streamlit_conversion/prompts/update_existing_app_prompt.py +2 -2
  16. mito_ai/streamlit_conversion/search_replace_utils.py +93 -0
  17. mito_ai/streamlit_conversion/streamlit_agent_handler.py +29 -39
  18. mito_ai/streamlit_conversion/streamlit_utils.py +11 -64
  19. mito_ai/streamlit_conversion/validate_streamlit_app.py +5 -18
  20. mito_ai/streamlit_preview/handlers.py +44 -84
  21. mito_ai/streamlit_preview/manager.py +6 -6
  22. mito_ai/streamlit_preview/utils.py +16 -19
  23. mito_ai/tests/streamlit_conversion/test_apply_search_replace.py +226 -0
  24. mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +29 -53
  25. mito_ai/tests/streamlit_conversion/test_streamlit_utils.py +26 -29
  26. mito_ai/tests/streamlit_conversion/test_validate_streamlit_app.py +6 -3
  27. mito_ai/tests/streamlit_preview/test_streamlit_preview_handler.py +12 -15
  28. mito_ai/tests/streamlit_preview/test_streamlit_preview_manager.py +22 -26
  29. mito_ai/user/handlers.py +15 -3
  30. mito_ai/utils/create.py +17 -1
  31. mito_ai/utils/error_classes.py +42 -0
  32. mito_ai/utils/message_history_utils.py +3 -1
  33. mito_ai/utils/telemetry_utils.py +75 -10
  34. {mito_ai-0.1.46.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +1 -1
  35. {mito_ai-0.1.46.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/package.json +2 -2
  36. {mito_ai-0.1.46.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/package.json.orig +1 -1
  37. mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.20f12766ecd3d430568e.js → mito_ai-0.1.47.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.2db61d2b629817845901.js +1274 -293
  38. mito_ai-0.1.47.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.2db61d2b629817845901.js.map +1 -0
  39. mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.54126ab6511271265443.js → mito_ai-0.1.47.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.e22c6cd4e56c32116daa.js +7 -7
  40. mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.54126ab6511271265443.js.map → mito_ai-0.1.47.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.e22c6cd4e56c32116daa.js.map +1 -1
  41. {mito_ai-0.1.46.dist-info → mito_ai-0.1.47.dist-info}/METADATA +1 -1
  42. {mito_ai-0.1.46.dist-info → mito_ai-0.1.47.dist-info}/RECORD +67 -65
  43. mito_ai/tests/streamlit_conversion/test_apply_patch_to_text.py +0 -368
  44. mito_ai/tests/streamlit_conversion/test_fix_diff_headers.py +0 -533
  45. mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.20f12766ecd3d430568e.js.map +0 -1
  46. /mito_ai/streamlit_conversion/{streamlit_system_prompt.py → prompts/streamlit_system_prompt.py} +0 -0
  47. {mito_ai-0.1.46.data → mito_ai-0.1.47.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
  48. {mito_ai-0.1.46.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/toolbar-buttons.json +0 -0
  49. {mito_ai-0.1.46.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js +0 -0
  50. {mito_ai-0.1.46.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js.map +0 -0
  51. {mito_ai-0.1.46.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
  52. {mito_ai-0.1.46.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js +0 -0
  53. {mito_ai-0.1.46.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js.map +0 -0
  54. {mito_ai-0.1.46.data → mito_ai-0.1.47.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
  55. {mito_ai-0.1.46.data → mito_ai-0.1.47.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
  56. {mito_ai-0.1.46.data → mito_ai-0.1.47.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
  57. {mito_ai-0.1.46.data → mito_ai-0.1.47.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
  58. {mito_ai-0.1.46.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js +0 -0
  59. {mito_ai-0.1.46.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js.map +0 -0
  60. {mito_ai-0.1.46.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js +0 -0
  61. {mito_ai-0.1.46.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js.map +0 -0
  62. {mito_ai-0.1.46.data → mito_ai-0.1.47.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
  63. {mito_ai-0.1.46.data → mito_ai-0.1.47.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
  64. {mito_ai-0.1.46.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js +0 -0
  65. {mito_ai-0.1.46.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js.map +0 -0
  66. {mito_ai-0.1.46.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js +0 -0
  67. {mito_ai-0.1.46.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js.map +0 -0
  68. {mito_ai-0.1.46.dist-info → mito_ai-0.1.47.dist-info}/WHEEL +0 -0
  69. {mito_ai-0.1.46.dist-info → mito_ai-0.1.47.dist-info}/entry_points.txt +0 -0
  70. {mito_ai-0.1.46.dist-info → mito_ai-0.1.47.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,93 @@
1
+ # Copyright (c) Saga Inc.
2
+ # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
+
4
+ import re
5
+ from typing import List, Tuple
6
+
7
+ from mito_ai.utils.error_classes import StreamlitConversionError
8
+
9
+
10
+ def extract_search_replace_blocks(message_content: str) -> List[Tuple[str, str]]:
11
+ """
12
+ Extract all search_replace blocks from Claude's response.
13
+
14
+ Returns:
15
+ List of tuples (search_text, replace_text) for each search/replace block
16
+ """
17
+ if "```search_replace" not in message_content:
18
+ return []
19
+
20
+ pattern = r'```search_replace\n(.*?)```'
21
+ matches = re.findall(pattern, message_content, re.DOTALL)
22
+
23
+ search_replace_pairs = []
24
+ for match in matches:
25
+ # Split by the separator
26
+ if "=======" not in match:
27
+ continue
28
+
29
+ parts = match.split("=======", 1)
30
+ if len(parts) != 2:
31
+ continue
32
+
33
+ search_part = parts[0]
34
+ replace_part = parts[1]
35
+
36
+ # Extract search text (after SEARCH marker)
37
+ if ">>>>>>> SEARCH" in search_part:
38
+ search_text = search_part.split(">>>>>>> SEARCH", 1)[1].strip()
39
+ else:
40
+ continue
41
+
42
+ # Extract replace text (before REPLACE marker)
43
+ if "<<<<<<< REPLACE" in replace_part:
44
+ replace_text = replace_part.split("<<<<<<< REPLACE", 1)[0].strip()
45
+ else:
46
+ continue
47
+
48
+ search_replace_pairs.append((search_text, replace_text))
49
+
50
+ return search_replace_pairs
51
+
52
+
53
+ def apply_search_replace(text: str, search_replace_pairs: List[Tuple[str, str]]) -> str:
54
+ """
55
+ Apply search/replace operations to the given text.
56
+
57
+ Parameters
58
+ ----------
59
+ text : str
60
+ The original file contents.
61
+ search_replace_pairs : List[Tuple[str, str]]
62
+ List of (search_text, replace_text) tuples to apply.
63
+
64
+ Returns
65
+ -------
66
+ str
67
+ The updated contents after applying all search/replace operations.
68
+
69
+ Raises
70
+ ------
71
+ ValueError
72
+ If a search text is not found or found multiple times.
73
+ """
74
+ if not search_replace_pairs:
75
+ return text
76
+
77
+ result = text
78
+
79
+ for search_text, replace_text in search_replace_pairs:
80
+ # Count occurrences of search text
81
+ count = result.count(search_text)
82
+
83
+ if count == 0:
84
+ print("Search Text Not Found: ", repr(search_text))
85
+ raise StreamlitConversionError(f"Search text not found: {repr(search_text)}", error_code=500)
86
+ elif count > 1:
87
+ print("Search Text Found Multiple Times: ", repr(search_text))
88
+ raise StreamlitConversionError(f"Search text found {count} times (must be unique): {repr(search_text)}", error_code=500)
89
+
90
+ # Perform the replacement
91
+ result = result.replace(search_text, replace_text)
92
+
93
+ return result
@@ -4,24 +4,18 @@
4
4
  import os
5
5
  from anthropic.types import MessageParam
6
6
  from typing import List, Optional, Tuple, cast
7
- from mito_ai.streamlit_conversion.agent_utils import apply_patch_to_text, extract_todo_placeholders, fix_diff_headers, get_response_from_agent
7
+ from mito_ai.streamlit_conversion.agent_utils import extract_todo_placeholders, get_response_from_agent
8
8
  from mito_ai.streamlit_conversion.prompts.streamlit_app_creation_prompt import get_streamlit_app_creation_prompt
9
9
  from mito_ai.streamlit_conversion.prompts.streamlit_error_correction_prompt import get_streamlit_error_correction_prompt
10
10
  from mito_ai.streamlit_conversion.prompts.streamlit_finish_todo_prompt import get_finish_todo_prompt
11
11
  from mito_ai.streamlit_conversion.prompts.update_existing_app_prompt import get_update_existing_app_prompt
12
12
  from mito_ai.streamlit_conversion.validate_streamlit_app import validate_app
13
- from mito_ai.streamlit_conversion.streamlit_utils import extract_code_blocks, create_app_file, extract_unified_diff_blocks, get_app_code_from_file, parse_jupyter_notebook_to_extract_required_content
13
+ from mito_ai.streamlit_conversion.streamlit_utils import extract_code_blocks, create_app_file, get_app_code_from_file, parse_jupyter_notebook_to_extract_required_content
14
+ from mito_ai.streamlit_conversion.search_replace_utils import extract_search_replace_blocks, apply_search_replace
14
15
  from mito_ai.completions.models import MessageType
15
- from mito_ai.utils.telemetry_utils import log_streamlit_app_creation_error, log_streamlit_app_creation_retry, log_streamlit_app_creation_success
16
- from mito_ai.streamlit_conversion.streamlit_utils import clean_directory_check
17
-
18
- def get_app_directory(notebook_path: str) -> str:
19
- # Make sure the path is absolute if it is not already
20
- absolute_notebook_path = os.path.abspath(notebook_path)
21
-
22
- # Get the directory of the notebook
23
- app_directory = os.path.dirname(absolute_notebook_path)
24
- return app_directory
16
+ from mito_ai.utils.error_classes import StreamlitConversionError
17
+ from mito_ai.utils.telemetry_utils import log_streamlit_app_validation_retry, log_streamlit_app_conversion_success
18
+ from mito_ai.path_utils import AbsoluteNotebookPath, get_absolute_notebook_dir_path, get_absolute_app_path
25
19
 
26
20
  async def generate_new_streamlit_code(notebook: List[dict]) -> str:
27
21
  """Send a query to the agent, get its response and parse the code"""
@@ -57,10 +51,9 @@ async def generate_new_streamlit_code(notebook: List[dict]) -> str:
57
51
  ]
58
52
  todo_response = await get_response_from_agent(todo_messages)
59
53
 
60
- # Apply the diff to the streamlit app
61
- exctracted_diff = extract_unified_diff_blocks(todo_response)
62
- fixed_diff = fix_diff_headers(exctracted_diff)
63
- converted_code = apply_patch_to_text(converted_code, fixed_diff)
54
+ # Apply the search/replace to the streamlit app
55
+ search_replace_pairs = extract_search_replace_blocks(todo_response)
56
+ converted_code = apply_search_replace(converted_code, search_replace_pairs)
64
57
 
65
58
  return converted_code
66
59
 
@@ -80,10 +73,12 @@ async def update_existing_streamlit_code(notebook: List[dict], streamlit_app_cod
80
73
  ]
81
74
 
82
75
  agent_response = await get_response_from_agent(messages)
83
- exctracted_diff = extract_unified_diff_blocks(agent_response)
84
- fixed_diff = fix_diff_headers(exctracted_diff)
85
- print(fixed_diff)
86
- converted_code = apply_patch_to_text(streamlit_app_code, fixed_diff)
76
+ print(f"[Mito AI Search/Replace Tool]:\n {agent_response}")
77
+
78
+ # Apply the search/replace to the streamlit app
79
+ search_replace_pairs = extract_search_replace_blocks(agent_response)
80
+ converted_code = apply_search_replace(streamlit_app_code, search_replace_pairs)
81
+ print(f"[Mito AI Search/Replace Tool]\nConverted code\n: {converted_code}")
87
82
  return converted_code
88
83
 
89
84
 
@@ -100,27 +95,26 @@ async def correct_error_in_generation(error: str, streamlit_app_code: str) -> st
100
95
  ]
101
96
  agent_response = await get_response_from_agent(messages)
102
97
 
103
- # Apply the diff to the streamlit app
104
- exctracted_diff = extract_unified_diff_blocks(agent_response)
105
- fixed_diff = fix_diff_headers(exctracted_diff)
106
- streamlit_app_code = apply_patch_to_text(streamlit_app_code, fixed_diff)
98
+ # Apply the search/replace to the streamlit app
99
+ search_replace_pairs = extract_search_replace_blocks(agent_response)
100
+ streamlit_app_code = apply_search_replace(streamlit_app_code, search_replace_pairs)
107
101
 
108
102
  return streamlit_app_code
109
103
 
110
- async def streamlit_handler(notebook_path: str, edit_prompt: str = "") -> Tuple[bool, Optional[str], str]:
104
+ async def streamlit_handler(notebook_path: AbsoluteNotebookPath, edit_prompt: str = "") -> None:
111
105
  """Handler function for streamlit code generation and validation"""
112
106
 
113
- clean_directory_check(notebook_path)
114
-
107
+ # Convert to absolute path for consistent handling
115
108
  notebook_code = parse_jupyter_notebook_to_extract_required_content(notebook_path)
116
- app_directory = get_app_directory(notebook_path)
109
+ app_directory = get_absolute_notebook_dir_path(notebook_path)
110
+ app_path = get_absolute_app_path(app_directory)
117
111
 
118
112
  if edit_prompt != "":
119
113
  # If the user is editing an existing streamlit app, use the update function
120
- streamlit_code = get_app_code_from_file(app_directory)
114
+ streamlit_code = get_app_code_from_file(app_path)
121
115
 
122
116
  if streamlit_code is None:
123
- return False, '', "Error updating existing streamlit app because app.py file was not found."
117
+ raise StreamlitConversionError("Error updating existing streamlit app because app.py file was not found.", 404)
124
118
 
125
119
  streamlit_code = await update_existing_streamlit_code(notebook_code, streamlit_code, edit_prompt)
126
120
  else:
@@ -139,17 +133,13 @@ async def streamlit_handler(notebook_path: str, edit_prompt: str = "") -> Tuple[
139
133
  if has_validation_error:
140
134
  # TODO: We can't easily get the key type here, so for the beta release
141
135
  # we are just defaulting to the mito server key since that is by far the most common.
142
- log_streamlit_app_creation_retry('mito_server_key', MessageType.STREAMLIT_CONVERSION, error)
136
+ log_streamlit_app_validation_retry('mito_server_key', MessageType.STREAMLIT_CONVERSION, errors)
143
137
  tries+=1
144
138
 
145
139
  if has_validation_error:
146
- log_streamlit_app_creation_error('mito_server_key', MessageType.STREAMLIT_CONVERSION, error, edit_prompt)
147
- return False, '', "Error generating streamlit code by agent"
140
+ final_errors = ', '.join(errors)
141
+ raise StreamlitConversionError(f"Streamlit agent failed generating code after max retries. Errors: {final_errors}", 500)
148
142
 
149
143
  # Finally, update the app.py file with the new code
150
- success_flag, app_path, message = create_app_file(app_directory, streamlit_code)
151
- if not success_flag:
152
- log_streamlit_app_creation_error('mito_server_key', MessageType.STREAMLIT_CONVERSION, message, edit_prompt)
153
-
154
- log_streamlit_app_creation_success('mito_server_key', MessageType.STREAMLIT_CONVERSION, edit_prompt)
155
- return success_flag, app_path, message
144
+ create_app_file(app_path, streamlit_code)
145
+ log_streamlit_app_conversion_success('mito_server_key', MessageType.STREAMLIT_CONVERSION, edit_prompt)
@@ -3,9 +3,11 @@
3
3
 
4
4
  import re
5
5
  import json
6
- import os
7
6
  from typing import Dict, List, Optional, Tuple, Any
7
+ from mito_ai.path_utils import AbsoluteAppPath, AbsoluteNotebookPath
8
8
  from pathlib import Path
9
+ from mito_ai.utils.error_classes import StreamlitConversionError
10
+
9
11
 
10
12
  def extract_code_blocks(message_content: str) -> str:
11
13
  """
@@ -28,19 +30,7 @@ def extract_code_blocks(message_content: str) -> str:
28
30
  result = '\n'.join(matches)
29
31
  return result
30
32
 
31
- def extract_unified_diff_blocks(message_content: str) -> str:
32
- """
33
- Extract all unified_diff blocks from Claude's response.
34
- """
35
- if "```unified_diff" not in message_content:
36
- return message_content
37
-
38
- pattern = r'```unified_diff\n(.*?)```'
39
- matches = re.findall(pattern, message_content, re.DOTALL)
40
- return '\n'.join(matches)
41
-
42
-
43
- def create_app_file(app_directory: str, code: str) -> Tuple[bool, str, str]:
33
+ def create_app_file(app_path: AbsoluteAppPath, code: str) -> None:
44
34
  """
45
35
  Create app.py file and write code to it with error handling
46
36
 
@@ -53,40 +43,21 @@ def create_app_file(app_directory: str, code: str) -> Tuple[bool, str, str]:
53
43
 
54
44
  """
55
45
  try:
56
- app_path = os.path.join(app_directory, "app.py")
57
-
58
46
  with open(app_path, 'w', encoding='utf-8') as f:
59
47
  f.write(code)
60
-
61
- return True, app_path, f"Successfully created {app_directory}"
62
48
  except IOError as e:
63
- return False, '', f"Error creating file: {str(e)}"
64
- except Exception as e:
65
- return False, '', f"Unexpected error: {str(e)}"
49
+ raise StreamlitConversionError(f"Error creating app file: {str(e)}", 500)
66
50
 
67
- def get_app_code_from_file(app_directory: str) -> Optional[str]:
68
- app_path = get_app_path(app_directory)
69
- if app_path is None:
70
- return None
51
+ def get_app_code_from_file(app_path: AbsoluteAppPath) -> Optional[str]:
71
52
  with open(app_path, 'r', encoding='utf-8') as f:
72
53
  return f.read()
73
-
74
-
75
- def get_app_path(app_directory: str) -> Optional[str]:
76
- """
77
- Check if the app.py file exists in the given directory.
78
- """
79
- app_path = os.path.join(app_directory, "app.py")
80
- if not os.path.exists(app_path):
81
- return None
82
- return app_path
83
54
 
84
- def parse_jupyter_notebook_to_extract_required_content(notebook_path: str) -> List[Dict[str, Any]]:
55
+ def parse_jupyter_notebook_to_extract_required_content(notebook_path: AbsoluteNotebookPath) -> List[Dict[str, Any]]:
85
56
  """
86
57
  Read a Jupyter notebook and filter cells to keep only cell_type and source fields.
87
58
 
88
59
  Args:
89
- notebook_path (str): Path to the .ipynb file (can be relative or absolute)
60
+ notebook_path: Absolute path to the .ipynb file
90
61
 
91
62
  Returns:
92
63
  dict: Filtered notebook dictionary with only cell_type and source in cells
@@ -96,10 +67,6 @@ def parse_jupyter_notebook_to_extract_required_content(notebook_path: str) -> Li
96
67
  json.JSONDecodeError: If the file is not valid JSON
97
68
  KeyError: If the notebook doesn't have the expected structure
98
69
  """
99
- # Convert to absolute path if it's not already absolute
100
- # Handle both Unix-style absolute paths (starting with /) and Windows-style absolute paths
101
- if not (notebook_path.startswith('/') or (len(notebook_path) > 1 and notebook_path[1] == ':')):
102
- notebook_path = os.path.join(os.getcwd(), notebook_path)
103
70
 
104
71
  try:
105
72
  # Read the notebook file
@@ -108,7 +75,7 @@ def parse_jupyter_notebook_to_extract_required_content(notebook_path: str) -> Li
108
75
 
109
76
  # Check if 'cells' key exists
110
77
  if 'cells' not in notebook_data:
111
- raise KeyError("Notebook does not contain 'cells' key")
78
+ raise StreamlitConversionError("Notebook does not contain 'cells' key", 400)
112
79
 
113
80
  # Filter each cell to keep only cell_type and source
114
81
  filtered_cells: List[Dict[str, Any]] = []
@@ -122,26 +89,6 @@ def parse_jupyter_notebook_to_extract_required_content(notebook_path: str) -> Li
122
89
  return filtered_cells
123
90
 
124
91
  except FileNotFoundError:
125
- raise FileNotFoundError(f"Notebook file not found: {notebook_path}")
92
+ raise StreamlitConversionError(f"Notebook file not found: {notebook_path}", 404)
126
93
  except json.JSONDecodeError as e:
127
- # JSONDecodeError requires msg, doc, pos
128
- raise json.JSONDecodeError(f"Invalid JSON in notebook file: {str(e)}", e.doc if hasattr(e, 'doc') else '', e.pos if hasattr(e, 'pos') else 0)
129
- except Exception as e:
130
- raise Exception(f"Error processing notebook: {str(e)}")
131
-
132
-
133
- def resolve_notebook_path(notebook_path:str) -> str:
134
- # Convert to absolute path if it's not already absolute
135
- # Handle both Unix-style absolute paths (starting with /) and Windows-style absolute paths
136
- if not (notebook_path.startswith('/') or (len(notebook_path) > 1 and notebook_path[1] == ':')):
137
- notebook_path = os.path.join(os.getcwd(), notebook_path)
138
- return notebook_path
139
-
140
- def clean_directory_check(notebook_path: str) -> None:
141
- notebook_path = resolve_notebook_path(notebook_path)
142
- # pathlib handles the cross OS path conversion automatically
143
- path = Path(notebook_path).resolve()
144
- dir_path = path.parent
145
-
146
- if not dir_path.exists():
147
- raise ValueError(f"Directory does not exist: {dir_path}")
94
+ raise StreamlitConversionError(f"Invalid JSON in notebook file: {str(e)}", 400)
@@ -1,28 +1,18 @@
1
1
  # Copyright (c) Saga Inc.
2
2
  # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
3
 
4
- import sys
5
4
  import os
6
- import time
7
- import requests
8
5
  import tempfile
9
- import shutil
10
6
  import traceback
11
7
  import ast
12
- import importlib.util
13
8
  import warnings
14
9
  from typing import List, Tuple, Optional, Dict, Any, Generator
15
10
  from streamlit.testing.v1 import AppTest
16
11
  from contextlib import contextmanager
17
- from mito_ai.streamlit_conversion.streamlit_utils import resolve_notebook_path
18
-
19
-
20
- # warnings.filterwarnings("ignore", message=r".*missing ScriptRunContext.*")
21
- # warnings.filterwarnings("ignore", category=UserWarning)
12
+ from mito_ai.path_utils import AbsoluteNotebookPath, get_absolute_notebook_dir_path
22
13
 
23
14
  warnings.filterwarnings("ignore", message=".*bare mode.*")
24
15
 
25
-
26
16
  def get_syntax_error(app_code: str) -> Optional[str]:
27
17
  """Check if the Python code has valid syntax"""
28
18
  try:
@@ -32,10 +22,10 @@ def get_syntax_error(app_code: str) -> Optional[str]:
32
22
  error_msg = ''.join(traceback.format_exception(type(e), e, e.__traceback__))
33
23
  return error_msg
34
24
 
35
- def get_runtime_errors(app_code: str, app_path: str) -> Optional[List[Dict[str, Any]]]:
25
+ def get_runtime_errors(app_code: str, app_path: AbsoluteNotebookPath) -> Optional[List[Dict[str, Any]]]:
36
26
  """Start the Streamlit app in a subprocess"""
37
27
 
38
- directory = os.path.dirname(app_path)
28
+ directory = get_absolute_notebook_dir_path(app_path)
39
29
 
40
30
  @contextmanager
41
31
  def change_working_directory(path: str) -> Generator[None, Any, None]:
@@ -89,7 +79,7 @@ def get_runtime_errors(app_code: str, app_path: str) -> Optional[List[Dict[str,
89
79
  except OSError:
90
80
  pass # File might already be deleted
91
81
 
92
- def check_for_errors(app_code: str, app_path: str) -> List[Dict[str, Any]]:
82
+ def check_for_errors(app_code: str, app_path: AbsoluteNotebookPath) -> List[Dict[str, Any]]:
93
83
  """Complete validation pipeline"""
94
84
  errors: List[Dict[str, Any]] = []
95
85
 
@@ -100,7 +90,6 @@ def check_for_errors(app_code: str, app_path: str) -> List[Dict[str, Any]]:
100
90
  errors.append({'type': 'syntax', 'details': syntax_error})
101
91
 
102
92
  runtime_errors = get_runtime_errors(app_code, app_path)
103
-
104
93
  if runtime_errors:
105
94
  errors.extend(runtime_errors)
106
95
 
@@ -109,10 +98,8 @@ def check_for_errors(app_code: str, app_path: str) -> List[Dict[str, Any]]:
109
98
 
110
99
  return errors
111
100
 
112
- def validate_app(app_code: str, notebook_path: str) -> Tuple[bool, List[str]]:
101
+ def validate_app(app_code: str, notebook_path: AbsoluteNotebookPath) -> Tuple[bool, List[str]]:
113
102
  """Convenience function to validate Streamlit code"""
114
- notebook_path = resolve_notebook_path(notebook_path)
115
-
116
103
  errors = check_for_errors(app_code, notebook_path)
117
104
 
118
105
  has_validation_error = len(errors) > 0
@@ -2,17 +2,17 @@
2
2
  # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
3
 
4
4
  import os
5
- import tempfile
5
+ from typing import Literal, TypedDict
6
6
  import uuid
7
- from mito_ai.streamlit_conversion.streamlit_utils import get_app_path
8
7
  from mito_ai.streamlit_preview.utils import ensure_app_exists, validate_request_body
9
8
  import tornado
10
9
  from jupyter_server.base.handlers import APIHandler
11
- from mito_ai.streamlit_conversion.streamlit_agent_handler import streamlit_handler
12
10
  from mito_ai.streamlit_preview.manager import get_preview_manager
13
- from mito_ai.utils.create import initialize_user
14
- from typing import Tuple, Optional
15
-
11
+ from mito_ai.path_utils import AbsoluteNotebookPath, get_absolute_notebook_dir_path, get_absolute_notebook_path
12
+ from mito_ai.utils.telemetry_utils import log_streamlit_app_conversion_error, log_streamlit_app_preview_failure, log_streamlit_app_preview_success
13
+ from mito_ai.completions.models import MessageType
14
+ from mito_ai.utils.error_classes import StreamlitConversionError, StreamlitPreviewError
15
+ import traceback
16
16
 
17
17
 
18
18
  class StreamlitPreviewHandler(APIHandler):
@@ -22,102 +22,62 @@ class StreamlitPreviewHandler(APIHandler):
22
22
  """Initialize the handler."""
23
23
  self.preview_manager = get_preview_manager()
24
24
 
25
- def _resolve_notebook_path(self, notebook_path: str) -> str:
26
- """
27
- Resolve the notebook path to an absolute path that can be found by the backend.
28
-
29
- This method handles path resolution issues that can occur in different environments:
30
-
31
- 1. **Test Environment**: Playwright tests create temporary directories with complex
32
- paths like 'mitoai_ui_tests-app_builde-ab3a5-n-Test-Preview-as-Streamlit-chromium/'
33
- that the backend can't directly access.
34
-
35
- 2. **JupyterHub/Cloud Deployments**: In cloud environments, users may have notebooks
36
- in subdirectories that aren't immediately accessible from the server root.
37
-
38
- 3. **Docker Containers**: When running in containers, the working directory and
39
- file paths may not align with what the frontend reports.
40
-
41
- 4. **Multi-user Environments**: In enterprise deployments, users may have notebooks
42
- in user-specific directories that require path resolution.
43
-
44
- The method tries multiple strategies:
45
- 1. If the path is already absolute, return it as-is
46
- 2. Try to resolve relative to the Jupyter server's root directory
47
- 3. Search recursively through subdirectories for a file with the same name
48
- 4. Return the original path if not found (will cause a clear error message)
49
-
50
- Args:
51
- notebook_path (str): The notebook path from the frontend (may be relative or absolute)
52
-
53
- Returns:
54
- str: The resolved absolute path to the notebook file
55
- """
56
- # If the path is already absolute, return it
57
- if os.path.isabs(notebook_path):
58
- return notebook_path
59
-
60
- # Get the Jupyter server's root directory
61
- server_root = self.settings.get("server_root_dir", os.getcwd())
62
-
63
- # Try to find the notebook file in the server root
64
- resolved_path = os.path.join(server_root, notebook_path)
65
- if os.path.exists(resolved_path):
66
- return resolved_path
67
-
68
- # If not found, try to find it in subdirectories
69
- # This handles cases where the notebook is in a subdirectory that the frontend
70
- # doesn't know about, or where the path structure differs between frontend and backend
71
- for root, dirs, files in os.walk(server_root):
72
- if os.path.basename(notebook_path) in files:
73
- return os.path.join(root, os.path.basename(notebook_path))
74
-
75
- # If still not found, return the original path (will cause a clear error)
76
- # This ensures we get a meaningful error message rather than a generic "file not found"
77
- return os.path.join(os.getcwd(), notebook_path)
78
-
79
25
  @tornado.web.authenticated
80
26
  async def post(self) -> None:
81
27
  """Start a new streamlit preview."""
82
28
  try:
83
29
  # Parse and validate request
84
30
  body = self.get_json_body()
85
- is_valid, error_msg, notebook_path, force_recreate, edit_prompt = validate_request_body(body)
86
- if not is_valid or not notebook_path:
87
- self.set_status(400)
88
- self.finish({"error": error_msg})
89
- return
31
+ notebook_path, force_recreate, edit_prompt = validate_request_body(body)
90
32
 
91
33
  # Ensure app exists
92
- resolved_notebook_path = self._resolve_notebook_path(notebook_path)
93
-
94
- success, error_msg = await ensure_app_exists(resolved_notebook_path, force_recreate, edit_prompt)
95
-
96
- if not success:
97
- self.set_status(500)
98
- self.finish({"error": error_msg})
99
- return
34
+ absolute_notebook_path = get_absolute_notebook_path(notebook_path)
35
+ await ensure_app_exists(absolute_notebook_path, force_recreate, edit_prompt)
100
36
 
101
37
  # Start preview
102
38
  # TODO: There's a bug here where when the user rebuilds and already running app. Instead of
103
39
  # creating a new process, we should update the existing process. The app displayed to the user
104
40
  # does update, but that's just because of hot reloading when we overwrite the app.py file.
105
41
  preview_id = str(uuid.uuid4())
106
- resolved_app_directory = os.path.dirname(resolved_notebook_path)
107
- success, message, port = self.preview_manager.start_streamlit_preview(resolved_app_directory, preview_id)
108
-
109
- if not success:
110
- self.set_status(500)
111
- self.finish({"error": f"Failed to start preview: {message}"})
112
- return
42
+ absolute_app_directory = get_absolute_notebook_dir_path(absolute_notebook_path)
43
+ port = self.preview_manager.start_streamlit_preview(absolute_app_directory, preview_id)
113
44
 
114
45
  # Return success response
115
- self.finish({"id": preview_id, "port": port, "url": f"http://localhost:{port}"})
116
-
46
+ self.finish({
47
+ "type": 'success',
48
+ "id": preview_id,
49
+ "port": port,
50
+ "url": f"http://localhost:{port}"
51
+ })
52
+ log_streamlit_app_preview_success('mito_server_key', MessageType.STREAMLIT_CONVERSION, edit_prompt)
53
+
54
+ except StreamlitConversionError as e:
55
+ print(e)
56
+ self.set_status(e.error_code)
57
+ error_message = str(e)
58
+ formatted_traceback = traceback.format_exc()
59
+ self.finish({"error": error_message})
60
+ log_streamlit_app_conversion_error(
61
+ 'mito_server_key',
62
+ MessageType.STREAMLIT_CONVERSION,
63
+ error_message,
64
+ formatted_traceback,
65
+ edit_prompt,
66
+ )
67
+ except StreamlitPreviewError as e:
68
+ print(e)
69
+ error_message = str(e)
70
+ formatted_traceback = traceback.format_exc()
71
+ self.set_status(e.error_code)
72
+ self.finish({"error": error_message})
73
+ log_streamlit_app_preview_failure('mito_server_key', MessageType.STREAMLIT_CONVERSION, error_message, formatted_traceback, edit_prompt)
117
74
  except Exception as e:
118
- print(f"Error in streamlit preview handler: {e}")
75
+ print(f"Exception in streamlit preview handler: {e}")
119
76
  self.set_status(500)
120
- self.finish({"error": str(e)})
77
+ error_message = str(e)
78
+ formatted_traceback = traceback.format_exc()
79
+ self.finish({"error": error_message})
80
+ log_streamlit_app_preview_failure('mito_server_key', MessageType.STREAMLIT_CONVERSION, error_message, formatted_traceback, "")
121
81
 
122
82
  @tornado.web.authenticated
123
83
  def delete(self, preview_id: str) -> None:
@@ -11,6 +11,7 @@ import requests
11
11
  from typing import Dict, Optional, Tuple
12
12
  from dataclasses import dataclass
13
13
  from mito_ai.logger import get_logger
14
+ from mito_ai.utils.error_classes import StreamlitPreviewError
14
15
 
15
16
 
16
17
  @dataclass
@@ -37,7 +38,7 @@ class StreamlitPreviewManager:
37
38
 
38
39
  return port
39
40
 
40
- def start_streamlit_preview(self, app_directory: str, preview_id: str) -> Tuple[bool, str, Optional[int]]:
41
+ def start_streamlit_preview(self, app_directory: str, preview_id: str) -> int:
41
42
  """Start a streamlit preview process.
42
43
 
43
44
  Args:
@@ -80,7 +81,7 @@ class StreamlitPreviewManager:
80
81
  if not ready:
81
82
  proc.terminate()
82
83
  proc.wait()
83
- return False, "Streamlit app failed to start", None
84
+ raise StreamlitPreviewError("Streamlit app failed to start as app is not ready", 500)
84
85
 
85
86
  # Register the process
86
87
  with self._lock:
@@ -90,11 +91,11 @@ class StreamlitPreviewManager:
90
91
  )
91
92
 
92
93
  self.log.info(f"Started streamlit preview {preview_id} on port {port}")
93
- return True, "Preview started successfully", port
94
+ return port
94
95
 
95
96
  except Exception as e:
96
97
  self.log.error(f"Error starting streamlit preview: {e}")
97
- return False, f"Failed to start preview: {str(e)}", None
98
+ raise StreamlitPreviewError(f"Failed to start preview: {str(e)}", 500)
98
99
 
99
100
  def _wait_for_app_ready(self, port: int, timeout: int = 30) -> bool:
100
101
  """Wait for streamlit app to be ready on the given port."""
@@ -106,7 +107,7 @@ class StreamlitPreviewManager:
106
107
  if response.status_code == 200:
107
108
  return True
108
109
  except requests.exceptions.RequestException as e:
109
- print(f"Error waiting for app to be ready: {e}")
110
+ print(f"Waiting for app to be ready...")
110
111
  pass
111
112
 
112
113
  time.sleep(1)
@@ -153,7 +154,6 @@ class StreamlitPreviewManager:
153
154
  # Global instance
154
155
  _preview_manager = StreamlitPreviewManager()
155
156
 
156
-
157
157
  def get_preview_manager() -> StreamlitPreviewManager:
158
158
  """Get the global preview manager instance."""
159
159
  return _preview_manager