mito-ai 0.1.46__py3-none-any.whl → 0.1.48__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 +98 -77
- mito_ai/app_deploy/models.py +16 -12
- mito_ai/completions/models.py +5 -1
- mito_ai/completions/prompt_builders/agent_execution_prompt.py +10 -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 +94 -0
- mito_ai/streamlit_conversion/streamlit_agent_handler.py +35 -46
- mito_ai/streamlit_conversion/streamlit_utils.py +12 -66
- mito_ai/streamlit_conversion/validate_streamlit_app.py +6 -21
- mito_ai/streamlit_preview/__init__.py +1 -2
- mito_ai/streamlit_preview/handlers.py +53 -85
- mito_ai/streamlit_preview/manager.py +7 -16
- mito_ai/streamlit_preview/utils.py +8 -28
- mito_ai/tests/message_history/test_message_history_utils.py +1 -0
- mito_ai/tests/streamlit_conversion/test_apply_search_replace.py +240 -0
- mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +39 -60
- mito_ai/tests/streamlit_conversion/test_streamlit_utils.py +26 -29
- mito_ai/tests/streamlit_conversion/test_validate_streamlit_app.py +25 -20
- mito_ai/tests/streamlit_preview/test_streamlit_preview_handler.py +81 -56
- mito_ai/tests/streamlit_preview/test_streamlit_preview_manager.py +24 -37
- 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 +78 -13
- {mito_ai-0.1.46.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +100 -100
- {mito_ai-0.1.46.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/package.json +2 -2
- {mito_ai-0.1.46.data → mito_ai-0.1.48.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.48.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.5c7d84a45ddeb5704b61.js +1515 -449
- mito_ai-0.1.48.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.5c7d84a45ddeb5704b61.js.map +1 -0
- mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.54126ab6511271265443.js → mito_ai-0.1.48.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.045d65d1de6fde3f3b72.js +18 -18
- mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.54126ab6511271265443.js.map → mito_ai-0.1.48.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.045d65d1de6fde3f3b72.js.map +1 -1
- {mito_ai-0.1.46.dist-info → mito_ai-0.1.48.dist-info}/METADATA +1 -1
- {mito_ai-0.1.46.dist-info → mito_ai-0.1.48.dist-info}/RECORD +69 -67
- 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.48.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.48.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.48.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.48.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.48.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.48.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.48.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.48.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.48.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.48.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.48.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.48.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.48.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.48.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.48.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.48.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.48.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.48.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.48.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.48.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.48.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.48.dist-info}/WHEEL +0 -0
- {mito_ai-0.1.46.dist-info → mito_ai-0.1.48.dist-info}/entry_points.txt +0 -0
- {mito_ai-0.1.46.dist-info → mito_ai-0.1.48.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,16 +1,15 @@
|
|
|
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
|
import socket
|
|
6
5
|
import subprocess
|
|
7
|
-
import tempfile
|
|
8
6
|
import time
|
|
9
7
|
import threading
|
|
10
8
|
import requests
|
|
11
9
|
from typing import Dict, Optional, Tuple
|
|
12
10
|
from dataclasses import dataclass
|
|
13
11
|
from mito_ai.logger import get_logger
|
|
12
|
+
from mito_ai.utils.error_classes import StreamlitPreviewError
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
@dataclass
|
|
@@ -37,7 +36,7 @@ class StreamlitPreviewManager:
|
|
|
37
36
|
|
|
38
37
|
return port
|
|
39
38
|
|
|
40
|
-
def start_streamlit_preview(self, app_directory: str, preview_id: str) ->
|
|
39
|
+
def start_streamlit_preview(self, app_directory: str, preview_id: str) -> int:
|
|
41
40
|
"""Start a streamlit preview process.
|
|
42
41
|
|
|
43
42
|
Args:
|
|
@@ -80,7 +79,7 @@ class StreamlitPreviewManager:
|
|
|
80
79
|
if not ready:
|
|
81
80
|
proc.terminate()
|
|
82
81
|
proc.wait()
|
|
83
|
-
|
|
82
|
+
raise StreamlitPreviewError("Streamlit app failed to start as app is not ready", 500)
|
|
84
83
|
|
|
85
84
|
# Register the process
|
|
86
85
|
with self._lock:
|
|
@@ -90,11 +89,11 @@ class StreamlitPreviewManager:
|
|
|
90
89
|
)
|
|
91
90
|
|
|
92
91
|
self.log.info(f"Started streamlit preview {preview_id} on port {port}")
|
|
93
|
-
return
|
|
92
|
+
return port
|
|
94
93
|
|
|
95
94
|
except Exception as e:
|
|
96
95
|
self.log.error(f"Error starting streamlit preview: {e}")
|
|
97
|
-
|
|
96
|
+
raise StreamlitPreviewError(f"Failed to start preview: {str(e)}", 500)
|
|
98
97
|
|
|
99
98
|
def _wait_for_app_ready(self, port: int, timeout: int = 30) -> bool:
|
|
100
99
|
"""Wait for streamlit app to be ready on the given port."""
|
|
@@ -106,7 +105,7 @@ class StreamlitPreviewManager:
|
|
|
106
105
|
if response.status_code == 200:
|
|
107
106
|
return True
|
|
108
107
|
except requests.exceptions.RequestException as e:
|
|
109
|
-
|
|
108
|
+
self.log.info(f"Waiting for app to be ready...")
|
|
110
109
|
pass
|
|
111
110
|
|
|
112
111
|
time.sleep(1)
|
|
@@ -122,7 +121,7 @@ class StreamlitPreviewManager:
|
|
|
122
121
|
Returns:
|
|
123
122
|
True if stopped successfully, False if not found
|
|
124
123
|
"""
|
|
125
|
-
|
|
124
|
+
self.log.info(f"Stopping preview {preview_id}")
|
|
126
125
|
with self._lock:
|
|
127
126
|
if preview_id not in self._previews:
|
|
128
127
|
return False
|
|
@@ -149,11 +148,3 @@ class StreamlitPreviewManager:
|
|
|
149
148
|
"""Get a preview process by ID."""
|
|
150
149
|
with self._lock:
|
|
151
150
|
return self._previews.get(preview_id)
|
|
152
|
-
|
|
153
|
-
# Global instance
|
|
154
|
-
_preview_manager = StreamlitPreviewManager()
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
def get_preview_manager() -> StreamlitPreviewManager:
|
|
158
|
-
"""Get the global preview manager instance."""
|
|
159
|
-
return _preview_manager
|
|
@@ -2,44 +2,24 @@
|
|
|
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
|
|
6
|
-
from mito_ai.streamlit_conversion.streamlit_utils import get_app_path
|
|
7
|
-
from mito_ai.streamlit_conversion.streamlit_agent_handler import streamlit_handler
|
|
5
|
+
from mito_ai.utils.error_classes import StreamlitPreviewError
|
|
8
6
|
|
|
9
7
|
|
|
10
|
-
def validate_request_body(body: Optional[dict]) -> Tuple[
|
|
8
|
+
def validate_request_body(body: Optional[dict]) -> Tuple[str, bool, str]:
|
|
11
9
|
"""Validate the request body and extract notebook_path and force_recreate."""
|
|
12
10
|
if body is None:
|
|
13
|
-
|
|
11
|
+
raise StreamlitPreviewError("Invalid or missing JSON body", 400)
|
|
14
12
|
|
|
15
13
|
notebook_path = body.get("notebook_path")
|
|
16
14
|
if not notebook_path:
|
|
17
|
-
|
|
15
|
+
raise StreamlitPreviewError("Missing notebook_path parameter", 400)
|
|
18
16
|
|
|
19
17
|
force_recreate = body.get("force_recreate", False)
|
|
20
18
|
if not isinstance(force_recreate, bool):
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
raise StreamlitPreviewError("force_recreate must be a boolean", 400)
|
|
20
|
+
|
|
23
21
|
edit_prompt = body.get("edit_prompt", "")
|
|
24
22
|
if not isinstance(edit_prompt, str):
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return True, "", notebook_path, force_recreate, edit_prompt
|
|
28
|
-
|
|
29
|
-
async def ensure_app_exists(resolved_notebook_path: str, force_recreate: bool = False, edit_prompt: str = "") -> Tuple[bool, str]:
|
|
30
|
-
"""Ensure app.py exists, generating it if necessary or if force_recreate is True."""
|
|
31
|
-
# Check if the app already exists
|
|
32
|
-
app_path = get_app_path(os.path.dirname(resolved_notebook_path))
|
|
33
|
-
|
|
34
|
-
if app_path is None or force_recreate:
|
|
35
|
-
if app_path is None:
|
|
36
|
-
print("[Mito AI] App path not found, generating streamlit code")
|
|
37
|
-
else:
|
|
38
|
-
print("[Mito AI] Force recreating streamlit app")
|
|
39
|
-
|
|
40
|
-
success, app_path, message = await streamlit_handler(resolved_notebook_path, edit_prompt)
|
|
41
|
-
|
|
42
|
-
if not success or app_path is None:
|
|
43
|
-
return False, f"Failed to generate streamlit code: {message}"
|
|
23
|
+
raise StreamlitPreviewError("edit_prompt must be a string", 400)
|
|
44
24
|
|
|
45
|
-
return
|
|
25
|
+
return notebook_path, force_recreate, edit_prompt
|
|
@@ -0,0 +1,240 @@
|
|
|
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
|
+
# Test case 11: Only replace first occurrence when search text exists multiple times
|
|
206
|
+
(
|
|
207
|
+
"""import streamlit as st
|
|
208
|
+
|
|
209
|
+
st.write("Hello World")
|
|
210
|
+
st.title("My App")
|
|
211
|
+
st.write("Hello World")
|
|
212
|
+
st.write("Another message")""",
|
|
213
|
+
[("st.write(\"Hello World\")", "st.write(\"Hi There\")")],
|
|
214
|
+
"""import streamlit as st
|
|
215
|
+
|
|
216
|
+
st.write("Hi There")
|
|
217
|
+
st.title("My App")
|
|
218
|
+
st.write("Hello World")
|
|
219
|
+
st.write("Another message")"""
|
|
220
|
+
)
|
|
221
|
+
])
|
|
222
|
+
def test_apply_search_replace(original_text, search_replace_pairs, expected_result):
|
|
223
|
+
"""Test the apply_search_replace function with various search/replace scenarios."""
|
|
224
|
+
result = apply_search_replace(original_text, search_replace_pairs)
|
|
225
|
+
|
|
226
|
+
print(f"Original text: {repr(original_text)}")
|
|
227
|
+
print(f"Search/replace pairs: {search_replace_pairs}")
|
|
228
|
+
print(f"Expected result: {repr(expected_result)}")
|
|
229
|
+
print(f"Result: {repr(result)}")
|
|
230
|
+
|
|
231
|
+
assert result == expected_result
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def test_apply_search_replace_search_not_found():
|
|
235
|
+
"""Test that ValueError is raised when search text is not found."""
|
|
236
|
+
with pytest.raises(StreamlitConversionError, match="Search text not found"):
|
|
237
|
+
apply_search_replace("st.title(\"My App\")", [("st.title(\"Not Found\")", "st.title(\"New Title\")")])
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
|
|
@@ -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,8 @@ 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
|
-
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.
|
|
144
|
-
async def test_streamlit_handler_success(self,
|
|
141
|
+
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.log_streamlit_app_conversion_success')
|
|
142
|
+
async def test_streamlit_handler_success(self, mock_log_success, mock_create_file, mock_validator, mock_generate_code, mock_parse):
|
|
145
143
|
"""Test successful streamlit handler execution"""
|
|
146
144
|
# Mock notebook parsing
|
|
147
145
|
mock_notebook_data: List[dict] = [{"cells": [{"cell_type": "code", "source": ["import pandas"]}]}]
|
|
@@ -151,35 +149,29 @@ class TestStreamlitHandler:
|
|
|
151
149
|
mock_generate_code.return_value = "import streamlit\nst.title('Test')"
|
|
152
150
|
|
|
153
151
|
# Mock validation (no errors)
|
|
154
|
-
mock_validator.return_value =
|
|
155
|
-
|
|
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
|
|
152
|
+
mock_validator.return_value = []
|
|
161
153
|
|
|
162
154
|
# Use a relative path that will work cross-platform
|
|
163
|
-
notebook_path = "notebook.ipynb"
|
|
164
|
-
result = await streamlit_handler(notebook_path)
|
|
155
|
+
notebook_path = AbsoluteNotebookPath("absolute/path/to/notebook.ipynb")
|
|
165
156
|
|
|
166
|
-
|
|
167
|
-
|
|
157
|
+
# Construct the expected app path using the same method as the production code
|
|
158
|
+
app_directory = get_absolute_notebook_dir_path(notebook_path)
|
|
159
|
+
expected_app_path = get_absolute_app_path(app_directory)
|
|
160
|
+
await streamlit_handler(notebook_path)
|
|
168
161
|
|
|
169
162
|
# Verify calls
|
|
170
163
|
mock_parse.assert_called_once_with(notebook_path)
|
|
171
164
|
mock_generate_code.assert_called_once_with(mock_notebook_data)
|
|
172
165
|
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')")
|
|
166
|
+
mock_create_file.assert_called_once_with(expected_app_path, "import streamlit\nst.title('Test')")
|
|
176
167
|
|
|
177
168
|
@pytest.mark.asyncio
|
|
178
169
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
|
|
179
170
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.generate_new_streamlit_code')
|
|
180
171
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.correct_error_in_generation')
|
|
181
172
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.validate_app')
|
|
182
|
-
|
|
173
|
+
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.log_streamlit_app_validation_retry')
|
|
174
|
+
async def test_streamlit_handler_max_retries_exceeded(self, mock_log_retry, mock_validator, mock_correct_error, mock_generate_code, mock_parse):
|
|
183
175
|
"""Test streamlit handler when max retries are exceeded"""
|
|
184
176
|
# Mock notebook parsing
|
|
185
177
|
mock_notebook_data: List[dict] = [{"cells": []}]
|
|
@@ -189,16 +181,15 @@ class TestStreamlitHandler:
|
|
|
189
181
|
mock_generate_code.return_value = "import streamlit\nst.title('Test')"
|
|
190
182
|
mock_correct_error.return_value = "import streamlit\nst.title('Fixed')"
|
|
191
183
|
|
|
192
|
-
# Mock validation (always errors) -
|
|
193
|
-
mock_validator.return_value =
|
|
184
|
+
# Mock validation (always errors) - validate_app returns List[str]
|
|
185
|
+
mock_validator.return_value = ["Persistent error"]
|
|
194
186
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
assert result[0] is False
|
|
199
|
-
assert "Error generating streamlit code by agent" in result[2]
|
|
187
|
+
# Now it should raise an exception instead of returning a tuple
|
|
188
|
+
with pytest.raises(Exception):
|
|
189
|
+
await streamlit_handler(AbsoluteNotebookPath("notebook.ipynb"))
|
|
200
190
|
|
|
201
|
-
# Verify that error correction was called 5 times (
|
|
191
|
+
# Verify that error correction was called 5 times (once per error, 5 retries)
|
|
192
|
+
# Each retry processes 1 error, so 5 retries = 5 calls
|
|
202
193
|
assert mock_correct_error.call_count == 5
|
|
203
194
|
|
|
204
195
|
@pytest.mark.asyncio
|
|
@@ -206,8 +197,7 @@ class TestStreamlitHandler:
|
|
|
206
197
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.generate_new_streamlit_code')
|
|
207
198
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.validate_app')
|
|
208
199
|
@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):
|
|
200
|
+
async def test_streamlit_handler_file_creation_failure(self, mock_create_file, mock_validator, mock_generate_code, mock_parse):
|
|
211
201
|
"""Test streamlit handler when file creation fails"""
|
|
212
202
|
# Mock notebook parsing
|
|
213
203
|
mock_notebook_data: List[dict] = [{"cells": []}]
|
|
@@ -216,38 +206,30 @@ class TestStreamlitHandler:
|
|
|
216
206
|
# Mock code generation
|
|
217
207
|
mock_generate_code.return_value = "import streamlit\nst.title('Test')"
|
|
218
208
|
|
|
219
|
-
# Mock validation (no errors)
|
|
220
|
-
mock_validator.return_value =
|
|
221
|
-
|
|
222
|
-
# Mock file creation failure
|
|
223
|
-
mock_create_file.return_value = (False, None, "Permission denied")
|
|
209
|
+
# Mock validation (no errors) - validate_app returns List[str]
|
|
210
|
+
mock_validator.return_value = []
|
|
224
211
|
|
|
225
|
-
# Mock
|
|
226
|
-
|
|
212
|
+
# Mock file creation failure - now it should raise an exception
|
|
213
|
+
mock_create_file.side_effect = Exception("Permission denied")
|
|
227
214
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
assert "Permission denied" in result[2]
|
|
215
|
+
# Now it should raise an exception instead of returning a tuple
|
|
216
|
+
with pytest.raises(Exception):
|
|
217
|
+
await streamlit_handler(AbsoluteNotebookPath("notebook.ipynb"))
|
|
232
218
|
|
|
233
219
|
@pytest.mark.asyncio
|
|
234
220
|
@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):
|
|
221
|
+
async def test_streamlit_handler_parse_notebook_exception(self, mock_parse):
|
|
237
222
|
"""Test streamlit handler when notebook parsing fails"""
|
|
238
|
-
# Mock clean directory check (no-op)
|
|
239
|
-
mock_clean_directory.return_value = None
|
|
240
223
|
|
|
241
224
|
mock_parse.side_effect = FileNotFoundError("Notebook not found")
|
|
242
225
|
|
|
243
226
|
with pytest.raises(FileNotFoundError, match="Notebook not found"):
|
|
244
|
-
await streamlit_handler("notebook.ipynb")
|
|
227
|
+
await streamlit_handler(AbsoluteNotebookPath("notebook.ipynb"))
|
|
245
228
|
|
|
246
229
|
@pytest.mark.asyncio
|
|
247
230
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
|
|
248
231
|
@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):
|
|
232
|
+
async def test_streamlit_handler_generation_exception(self, mock_generate_code, mock_parse):
|
|
251
233
|
"""Test streamlit handler when code generation fails"""
|
|
252
234
|
# Mock notebook parsing
|
|
253
235
|
mock_notebook_data: List[dict] = [{"cells": []}]
|
|
@@ -256,11 +238,8 @@ class TestStreamlitHandler:
|
|
|
256
238
|
# Mock code generation failure
|
|
257
239
|
mock_generate_code.side_effect = Exception("Generation failed")
|
|
258
240
|
|
|
259
|
-
# Mock clean directory check (no-op)
|
|
260
|
-
mock_clean_directory.return_value = None
|
|
261
|
-
|
|
262
241
|
with pytest.raises(Exception, match="Generation failed"):
|
|
263
|
-
await streamlit_handler("notebook.ipynb")
|
|
242
|
+
await streamlit_handler(AbsoluteNotebookPath("notebook.ipynb"))
|
|
264
243
|
|
|
265
244
|
|
|
266
245
|
|