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
@@ -1,7 +1,7 @@
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
- from mito_ai.streamlit_conversion.prompts.prompt_constants import unified_diff_instructions
4
+ from mito_ai.streamlit_conversion.prompts.prompt_constants import search_replace_instructions
5
5
  from mito_ai.streamlit_conversion.prompts.prompt_utils import add_line_numbers_to_code
6
6
 
7
7
  def get_streamlit_error_correction_prompt(error: str, streamlit_app_code: str) -> str:
@@ -12,7 +12,7 @@ def get_streamlit_error_correction_prompt(error: str, streamlit_app_code: str) -
12
12
 
13
13
  Your job is to fix the error now. Only fix the specific error that you are instructed to fix now. Do not fix other error that that you anticipate. You will be asked to fix other errors later.
14
14
 
15
- {unified_diff_instructions}
15
+ {search_replace_instructions}
16
16
 
17
17
  ===============================================
18
18
 
@@ -21,7 +21,7 @@ EXISTING STREAMLIT APP:
21
21
 
22
22
  ===============================================
23
23
 
24
- Please create a unified diff that corrects this error. Please keep your fix concise:
24
+ Please create a search/replace block that corrects this error. Please keep your fix concise:
25
25
  {error}
26
26
 
27
27
  """
@@ -2,7 +2,7 @@
2
2
  # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
3
 
4
4
  from typing import List
5
- from mito_ai.streamlit_conversion.prompts.prompt_constants import MITO_TODO_PLACEHOLDER, unified_diff_instructions
5
+ from mito_ai.streamlit_conversion.prompts.prompt_constants import MITO_TODO_PLACEHOLDER, search_replace_instructions
6
6
  from mito_ai.streamlit_conversion.prompts.prompt_utils import add_line_numbers_to_code
7
7
 
8
8
  def get_finish_todo_prompt(notebook: List[dict], existing_streamlit_app_code: str, todo_placeholder: str) -> str:
@@ -25,7 +25,7 @@ You have ONE and ONLY ONE opportunity to complete this TODO. If you do not finis
25
25
  - If creating functions: Implement ALL required functionality
26
26
  - If converting a visualization: Copy over ALL of the visualization code from the notebook, including all styling and formatting.
27
27
 
28
- {unified_diff_instructions}
28
+ {search_replace_instructions}
29
29
 
30
30
  ===============================================
31
31
 
@@ -2,7 +2,7 @@
2
2
  # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
3
 
4
4
  from typing import List
5
- from mito_ai.streamlit_conversion.prompts.prompt_constants import unified_diff_instructions
5
+ from mito_ai.streamlit_conversion.prompts.prompt_constants import search_replace_instructions
6
6
  from mito_ai.streamlit_conversion.prompts.prompt_utils import add_line_numbers_to_code
7
7
 
8
8
  def get_update_existing_app_prompt(notebook: List[dict], streamlit_app_code: str, edit_prompt: str) -> str:
@@ -30,7 +30,7 @@ You have ONE and ONLY ONE opportunity to complete this edit request. If you do n
30
30
  - If creating functions: Implement ALL required functionality.
31
31
  - If converting a visualization: Copy over ALL of the visualization code from the notebook, including all styling and formatting.
32
32
 
33
- {unified_diff_instructions}
33
+ {search_replace_instructions}
34
34
 
35
35
  ===============================================
36
36
 
@@ -0,0 +1,94 @@
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
+ from mito_ai.utils.telemetry_utils import log
9
+
10
+
11
+ def extract_search_replace_blocks(message_content: str) -> List[Tuple[str, str]]:
12
+ """
13
+ Extract all search_replace blocks from Claude's response.
14
+
15
+ Returns:
16
+ List of tuples (search_text, replace_text) for each search/replace block
17
+ """
18
+ if "```search_replace" not in message_content:
19
+ return []
20
+
21
+ pattern = r'```search_replace\n(.*?)```'
22
+ matches = re.findall(pattern, message_content, re.DOTALL)
23
+
24
+ search_replace_pairs = []
25
+ for match in matches:
26
+ # Split by the separator
27
+ if "=======" not in match:
28
+ continue
29
+
30
+ parts = match.split("=======", 1)
31
+ if len(parts) != 2:
32
+ continue
33
+
34
+ search_part = parts[0]
35
+ replace_part = parts[1]
36
+
37
+ # Extract search text (after SEARCH marker)
38
+ if ">>>>>>> SEARCH" in search_part:
39
+ search_text = search_part.split(">>>>>>> SEARCH", 1)[1].strip()
40
+ else:
41
+ continue
42
+
43
+ # Extract replace text (before REPLACE marker)
44
+ if "<<<<<<< REPLACE" in replace_part:
45
+ replace_text = replace_part.split("<<<<<<< REPLACE", 1)[0].strip()
46
+ else:
47
+ continue
48
+
49
+ search_replace_pairs.append((search_text, replace_text))
50
+
51
+ return search_replace_pairs
52
+
53
+
54
+ def apply_search_replace(text: str, search_replace_pairs: List[Tuple[str, str]]) -> str:
55
+ """
56
+ Apply search/replace operations to the given text.
57
+
58
+ Parameters
59
+ ----------
60
+ text : str
61
+ The original file contents.
62
+ search_replace_pairs : List[Tuple[str, str]]
63
+ List of (search_text, replace_text) tuples to apply.
64
+
65
+ Returns
66
+ -------
67
+ str
68
+ The updated contents after applying all search/replace operations.
69
+
70
+ Raises
71
+ ------
72
+ ValueError
73
+ If a search text is not found or found multiple times.
74
+ """
75
+ if not search_replace_pairs:
76
+ return text
77
+
78
+ result = text
79
+
80
+ for search_text, replace_text in search_replace_pairs:
81
+ # Count occurrences of search text
82
+ count = result.count(search_text)
83
+
84
+ if count == 0:
85
+ print("Search Text Not Found: ", repr(search_text))
86
+ raise StreamlitConversionError(f"Search text not found: {repr(search_text)}", error_code=500)
87
+ elif count > 1:
88
+ print("Search Text Found Multiple Times: ", repr(search_text))
89
+ log("mito_ai_search_text_found_multiple_times", params={"search_text": repr(search_text)}, key_type="mito_server_key")
90
+
91
+ # Perform the replacement
92
+ result = result.replace(search_text, replace_text, 1)
93
+
94
+ return result
@@ -1,27 +1,20 @@
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 os
5
4
  from anthropic.types import MessageParam
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
5
+ from typing import List, cast
6
+ from mito_ai.streamlit_conversion.agent_utils import extract_todo_placeholders, get_response_from_agent
8
7
  from mito_ai.streamlit_conversion.prompts.streamlit_app_creation_prompt import get_streamlit_app_creation_prompt
9
8
  from mito_ai.streamlit_conversion.prompts.streamlit_error_correction_prompt import get_streamlit_error_correction_prompt
10
9
  from mito_ai.streamlit_conversion.prompts.streamlit_finish_todo_prompt import get_finish_todo_prompt
11
10
  from mito_ai.streamlit_conversion.prompts.update_existing_app_prompt import get_update_existing_app_prompt
12
11
  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
12
+ 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
13
+ from mito_ai.streamlit_conversion.search_replace_utils import extract_search_replace_blocks, apply_search_replace
14
14
  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
15
+ from mito_ai.utils.error_classes import StreamlitConversionError
16
+ from mito_ai.utils.telemetry_utils import log_streamlit_app_validation_retry, log_streamlit_app_conversion_success
17
+ from mito_ai.path_utils import AbsoluteNotebookPath, AppFileName, get_absolute_notebook_dir_path, get_absolute_app_path, get_app_file_name
25
18
 
26
19
  async def generate_new_streamlit_code(notebook: List[dict]) -> str:
27
20
  """Send a query to the agent, get its response and parse the code"""
@@ -57,10 +50,9 @@ async def generate_new_streamlit_code(notebook: List[dict]) -> str:
57
50
  ]
58
51
  todo_response = await get_response_from_agent(todo_messages)
59
52
 
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)
53
+ # Apply the search/replace to the streamlit app
54
+ search_replace_pairs = extract_search_replace_blocks(todo_response)
55
+ converted_code = apply_search_replace(converted_code, search_replace_pairs)
64
56
 
65
57
  return converted_code
66
58
 
@@ -80,10 +72,12 @@ async def update_existing_streamlit_code(notebook: List[dict], streamlit_app_cod
80
72
  ]
81
73
 
82
74
  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)
75
+ print(f"[Mito AI Search/Replace Tool]:\n {agent_response}")
76
+
77
+ # Apply the search/replace to the streamlit app
78
+ search_replace_pairs = extract_search_replace_blocks(agent_response)
79
+ converted_code = apply_search_replace(streamlit_app_code, search_replace_pairs)
80
+ print(f"[Mito AI Search/Replace Tool]\nConverted code\n: {converted_code}")
87
81
  return converted_code
88
82
 
89
83
 
@@ -100,27 +94,26 @@ async def correct_error_in_generation(error: str, streamlit_app_code: str) -> st
100
94
  ]
101
95
  agent_response = await get_response_from_agent(messages)
102
96
 
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)
97
+ # Apply the search/replace to the streamlit app
98
+ search_replace_pairs = extract_search_replace_blocks(agent_response)
99
+ streamlit_app_code = apply_search_replace(streamlit_app_code, search_replace_pairs)
107
100
 
108
101
  return streamlit_app_code
109
102
 
110
- async def streamlit_handler(notebook_path: str, edit_prompt: str = "") -> Tuple[bool, Optional[str], str]:
103
+ async def streamlit_handler(notebook_path: AbsoluteNotebookPath, app_file_name: AppFileName, edit_prompt: str = "") -> None:
111
104
  """Handler function for streamlit code generation and validation"""
112
105
 
113
- clean_directory_check(notebook_path)
114
-
106
+ # Convert to absolute path for consistent handling
115
107
  notebook_code = parse_jupyter_notebook_to_extract_required_content(notebook_path)
116
- app_directory = get_app_directory(notebook_path)
108
+ app_directory = get_absolute_notebook_dir_path(notebook_path)
109
+ app_path = get_absolute_app_path(app_directory, app_file_name)
117
110
 
118
111
  if edit_prompt != "":
119
112
  # If the user is editing an existing streamlit app, use the update function
120
- streamlit_code = get_app_code_from_file(app_directory)
113
+ streamlit_code = get_app_code_from_file(app_path)
121
114
 
122
115
  if streamlit_code is None:
123
- return False, '', "Error updating existing streamlit app because app.py file was not found."
116
+ raise StreamlitConversionError("Error updating existing streamlit app because app.py file was not found.", 404)
124
117
 
125
118
  streamlit_code = await update_existing_streamlit_code(notebook_code, streamlit_code, edit_prompt)
126
119
  else:
@@ -128,28 +121,24 @@ async def streamlit_handler(notebook_path: str, edit_prompt: str = "") -> Tuple[
128
121
  streamlit_code = await generate_new_streamlit_code(notebook_code)
129
122
 
130
123
  # Then, after creating/updating the app, validate that the new code runs
131
- has_validation_error, errors = validate_app(streamlit_code, notebook_path)
124
+ errors = validate_app(streamlit_code, notebook_path)
132
125
  tries = 0
133
- while has_validation_error and tries < 5:
126
+ while len(errors)>0 and tries < 5:
134
127
  for error in errors:
135
128
  streamlit_code = await correct_error_in_generation(error, streamlit_code)
136
129
 
137
- has_validation_error, errors = validate_app(streamlit_code, notebook_path)
130
+ errors = validate_app(streamlit_code, notebook_path)
138
131
 
139
- if has_validation_error:
132
+ if len(errors)>0:
140
133
  # TODO: We can't easily get the key type here, so for the beta release
141
134
  # 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)
135
+ log_streamlit_app_validation_retry('mito_server_key', MessageType.STREAMLIT_CONVERSION, errors)
143
136
  tries+=1
144
137
 
145
- 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"
138
+ if len(errors)>0:
139
+ final_errors = ', '.join(errors)
140
+ raise StreamlitConversionError(f"Streamlit agent failed generating code after max retries. Errors: {final_errors}", 500)
148
141
 
149
142
  # 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
143
+ create_app_file(app_path, streamlit_code)
144
+ log_streamlit_app_conversion_success('mito_server_key', MessageType.STREAMLIT_CONVERSION, edit_prompt)
@@ -3,9 +3,10 @@
3
3
 
4
4
  import re
5
5
  import json
6
- import os
7
- from typing import Dict, List, Optional, Tuple, Any
8
- from pathlib import Path
6
+ from typing import Dict, List, Optional, Any
7
+ from mito_ai.path_utils import AbsoluteAppPath, AbsoluteNotebookPath
8
+ from mito_ai.utils.error_classes import StreamlitConversionError
9
+
9
10
 
10
11
  def extract_code_blocks(message_content: str) -> str:
11
12
  """
@@ -28,65 +29,26 @@ def extract_code_blocks(message_content: str) -> str:
28
29
  result = '\n'.join(matches)
29
30
  return result
30
31
 
31
- def extract_unified_diff_blocks(message_content: str) -> str:
32
- """
33
- Extract all unified_diff blocks from Claude's response.
32
+ def create_app_file(app_path: AbsoluteAppPath, code: str) -> None:
34
33
  """
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]:
44
- """
45
- Create app.py file and write code to it with error handling
46
-
47
- Args:
48
- file_path (str): The actual content from the agent's response
49
- code (str): The actual content from the agent's response
50
-
51
- Returns:
52
- str: Removes the ```python``` part to be able to parse the code
53
-
34
+ Create .py file and write code to it with error handling
54
35
  """
55
36
  try:
56
- app_path = os.path.join(app_directory, "app.py")
57
-
58
37
  with open(app_path, 'w', encoding='utf-8') as f:
59
38
  f.write(code)
60
-
61
- return True, app_path, f"Successfully created {app_directory}"
62
39
  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)}"
40
+ raise StreamlitConversionError(f"Error creating app file: {str(e)}", 500)
66
41
 
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
42
+ def get_app_code_from_file(app_path: AbsoluteAppPath) -> Optional[str]:
71
43
  with open(app_path, 'r', encoding='utf-8') as f:
72
44
  return f.read()
73
-
74
45
 
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
-
84
- def parse_jupyter_notebook_to_extract_required_content(notebook_path: str) -> List[Dict[str, Any]]:
46
+ def parse_jupyter_notebook_to_extract_required_content(notebook_path: AbsoluteNotebookPath) -> List[Dict[str, Any]]:
85
47
  """
86
48
  Read a Jupyter notebook and filter cells to keep only cell_type and source fields.
87
49
 
88
50
  Args:
89
- notebook_path (str): Path to the .ipynb file (can be relative or absolute)
51
+ notebook_path: Absolute path to the .ipynb file
90
52
 
91
53
  Returns:
92
54
  dict: Filtered notebook dictionary with only cell_type and source in cells
@@ -96,10 +58,6 @@ def parse_jupyter_notebook_to_extract_required_content(notebook_path: str) -> Li
96
58
  json.JSONDecodeError: If the file is not valid JSON
97
59
  KeyError: If the notebook doesn't have the expected structure
98
60
  """
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
61
 
104
62
  try:
105
63
  # Read the notebook file
@@ -108,7 +66,7 @@ def parse_jupyter_notebook_to_extract_required_content(notebook_path: str) -> Li
108
66
 
109
67
  # Check if 'cells' key exists
110
68
  if 'cells' not in notebook_data:
111
- raise KeyError("Notebook does not contain 'cells' key")
69
+ raise StreamlitConversionError("Notebook does not contain 'cells' key", 400)
112
70
 
113
71
  # Filter each cell to keep only cell_type and source
114
72
  filtered_cells: List[Dict[str, Any]] = []
@@ -122,26 +80,6 @@ def parse_jupyter_notebook_to_extract_required_content(notebook_path: str) -> Li
122
80
  return filtered_cells
123
81
 
124
82
  except FileNotFoundError:
125
- raise FileNotFoundError(f"Notebook file not found: {notebook_path}")
83
+ raise StreamlitConversionError(f"Notebook file not found: {notebook_path}", 404)
126
84
  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}")
85
+ 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,12 +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) -> 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
-
118
- has_validation_error = len(errors) > 0
119
104
  stringified_errors = [str(error) for error in errors]
120
- return has_validation_error, stringified_errors
105
+ return stringified_errors
@@ -1,7 +1,6 @@
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
- from .manager import get_preview_manager
5
4
  from .handlers import StreamlitPreviewHandler
6
5
 
7
- __all__ = ['get_preview_manager', 'StreamlitPreviewHandler']
6
+ __all__ = ['StreamlitPreviewHandler']