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
|
@@ -2,44 +2,41 @@
|
|
|
2
2
|
# Distributed under the terms of the GNU Affero General Public License v3.0 License.
|
|
3
3
|
|
|
4
4
|
from typing import Tuple, Optional
|
|
5
|
-
import os
|
|
6
|
-
from mito_ai.streamlit_conversion.streamlit_utils import get_app_path
|
|
7
5
|
from mito_ai.streamlit_conversion.streamlit_agent_handler import streamlit_handler
|
|
6
|
+
from mito_ai.path_utils import AbsoluteNotebookPath, does_app_path_exists, get_absolute_app_path, get_absolute_notebook_dir_path
|
|
7
|
+
from mito_ai.utils.error_classes import StreamlitPreviewError
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
def validate_request_body(body: Optional[dict]) -> Tuple[
|
|
10
|
+
def validate_request_body(body: Optional[dict]) -> Tuple[str, bool, str]:
|
|
11
11
|
"""Validate the request body and extract notebook_path and force_recreate."""
|
|
12
12
|
if body is None:
|
|
13
|
-
|
|
13
|
+
raise StreamlitPreviewError("Invalid or missing JSON body", 400)
|
|
14
14
|
|
|
15
15
|
notebook_path = body.get("notebook_path")
|
|
16
16
|
if not notebook_path:
|
|
17
|
-
|
|
17
|
+
raise StreamlitPreviewError("Missing notebook_path parameter", 400)
|
|
18
18
|
|
|
19
19
|
force_recreate = body.get("force_recreate", False)
|
|
20
20
|
if not isinstance(force_recreate, bool):
|
|
21
|
-
|
|
21
|
+
raise StreamlitPreviewError("force_recreate must be a boolean", 400)
|
|
22
22
|
|
|
23
23
|
edit_prompt = body.get("edit_prompt", "")
|
|
24
24
|
if not isinstance(edit_prompt, str):
|
|
25
|
-
|
|
25
|
+
raise StreamlitPreviewError("edit_prompt must be a string", 400)
|
|
26
26
|
|
|
27
|
-
return
|
|
27
|
+
return notebook_path, force_recreate, edit_prompt
|
|
28
28
|
|
|
29
|
-
async def ensure_app_exists(
|
|
29
|
+
async def ensure_app_exists(absolute_notebook_path: AbsoluteNotebookPath, force_recreate: bool = False, edit_prompt: str = "") -> None:
|
|
30
30
|
"""Ensure app.py exists, generating it if necessary or if force_recreate is True."""
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
|
|
32
|
+
absolute_notebook_dir_path = get_absolute_notebook_dir_path(absolute_notebook_path)
|
|
33
|
+
absolute_app_path = get_absolute_app_path(absolute_notebook_dir_path)
|
|
34
|
+
app_path_exists = does_app_path_exists(absolute_app_path)
|
|
33
35
|
|
|
34
|
-
if
|
|
35
|
-
if
|
|
36
|
+
if not app_path_exists or force_recreate:
|
|
37
|
+
if not app_path_exists:
|
|
36
38
|
print("[Mito AI] App path not found, generating streamlit code")
|
|
37
39
|
else:
|
|
38
40
|
print("[Mito AI] Force recreating streamlit app")
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if not success or app_path is None:
|
|
43
|
-
return False, f"Failed to generate streamlit code: {message}"
|
|
44
|
-
|
|
45
|
-
return True, ""
|
|
42
|
+
await streamlit_handler(absolute_notebook_path, edit_prompt)
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# Copyright (c) Saga Inc.
|
|
2
|
+
# Distributed under the terms of the GNU Affero General Public License v3.0 License.
|
|
3
|
+
|
|
4
|
+
from mito_ai.utils.error_classes import StreamlitConversionError
|
|
5
|
+
import pytest
|
|
6
|
+
from mito_ai.streamlit_conversion.search_replace_utils import apply_search_replace
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.mark.parametrize("original_text,search_replace_pairs,expected_result", [
|
|
10
|
+
# Test case 1: Simple title change
|
|
11
|
+
(
|
|
12
|
+
"""import streamlit as st
|
|
13
|
+
|
|
14
|
+
st.markdown(\"\"\"
|
|
15
|
+
<style>
|
|
16
|
+
#MainMenu {visibility: hidden;}
|
|
17
|
+
.stAppDeployButton {display:none;}
|
|
18
|
+
footer {visibility: hidden;}
|
|
19
|
+
.stMainBlockContainer {padding: 2rem 1rem 2rem 1rem;}
|
|
20
|
+
</style>
|
|
21
|
+
\"\"\", unsafe_allow_html=True)
|
|
22
|
+
|
|
23
|
+
st.title("Simple Calculation")
|
|
24
|
+
|
|
25
|
+
x = 5
|
|
26
|
+
y = 10
|
|
27
|
+
result = x + y
|
|
28
|
+
|
|
29
|
+
st.write(f"x = {x}")
|
|
30
|
+
st.write(f"y = {y}")
|
|
31
|
+
st.write(f"x + y = {result}")""",
|
|
32
|
+
[("st.title(\"Simple Calculation\")", "st.title(\"Math Examples\")")],
|
|
33
|
+
"""import streamlit as st
|
|
34
|
+
|
|
35
|
+
st.markdown(\"\"\"
|
|
36
|
+
<style>
|
|
37
|
+
#MainMenu {visibility: hidden;}
|
|
38
|
+
.stAppDeployButton {display:none;}
|
|
39
|
+
footer {visibility: hidden;}
|
|
40
|
+
.stMainBlockContainer {padding: 2rem 1rem 2rem 1rem;}
|
|
41
|
+
</style>
|
|
42
|
+
\"\"\", unsafe_allow_html=True)
|
|
43
|
+
|
|
44
|
+
st.title("Math Examples")
|
|
45
|
+
|
|
46
|
+
x = 5
|
|
47
|
+
y = 10
|
|
48
|
+
result = x + y
|
|
49
|
+
|
|
50
|
+
st.write(f"x = {x}")
|
|
51
|
+
st.write(f"y = {y}")
|
|
52
|
+
st.write(f"x + y = {result}")"""
|
|
53
|
+
),
|
|
54
|
+
|
|
55
|
+
# Test case 2: Add new content
|
|
56
|
+
(
|
|
57
|
+
"""import streamlit as st
|
|
58
|
+
|
|
59
|
+
st.title("My App")""",
|
|
60
|
+
[("st.title(\"My App\")", """st.title("My App")
|
|
61
|
+
st.header("Welcome")
|
|
62
|
+
st.write("This is a test app")""")],
|
|
63
|
+
"""import streamlit as st
|
|
64
|
+
|
|
65
|
+
st.title("My App")
|
|
66
|
+
st.header("Welcome")
|
|
67
|
+
st.write("This is a test app")"""
|
|
68
|
+
),
|
|
69
|
+
|
|
70
|
+
# Test case 3: Remove lines
|
|
71
|
+
(
|
|
72
|
+
"""import streamlit as st
|
|
73
|
+
|
|
74
|
+
st.header("Welcome")
|
|
75
|
+
st.title("My App")
|
|
76
|
+
st.write("This is a test app")""",
|
|
77
|
+
[("""st.header("Welcome")
|
|
78
|
+
st.title("My App")
|
|
79
|
+
st.write("This is a test app")""", "st.title(\"My App\")")],
|
|
80
|
+
"""import streamlit as st
|
|
81
|
+
|
|
82
|
+
st.title("My App")"""
|
|
83
|
+
),
|
|
84
|
+
|
|
85
|
+
# Test case 4: Multiple replacements
|
|
86
|
+
(
|
|
87
|
+
"""import streamlit as st
|
|
88
|
+
|
|
89
|
+
st.title("Old Title")
|
|
90
|
+
x = 5
|
|
91
|
+
y = 10
|
|
92
|
+
st.write("Old message")""",
|
|
93
|
+
[
|
|
94
|
+
("st.title(\"Old Title\")", "st.title(\"New Title\")"),
|
|
95
|
+
("st.write(\"Old message\")", "st.write(\"New message\")")
|
|
96
|
+
],
|
|
97
|
+
"""import streamlit as st
|
|
98
|
+
|
|
99
|
+
st.title("New Title")
|
|
100
|
+
x = 5
|
|
101
|
+
y = 10
|
|
102
|
+
st.write("New message")"""
|
|
103
|
+
),
|
|
104
|
+
|
|
105
|
+
# Test case 5: Empty search/replace pairs
|
|
106
|
+
(
|
|
107
|
+
"""import streamlit as st
|
|
108
|
+
|
|
109
|
+
st.title("My App")""",
|
|
110
|
+
[],
|
|
111
|
+
"""import streamlit as st
|
|
112
|
+
|
|
113
|
+
st.title("My App")"""
|
|
114
|
+
),
|
|
115
|
+
|
|
116
|
+
# Test case 6: Complex replacement with context
|
|
117
|
+
(
|
|
118
|
+
"""import streamlit as st
|
|
119
|
+
|
|
120
|
+
# This is a comment
|
|
121
|
+
st.title("Old Title")
|
|
122
|
+
# Another comment
|
|
123
|
+
x = 5
|
|
124
|
+
y = 10
|
|
125
|
+
# Final comment""",
|
|
126
|
+
[("""# This is a comment
|
|
127
|
+
st.title("Old Title")
|
|
128
|
+
# Another comment""", """# This is a comment
|
|
129
|
+
st.title("New Title")
|
|
130
|
+
# Another comment""")],
|
|
131
|
+
"""import streamlit as st
|
|
132
|
+
|
|
133
|
+
# This is a comment
|
|
134
|
+
st.title("New Title")
|
|
135
|
+
# Another comment
|
|
136
|
+
x = 5
|
|
137
|
+
y = 10
|
|
138
|
+
# Final comment"""
|
|
139
|
+
),
|
|
140
|
+
|
|
141
|
+
# Test case 7: Replace multiple consecutive lines
|
|
142
|
+
(
|
|
143
|
+
"""import streamlit as st
|
|
144
|
+
|
|
145
|
+
st.title("My App")
|
|
146
|
+
st.write("Line 1")
|
|
147
|
+
st.write("Line 2")
|
|
148
|
+
st.write("Line 3")
|
|
149
|
+
|
|
150
|
+
x = 5""",
|
|
151
|
+
[("""st.write("Line 1")
|
|
152
|
+
st.write("Line 2")
|
|
153
|
+
st.write("Line 3")""", "st.write(\"New content\")")],
|
|
154
|
+
"""import streamlit as st
|
|
155
|
+
|
|
156
|
+
st.title("My App")
|
|
157
|
+
st.write("New content")
|
|
158
|
+
|
|
159
|
+
x = 5"""
|
|
160
|
+
),
|
|
161
|
+
|
|
162
|
+
# Test case 8: Add lines at the beginning
|
|
163
|
+
(
|
|
164
|
+
"""import streamlit as st
|
|
165
|
+
|
|
166
|
+
st.title("My App")""",
|
|
167
|
+
[("import streamlit as st", """import pandas as pd
|
|
168
|
+
import streamlit as st""")],
|
|
169
|
+
"""import pandas as pd
|
|
170
|
+
import streamlit as st
|
|
171
|
+
|
|
172
|
+
st.title("My App")"""
|
|
173
|
+
),
|
|
174
|
+
|
|
175
|
+
# Test case 9: Add lines at the end
|
|
176
|
+
(
|
|
177
|
+
"""import streamlit as st
|
|
178
|
+
|
|
179
|
+
st.title("My App")""",
|
|
180
|
+
[("st.title(\"My App\")", """st.title("My App")
|
|
181
|
+
|
|
182
|
+
st.write("Footer content")
|
|
183
|
+
st.write("More footer")""")],
|
|
184
|
+
"""import streamlit as st
|
|
185
|
+
|
|
186
|
+
st.title("My App")
|
|
187
|
+
|
|
188
|
+
st.write("Footer content")
|
|
189
|
+
st.write("More footer")"""
|
|
190
|
+
),
|
|
191
|
+
|
|
192
|
+
# Test case 10: Add emoji to streamlit app title
|
|
193
|
+
(
|
|
194
|
+
"""import streamlit as st
|
|
195
|
+
|
|
196
|
+
st.title("My App")
|
|
197
|
+
st.write("Welcome to my application")""",
|
|
198
|
+
[("st.title(\"My App\")", "st.title(\"🚀 My App\")")],
|
|
199
|
+
"""import streamlit as st
|
|
200
|
+
|
|
201
|
+
st.title("🚀 My App")
|
|
202
|
+
st.write("Welcome to my application")"""
|
|
203
|
+
)
|
|
204
|
+
])
|
|
205
|
+
def test_apply_search_replace(original_text, search_replace_pairs, expected_result):
|
|
206
|
+
"""Test the apply_search_replace function with various search/replace scenarios."""
|
|
207
|
+
result = apply_search_replace(original_text, search_replace_pairs)
|
|
208
|
+
|
|
209
|
+
print(f"Original text: {repr(original_text)}")
|
|
210
|
+
print(f"Search/replace pairs: {search_replace_pairs}")
|
|
211
|
+
print(f"Expected result: {repr(expected_result)}")
|
|
212
|
+
print(f"Result: {repr(result)}")
|
|
213
|
+
|
|
214
|
+
assert result == expected_result
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def test_apply_search_replace_search_not_found():
|
|
218
|
+
"""Test that ValueError is raised when search text is not found."""
|
|
219
|
+
with pytest.raises(StreamlitConversionError, match="Search text not found"):
|
|
220
|
+
apply_search_replace("st.title(\"My App\")", [("st.title(\"Not Found\")", "st.title(\"New Title\")")])
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def test_apply_search_replace_multiple_matches():
|
|
224
|
+
"""Test that ValueError is raised when search text is found multiple times."""
|
|
225
|
+
with pytest.raises(StreamlitConversionError, match="Search text found 2 times"):
|
|
226
|
+
apply_search_replace("st.write(\"Hello\")\nst.write(\"Hello\")", [("st.write(\"Hello\")", "st.write(\"Hi\")")])
|
|
@@ -12,7 +12,7 @@ from mito_ai.streamlit_conversion.streamlit_agent_handler import (
|
|
|
12
12
|
correct_error_in_generation,
|
|
13
13
|
streamlit_handler
|
|
14
14
|
)
|
|
15
|
-
from mito_ai.
|
|
15
|
+
from mito_ai.path_utils import AbsoluteAppPath, AbsoluteNotebookPath, get_absolute_app_path, get_absolute_notebook_dir_path, get_absolute_notebook_path
|
|
16
16
|
|
|
17
17
|
# Add this line to enable async support
|
|
18
18
|
pytest_plugins = ('pytest_asyncio',)
|
|
@@ -102,14 +102,12 @@ class TestCorrectErrorInGeneration:
|
|
|
102
102
|
@patch('mito_ai.streamlit_conversion.agent_utils.stream_anthropic_completion_from_mito_server')
|
|
103
103
|
async def test_correct_error_in_generation_success(self, mock_stream):
|
|
104
104
|
"""Test successful error correction"""
|
|
105
|
-
mock_response = """```
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
+import streamlit
|
|
112
|
-
+st.title('Fixed')
|
|
105
|
+
mock_response = """```search_replace
|
|
106
|
+
>>>>>>> SEARCH
|
|
107
|
+
st.title('Test')
|
|
108
|
+
=======
|
|
109
|
+
st.title('Fixed')
|
|
110
|
+
<<<<<<< REPLACE
|
|
113
111
|
```"""
|
|
114
112
|
async def mock_async_gen():
|
|
115
113
|
for item in [mock_response]:
|
|
@@ -117,8 +115,8 @@ class TestCorrectErrorInGeneration:
|
|
|
117
115
|
|
|
118
116
|
mock_stream.return_value = mock_async_gen()
|
|
119
117
|
|
|
120
|
-
result = await correct_error_in_generation("ImportError: No module named 'pandas'", "import streamlit\nst.title('Test')")
|
|
121
|
-
|
|
118
|
+
result = await correct_error_in_generation("ImportError: No module named 'pandas'", "import streamlit\nst.title('Test')\n")
|
|
119
|
+
|
|
122
120
|
expected_code = "import streamlit\nst.title('Fixed')\n"
|
|
123
121
|
assert result == expected_code
|
|
124
122
|
|
|
@@ -140,8 +138,7 @@ class TestStreamlitHandler:
|
|
|
140
138
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.generate_new_streamlit_code')
|
|
141
139
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.validate_app')
|
|
142
140
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.create_app_file')
|
|
143
|
-
|
|
144
|
-
async def test_streamlit_handler_success(self, mock_clean_directory, mock_create_file, mock_validator, mock_generate_code, mock_parse):
|
|
141
|
+
async def test_streamlit_handler_success(self, mock_create_file, mock_validator, mock_generate_code, mock_parse):
|
|
145
142
|
"""Test successful streamlit handler execution"""
|
|
146
143
|
# Mock notebook parsing
|
|
147
144
|
mock_notebook_data: List[dict] = [{"cells": [{"cell_type": "code", "source": ["import pandas"]}]}]
|
|
@@ -153,26 +150,19 @@ class TestStreamlitHandler:
|
|
|
153
150
|
# Mock validation (no errors)
|
|
154
151
|
mock_validator.return_value = (False, "")
|
|
155
152
|
|
|
156
|
-
# Mock file creation
|
|
157
|
-
mock_create_file.return_value = (True, "/path/to/app.py", "File created successfully")
|
|
158
|
-
|
|
159
|
-
# Mock clean directory check (no-op)
|
|
160
|
-
mock_clean_directory.return_value = None
|
|
161
|
-
|
|
162
153
|
# Use a relative path that will work cross-platform
|
|
163
|
-
notebook_path = "notebook.ipynb"
|
|
164
|
-
result = await streamlit_handler(notebook_path)
|
|
154
|
+
notebook_path = AbsoluteNotebookPath("absolute/path/to/notebook.ipynb")
|
|
165
155
|
|
|
166
|
-
|
|
167
|
-
|
|
156
|
+
# Construct the expected app path using the same method as the production code
|
|
157
|
+
app_directory = get_absolute_notebook_dir_path(notebook_path)
|
|
158
|
+
expected_app_path = get_absolute_app_path(app_directory)
|
|
159
|
+
await streamlit_handler(notebook_path)
|
|
168
160
|
|
|
169
161
|
# Verify calls
|
|
170
162
|
mock_parse.assert_called_once_with(notebook_path)
|
|
171
163
|
mock_generate_code.assert_called_once_with(mock_notebook_data)
|
|
172
164
|
mock_validator.assert_called_once_with("import streamlit\nst.title('Test')", notebook_path)
|
|
173
|
-
|
|
174
|
-
expected_app_dir = os.path.dirname(os.path.abspath(notebook_path))
|
|
175
|
-
mock_create_file.assert_called_once_with(expected_app_dir, "import streamlit\nst.title('Test')")
|
|
165
|
+
mock_create_file.assert_called_once_with(expected_app_path, "import streamlit\nst.title('Test')")
|
|
176
166
|
|
|
177
167
|
@pytest.mark.asyncio
|
|
178
168
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
|
|
@@ -192,11 +182,9 @@ class TestStreamlitHandler:
|
|
|
192
182
|
# Mock validation (always errors) - Return list of errors as expected by validate_app
|
|
193
183
|
mock_validator.return_value = (True, ["Persistent error"])
|
|
194
184
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
assert result[0] is False
|
|
199
|
-
assert "Error generating streamlit code by agent" in result[2]
|
|
185
|
+
# Now it should raise an exception instead of returning a tuple
|
|
186
|
+
with pytest.raises(Exception):
|
|
187
|
+
await streamlit_handler(AbsoluteNotebookPath("notebook.ipynb"))
|
|
200
188
|
|
|
201
189
|
# Verify that error correction was called 5 times (max retries)
|
|
202
190
|
assert mock_correct_error.call_count == 5
|
|
@@ -206,8 +194,7 @@ class TestStreamlitHandler:
|
|
|
206
194
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.generate_new_streamlit_code')
|
|
207
195
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.validate_app')
|
|
208
196
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.create_app_file')
|
|
209
|
-
|
|
210
|
-
async def test_streamlit_handler_file_creation_failure(self, mock_clean_directory, mock_create_file, mock_validator, mock_generate_code, mock_parse):
|
|
197
|
+
async def test_streamlit_handler_file_creation_failure(self, mock_create_file, mock_validator, mock_generate_code, mock_parse):
|
|
211
198
|
"""Test streamlit handler when file creation fails"""
|
|
212
199
|
# Mock notebook parsing
|
|
213
200
|
mock_notebook_data: List[dict] = [{"cells": []}]
|
|
@@ -219,35 +206,27 @@ class TestStreamlitHandler:
|
|
|
219
206
|
# Mock validation (no errors)
|
|
220
207
|
mock_validator.return_value = (False, "")
|
|
221
208
|
|
|
222
|
-
# Mock file creation failure
|
|
223
|
-
mock_create_file.
|
|
224
|
-
|
|
225
|
-
# Mock clean directory check (no-op)
|
|
226
|
-
mock_clean_directory.return_value = None
|
|
209
|
+
# Mock file creation failure - now it should raise an exception
|
|
210
|
+
mock_create_file.side_effect = Exception("Permission denied")
|
|
227
211
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
assert "Permission denied" in result[2]
|
|
212
|
+
# Now it should raise an exception instead of returning a tuple
|
|
213
|
+
with pytest.raises(Exception, match="Permission denied"):
|
|
214
|
+
await streamlit_handler(AbsoluteNotebookPath("notebook.ipynb"))
|
|
232
215
|
|
|
233
216
|
@pytest.mark.asyncio
|
|
234
217
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
|
|
235
|
-
|
|
236
|
-
async def test_streamlit_handler_parse_notebook_exception(self, mock_clean_directory, mock_parse):
|
|
218
|
+
async def test_streamlit_handler_parse_notebook_exception(self, mock_parse):
|
|
237
219
|
"""Test streamlit handler when notebook parsing fails"""
|
|
238
|
-
# Mock clean directory check (no-op)
|
|
239
|
-
mock_clean_directory.return_value = None
|
|
240
220
|
|
|
241
221
|
mock_parse.side_effect = FileNotFoundError("Notebook not found")
|
|
242
222
|
|
|
243
223
|
with pytest.raises(FileNotFoundError, match="Notebook not found"):
|
|
244
|
-
await streamlit_handler("notebook.ipynb")
|
|
224
|
+
await streamlit_handler(AbsoluteNotebookPath("notebook.ipynb"))
|
|
245
225
|
|
|
246
226
|
@pytest.mark.asyncio
|
|
247
227
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
|
|
248
228
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.generate_new_streamlit_code')
|
|
249
|
-
|
|
250
|
-
async def test_streamlit_handler_generation_exception(self, mock_clean_directory, mock_generate_code, mock_parse):
|
|
229
|
+
async def test_streamlit_handler_generation_exception(self, mock_generate_code, mock_parse):
|
|
251
230
|
"""Test streamlit handler when code generation fails"""
|
|
252
231
|
# Mock notebook parsing
|
|
253
232
|
mock_notebook_data: List[dict] = [{"cells": []}]
|
|
@@ -256,11 +235,8 @@ class TestStreamlitHandler:
|
|
|
256
235
|
# Mock code generation failure
|
|
257
236
|
mock_generate_code.side_effect = Exception("Generation failed")
|
|
258
237
|
|
|
259
|
-
# Mock clean directory check (no-op)
|
|
260
|
-
mock_clean_directory.return_value = None
|
|
261
|
-
|
|
262
238
|
with pytest.raises(Exception, match="Generation failed"):
|
|
263
|
-
await streamlit_handler("notebook.ipynb")
|
|
239
|
+
await streamlit_handler(AbsoluteNotebookPath("notebook.ipynb"))
|
|
264
240
|
|
|
265
241
|
|
|
266
242
|
|
|
@@ -11,6 +11,7 @@ from mito_ai.streamlit_conversion.streamlit_utils import (
|
|
|
11
11
|
create_app_file,
|
|
12
12
|
parse_jupyter_notebook_to_extract_required_content
|
|
13
13
|
)
|
|
14
|
+
from mito_ai.path_utils import AbsoluteAppPath, AbsoluteNotebookDirPath, AbsoluteNotebookPath, get_absolute_notebook_path
|
|
14
15
|
from typing import Dict, Any
|
|
15
16
|
|
|
16
17
|
class TestExtractCodeBlocks:
|
|
@@ -48,55 +49,47 @@ class TestCreateAppFile:
|
|
|
48
49
|
|
|
49
50
|
def test_create_app_file_success(self, tmp_path):
|
|
50
51
|
"""Test successful file creation"""
|
|
51
|
-
|
|
52
|
+
app_path = os.path.join(str(tmp_path), "app.py")
|
|
52
53
|
code = "import streamlit\nst.title('Test')"
|
|
53
54
|
|
|
54
|
-
|
|
55
|
+
create_app_file(AbsoluteAppPath(app_path), code)
|
|
55
56
|
|
|
56
|
-
assert
|
|
57
|
-
assert
|
|
57
|
+
assert app_path is not None
|
|
58
|
+
assert os.path.exists(app_path)
|
|
58
59
|
|
|
59
60
|
# Verify file was created with correct content
|
|
60
|
-
|
|
61
|
-
assert os.path.exists(app_file_path)
|
|
62
|
-
|
|
63
|
-
with open(app_file_path, 'r') as f:
|
|
61
|
+
with open(app_path, 'r') as f:
|
|
64
62
|
content = f.read()
|
|
65
63
|
assert content == code
|
|
66
64
|
|
|
67
65
|
def test_create_app_file_io_error(self):
|
|
68
66
|
"""Test file creation with IO error"""
|
|
69
|
-
file_path = "/nonexistent/path/that/should/fail"
|
|
67
|
+
file_path = AbsoluteAppPath("/nonexistent/path/that/should/fail")
|
|
70
68
|
code = "import streamlit"
|
|
71
69
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
assert success is False
|
|
75
|
-
assert "Error creating file" in message
|
|
70
|
+
with pytest.raises(Exception):
|
|
71
|
+
create_app_file(file_path, code)
|
|
76
72
|
|
|
77
73
|
@patch('builtins.open', side_effect=Exception("Unexpected error"))
|
|
78
74
|
def test_create_app_file_unexpected_error(self, mock_open):
|
|
79
75
|
"""Test file creation with unexpected error"""
|
|
80
|
-
|
|
76
|
+
app_path = AbsoluteAppPath("/tmp/test")
|
|
81
77
|
code = "import streamlit"
|
|
82
78
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
assert success is False
|
|
86
|
-
assert "Unexpected error" in message
|
|
79
|
+
with pytest.raises(Exception, match="Unexpected error"):
|
|
80
|
+
create_app_file(app_path, code)
|
|
87
81
|
|
|
88
82
|
def test_create_app_file_empty_code(self, tmp_path):
|
|
89
83
|
"""Test creating file with empty code"""
|
|
90
|
-
|
|
84
|
+
app_path = AbsoluteAppPath(os.path.join(str(tmp_path), "app.py"))
|
|
91
85
|
code = ""
|
|
92
86
|
|
|
93
|
-
|
|
87
|
+
create_app_file(app_path, code)
|
|
94
88
|
|
|
95
|
-
assert
|
|
96
|
-
assert
|
|
89
|
+
assert app_path is not None
|
|
90
|
+
assert os.path.exists(app_path)
|
|
97
91
|
|
|
98
|
-
|
|
99
|
-
with open(app_file_path, 'r') as f:
|
|
92
|
+
with open(app_path, 'r') as f:
|
|
100
93
|
content = f.read()
|
|
101
94
|
assert content == ""
|
|
102
95
|
|
|
@@ -128,7 +121,8 @@ class TestParseJupyterNotebookToExtractRequiredContent:
|
|
|
128
121
|
with open(notebook_path, 'w') as f:
|
|
129
122
|
json.dump(notebook_data, f)
|
|
130
123
|
|
|
131
|
-
|
|
124
|
+
absolute_path = get_absolute_notebook_path(str(notebook_path))
|
|
125
|
+
result = parse_jupyter_notebook_to_extract_required_content(absolute_path)
|
|
132
126
|
|
|
133
127
|
# Check that only cell_type and source are preserved
|
|
134
128
|
assert len(result) == 2
|
|
@@ -143,8 +137,9 @@ class TestParseJupyterNotebookToExtractRequiredContent:
|
|
|
143
137
|
|
|
144
138
|
def test_parse_notebook_file_not_found(self):
|
|
145
139
|
"""Test parsing non-existent notebook file"""
|
|
146
|
-
|
|
147
|
-
|
|
140
|
+
from mito_ai.utils.error_classes import StreamlitConversionError
|
|
141
|
+
with pytest.raises(StreamlitConversionError, match="Notebook file not found"):
|
|
142
|
+
parse_jupyter_notebook_to_extract_required_content(AbsoluteNotebookPath("/nonexistent/path/notebook.ipynb"))
|
|
148
143
|
|
|
149
144
|
def test_parse_notebook_with_missing_cell_fields(self, tmp_path):
|
|
150
145
|
"""Test parsing notebook where cells are missing cell_type or source"""
|
|
@@ -169,7 +164,8 @@ class TestParseJupyterNotebookToExtractRequiredContent:
|
|
|
169
164
|
with open(notebook_path, 'w') as f:
|
|
170
165
|
json.dump(notebook_data, f)
|
|
171
166
|
|
|
172
|
-
|
|
167
|
+
absolute_path = get_absolute_notebook_path(str(notebook_path))
|
|
168
|
+
result = parse_jupyter_notebook_to_extract_required_content(absolute_path)
|
|
173
169
|
|
|
174
170
|
assert len(result) == 3
|
|
175
171
|
assert result[0]['cell_type'] == 'code'
|
|
@@ -191,6 +187,7 @@ class TestParseJupyterNotebookToExtractRequiredContent:
|
|
|
191
187
|
with open(notebook_path, 'w') as f:
|
|
192
188
|
json.dump(notebook_data, f)
|
|
193
189
|
|
|
194
|
-
|
|
190
|
+
absolute_path = get_absolute_notebook_path(str(notebook_path))
|
|
191
|
+
result = parse_jupyter_notebook_to_extract_required_content(absolute_path)
|
|
195
192
|
|
|
196
193
|
assert result == []
|
|
@@ -11,6 +11,7 @@ from mito_ai.streamlit_conversion.validate_streamlit_app import (
|
|
|
11
11
|
validate_app
|
|
12
12
|
)
|
|
13
13
|
import pytest
|
|
14
|
+
from mito_ai.path_utils import AbsoluteNotebookPath
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
class TestGetSyntaxError:
|
|
@@ -56,7 +57,9 @@ class TestGetRuntimeErrors:
|
|
|
56
57
|
])
|
|
57
58
|
def test_get_runtime_errors(self, app_code, expected_error):
|
|
58
59
|
"""Test getting runtime errors"""
|
|
59
|
-
|
|
60
|
+
|
|
61
|
+
absolute_path = AbsoluteNotebookPath('/notebook.ipynb')
|
|
62
|
+
errors = get_runtime_errors(app_code, absolute_path)
|
|
60
63
|
|
|
61
64
|
if expected_error is None:
|
|
62
65
|
assert errors is None
|
|
@@ -85,7 +88,7 @@ df=pd.read_csv('data.csv')
|
|
|
85
88
|
with open(csv_path, "w") as f:
|
|
86
89
|
f.write("name,age\nJohn,25\nJane,30")
|
|
87
90
|
|
|
88
|
-
errors = get_runtime_errors(app_code, app_path)
|
|
91
|
+
errors = get_runtime_errors(app_code, AbsoluteNotebookPath(app_path))
|
|
89
92
|
assert errors is None
|
|
90
93
|
|
|
91
94
|
class TestValidateApp:
|
|
@@ -99,7 +102,7 @@ class TestValidateApp:
|
|
|
99
102
|
])
|
|
100
103
|
def test_validate_app(self, app_code, expected_has_validation_error, expected_error_message):
|
|
101
104
|
"""Test the validate_app function"""
|
|
102
|
-
has_validation_error, errors = validate_app(app_code, '/
|
|
105
|
+
has_validation_error, errors = validate_app(app_code, AbsoluteNotebookPath('/notebook.ipynb'))
|
|
103
106
|
|
|
104
107
|
assert has_validation_error == expected_has_validation_error
|
|
105
108
|
assert expected_error_message in str(errors)
|