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.
- mito_ai/_version.py +1 -1
- mito_ai/app_deploy/handlers.py +97 -77
- mito_ai/app_deploy/models.py +16 -12
- mito_ai/completions/models.py +4 -1
- mito_ai/completions/prompt_builders/agent_execution_prompt.py +6 -1
- mito_ai/completions/prompt_builders/agent_system_message.py +63 -4
- mito_ai/completions/prompt_builders/chat_system_message.py +4 -0
- mito_ai/completions/prompt_builders/prompt_constants.py +1 -0
- mito_ai/completions/prompt_builders/utils.py +14 -0
- mito_ai/path_utils.py +56 -0
- mito_ai/streamlit_conversion/agent_utils.py +4 -201
- mito_ai/streamlit_conversion/prompts/prompt_constants.py +142 -152
- mito_ai/streamlit_conversion/prompts/streamlit_error_correction_prompt.py +3 -3
- mito_ai/streamlit_conversion/prompts/streamlit_finish_todo_prompt.py +2 -2
- mito_ai/streamlit_conversion/prompts/update_existing_app_prompt.py +2 -2
- mito_ai/streamlit_conversion/search_replace_utils.py +93 -0
- mito_ai/streamlit_conversion/streamlit_agent_handler.py +29 -39
- mito_ai/streamlit_conversion/streamlit_utils.py +11 -64
- mito_ai/streamlit_conversion/validate_streamlit_app.py +5 -18
- mito_ai/streamlit_preview/handlers.py +44 -84
- mito_ai/streamlit_preview/manager.py +6 -6
- mito_ai/streamlit_preview/utils.py +16 -19
- mito_ai/tests/streamlit_conversion/test_apply_search_replace.py +226 -0
- mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +29 -53
- mito_ai/tests/streamlit_conversion/test_streamlit_utils.py +26 -29
- mito_ai/tests/streamlit_conversion/test_validate_streamlit_app.py +6 -3
- mito_ai/tests/streamlit_preview/test_streamlit_preview_handler.py +12 -15
- mito_ai/tests/streamlit_preview/test_streamlit_preview_manager.py +22 -26
- mito_ai/user/handlers.py +15 -3
- mito_ai/utils/create.py +17 -1
- mito_ai/utils/error_classes.py +42 -0
- mito_ai/utils/message_history_utils.py +3 -1
- mito_ai/utils/telemetry_utils.py +75 -10
- {mito_ai-0.1.46.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +1 -1
- {mito_ai-0.1.46.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/package.json +2 -2
- {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
- 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
- mito_ai-0.1.47.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.2db61d2b629817845901.js.map +1 -0
- 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
- 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
- {mito_ai-0.1.46.dist-info → mito_ai-0.1.47.dist-info}/METADATA +1 -1
- {mito_ai-0.1.46.dist-info → mito_ai-0.1.47.dist-info}/RECORD +67 -65
- mito_ai/tests/streamlit_conversion/test_apply_patch_to_text.py +0 -368
- mito_ai/tests/streamlit_conversion/test_fix_diff_headers.py +0 -533
- mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.20f12766ecd3d430568e.js.map +0 -1
- /mito_ai/streamlit_conversion/{streamlit_system_prompt.py → prompts/streamlit_system_prompt.py} +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.47.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
- {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
- {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
- {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
- {mito_ai-0.1.46.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {mito_ai-0.1.46.dist-info → mito_ai-0.1.47.dist-info}/WHEEL +0 -0
- {mito_ai-0.1.46.dist-info → mito_ai-0.1.47.dist-info}/entry_points.txt +0 -0
- {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
|
|
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,
|
|
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.
|
|
16
|
-
from mito_ai.
|
|
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
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
104
|
-
|
|
105
|
-
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
114
|
+
streamlit_code = get_app_code_from_file(app_path)
|
|
121
115
|
|
|
122
116
|
if streamlit_code is None:
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
147
|
-
|
|
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
|
-
|
|
151
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
92
|
+
raise StreamlitConversionError(f"Notebook file not found: {notebook_path}", 404)
|
|
126
93
|
except json.JSONDecodeError as e:
|
|
127
|
-
|
|
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.
|
|
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:
|
|
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 =
|
|
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:
|
|
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:
|
|
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
|
|
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.
|
|
14
|
-
from
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
107
|
-
|
|
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({
|
|
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"
|
|
75
|
+
print(f"Exception in streamlit preview handler: {e}")
|
|
119
76
|
self.set_status(500)
|
|
120
|
-
|
|
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) ->
|
|
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
|
-
|
|
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
|
|
94
|
+
return port
|
|
94
95
|
|
|
95
96
|
except Exception as e:
|
|
96
97
|
self.log.error(f"Error starting streamlit preview: {e}")
|
|
97
|
-
|
|
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"
|
|
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
|