mito-ai 0.1.47__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 +3 -2
- mito_ai/completions/models.py +1 -0
- mito_ai/completions/prompt_builders/agent_execution_prompt.py +4 -0
- mito_ai/completions/prompt_builders/agent_system_message.py +1 -1
- mito_ai/path_utils.py +1 -1
- mito_ai/streamlit_conversion/search_replace_utils.py +4 -3
- mito_ai/streamlit_conversion/streamlit_agent_handler.py +6 -7
- mito_ai/streamlit_conversion/streamlit_utils.py +1 -2
- mito_ai/streamlit_conversion/validate_streamlit_app.py +2 -4
- mito_ai/streamlit_preview/__init__.py +1 -2
- mito_ai/streamlit_preview/handlers.py +17 -9
- mito_ai/streamlit_preview/manager.py +2 -11
- mito_ai/streamlit_preview/utils.py +1 -18
- mito_ai/tests/message_history/test_message_history_utils.py +1 -0
- mito_ai/tests/streamlit_conversion/test_apply_search_replace.py +18 -4
- mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +12 -9
- mito_ai/tests/streamlit_conversion/test_validate_streamlit_app.py +20 -18
- mito_ai/tests/streamlit_preview/test_streamlit_preview_handler.py +80 -52
- mito_ai/tests/streamlit_preview/test_streamlit_preview_manager.py +2 -11
- mito_ai/utils/telemetry_utils.py +9 -9
- {mito_ai-0.1.47.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +100 -100
- {mito_ai-0.1.47.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/package.json +2 -2
- {mito_ai-0.1.47.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.47.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.2db61d2b629817845901.js → mito_ai-0.1.48.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.5c7d84a45ddeb5704b61.js +239 -154
- 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.47.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.e22c6cd4e56c32116daa.js → mito_ai-0.1.48.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.045d65d1de6fde3f3b72.js +16 -16
- mito_ai-0.1.47.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.e22c6cd4e56c32116daa.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.47.dist-info → mito_ai-0.1.48.dist-info}/METADATA +1 -1
- {mito_ai-0.1.47.dist-info → mito_ai-0.1.48.dist-info}/RECORD +54 -54
- mito_ai-0.1.47.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.2db61d2b629817845901.js.map +0 -1
- {mito_ai-0.1.47.data → mito_ai-0.1.48.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
- {mito_ai-0.1.47.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.47.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.47.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.47.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
- {mito_ai-0.1.47.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.47.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.47.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.47.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.47.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.47.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.47.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.47.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.47.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.47.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.47.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.47.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.47.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.47.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.47.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.47.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.47.dist-info → mito_ai-0.1.48.dist-info}/WHEEL +0 -0
- {mito_ai-0.1.47.dist-info → mito_ai-0.1.48.dist-info}/entry_points.txt +0 -0
- {mito_ai-0.1.47.dist-info → mito_ai-0.1.48.dist-info}/licenses/LICENSE +0 -0
mito_ai/_version.py
CHANGED
mito_ai/app_deploy/handlers.py
CHANGED
|
@@ -6,7 +6,7 @@ import time
|
|
|
6
6
|
import logging
|
|
7
7
|
from typing import Any, Union, List, Optional
|
|
8
8
|
import tempfile
|
|
9
|
-
from mito_ai.path_utils import AbsoluteAppPath,
|
|
9
|
+
from mito_ai.path_utils import AbsoluteAppPath, does_app_path_exist, get_absolute_app_path, get_absolute_notebook_dir_path, get_absolute_notebook_path
|
|
10
10
|
from mito_ai.utils.create import initialize_user
|
|
11
11
|
from mito_ai.utils.error_classes import StreamlitDeploymentError
|
|
12
12
|
from mito_ai.utils.version_utils import is_pro
|
|
@@ -131,6 +131,7 @@ class AppDeployHandler(BaseWebSocketHandler):
|
|
|
131
131
|
raise StreamlitDeploymentError(error)
|
|
132
132
|
|
|
133
133
|
if not notebook_path:
|
|
134
|
+
self.log.error("Missing notebook_path in request")
|
|
134
135
|
error = AppDeployError(
|
|
135
136
|
error_type="InvalidRequest",
|
|
136
137
|
message="Missing 'notebook_path' parameter",
|
|
@@ -162,7 +163,7 @@ class AppDeployHandler(BaseWebSocketHandler):
|
|
|
162
163
|
app_path = get_absolute_app_path(absolute_app_directory)
|
|
163
164
|
|
|
164
165
|
# Check if the app.py file exists
|
|
165
|
-
app_path_exists =
|
|
166
|
+
app_path_exists = does_app_path_exist(app_path)
|
|
166
167
|
if not app_path_exists:
|
|
167
168
|
error = AppDeployError(
|
|
168
169
|
error_type="AppNotFound",
|
mito_ai/completions/models.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
from mito_ai.completions.models import AgentExecutionMetadata
|
|
5
5
|
from mito_ai.completions.prompt_builders.prompt_constants import (
|
|
6
|
+
ACTIVE_CELL_ID_SECTION_HEADING,
|
|
6
7
|
FILES_SECTION_HEADING,
|
|
7
8
|
JUPYTER_NOTEBOOK_SECTION_HEADING,
|
|
8
9
|
STREAMLIT_APP_STATUS_SECTION_HEADING,
|
|
@@ -40,6 +41,9 @@ def create_agent_execution_prompt(md: AgentExecutionMetadata) -> str:
|
|
|
40
41
|
{STREAMLIT_APP_STATUS_SECTION_HEADING}
|
|
41
42
|
{streamlit_status_str}
|
|
42
43
|
|
|
44
|
+
{ACTIVE_CELL_ID_SECTION_HEADING}
|
|
45
|
+
{md.activeCellId}
|
|
46
|
+
|
|
43
47
|
{selected_context_str}
|
|
44
48
|
|
|
45
49
|
{cell_update_output_str(md.base64EncodedActiveCellOutput is not None)}"""
|
|
@@ -469,4 +469,4 @@ REMEMBER, YOU ARE GOING TO COMPLETE THE USER'S TASK OVER THE COURSE OF THE ENTIR
|
|
|
469
469
|
|
|
470
470
|
OTHER USEFUL INFORMATION:
|
|
471
471
|
1. When importing matplotlib, write the code `%matplotlib inline` to make sure the graphs render in Jupyter
|
|
472
|
-
"""
|
|
472
|
+
2. The active cell ID is shared with you so that when the user refers to "this cell" or similar phrases, you know which cell they mean. However, you are free to edit any cell that you see fit."""
|
mito_ai/path_utils.py
CHANGED
|
@@ -48,7 +48,7 @@ def get_absolute_app_path(app_directory: AbsoluteNotebookDirPath) -> AbsoluteApp
|
|
|
48
48
|
"""
|
|
49
49
|
return AbsoluteAppPath(os.path.join(app_directory, "app.py"))
|
|
50
50
|
|
|
51
|
-
def
|
|
51
|
+
def does_app_path_exist(app_path: AbsoluteAppPath) -> bool:
|
|
52
52
|
"""
|
|
53
53
|
Check if the app.py file exists in the given directory.
|
|
54
54
|
"""
|
|
@@ -5,6 +5,7 @@ import re
|
|
|
5
5
|
from typing import List, Tuple
|
|
6
6
|
|
|
7
7
|
from mito_ai.utils.error_classes import StreamlitConversionError
|
|
8
|
+
from mito_ai.utils.telemetry_utils import log
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
def extract_search_replace_blocks(message_content: str) -> List[Tuple[str, str]]:
|
|
@@ -85,9 +86,9 @@ def apply_search_replace(text: str, search_replace_pairs: List[Tuple[str, str]])
|
|
|
85
86
|
raise StreamlitConversionError(f"Search text not found: {repr(search_text)}", error_code=500)
|
|
86
87
|
elif count > 1:
|
|
87
88
|
print("Search Text Found Multiple Times: ", repr(search_text))
|
|
88
|
-
|
|
89
|
-
|
|
89
|
+
log("mito_ai_search_text_found_multiple_times", params={"search_text": repr(search_text)}, key_type="mito_server_key")
|
|
90
|
+
|
|
90
91
|
# Perform the replacement
|
|
91
|
-
result = result.replace(search_text, replace_text)
|
|
92
|
+
result = result.replace(search_text, replace_text, 1)
|
|
92
93
|
|
|
93
94
|
return result
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
# Copyright (c) Saga Inc.
|
|
2
2
|
# Distributed under the terms of the GNU Affero General Public License v3.0 License.
|
|
3
3
|
|
|
4
|
-
import os
|
|
5
4
|
from anthropic.types import MessageParam
|
|
6
|
-
from typing import List,
|
|
5
|
+
from typing import List, cast
|
|
7
6
|
from mito_ai.streamlit_conversion.agent_utils import extract_todo_placeholders, get_response_from_agent
|
|
8
7
|
from mito_ai.streamlit_conversion.prompts.streamlit_app_creation_prompt import get_streamlit_app_creation_prompt
|
|
9
8
|
from mito_ai.streamlit_conversion.prompts.streamlit_error_correction_prompt import get_streamlit_error_correction_prompt
|
|
@@ -122,21 +121,21 @@ async def streamlit_handler(notebook_path: AbsoluteNotebookPath, edit_prompt: st
|
|
|
122
121
|
streamlit_code = await generate_new_streamlit_code(notebook_code)
|
|
123
122
|
|
|
124
123
|
# Then, after creating/updating the app, validate that the new code runs
|
|
125
|
-
|
|
124
|
+
errors = validate_app(streamlit_code, notebook_path)
|
|
126
125
|
tries = 0
|
|
127
|
-
while
|
|
126
|
+
while len(errors)>0 and tries < 5:
|
|
128
127
|
for error in errors:
|
|
129
128
|
streamlit_code = await correct_error_in_generation(error, streamlit_code)
|
|
130
129
|
|
|
131
|
-
|
|
130
|
+
errors = validate_app(streamlit_code, notebook_path)
|
|
132
131
|
|
|
133
|
-
if
|
|
132
|
+
if len(errors)>0:
|
|
134
133
|
# TODO: We can't easily get the key type here, so for the beta release
|
|
135
134
|
# we are just defaulting to the mito server key since that is by far the most common.
|
|
136
135
|
log_streamlit_app_validation_retry('mito_server_key', MessageType.STREAMLIT_CONVERSION, errors)
|
|
137
136
|
tries+=1
|
|
138
137
|
|
|
139
|
-
if
|
|
138
|
+
if len(errors)>0:
|
|
140
139
|
final_errors = ', '.join(errors)
|
|
141
140
|
raise StreamlitConversionError(f"Streamlit agent failed generating code after max retries. Errors: {final_errors}", 500)
|
|
142
141
|
|
|
@@ -3,9 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
import re
|
|
5
5
|
import json
|
|
6
|
-
from typing import Dict, List, Optional,
|
|
6
|
+
from typing import Dict, List, Optional, Any
|
|
7
7
|
from mito_ai.path_utils import AbsoluteAppPath, AbsoluteNotebookPath
|
|
8
|
-
from pathlib import Path
|
|
9
8
|
from mito_ai.utils.error_classes import StreamlitConversionError
|
|
10
9
|
|
|
11
10
|
|
|
@@ -98,10 +98,8 @@ def check_for_errors(app_code: str, app_path: AbsoluteNotebookPath) -> List[Dict
|
|
|
98
98
|
|
|
99
99
|
return errors
|
|
100
100
|
|
|
101
|
-
def validate_app(app_code: str, notebook_path: AbsoluteNotebookPath) ->
|
|
101
|
+
def validate_app(app_code: str, notebook_path: AbsoluteNotebookPath) -> List[str]:
|
|
102
102
|
"""Convenience function to validate Streamlit code"""
|
|
103
103
|
errors = check_for_errors(app_code, notebook_path)
|
|
104
|
-
|
|
105
|
-
has_validation_error = len(errors) > 0
|
|
106
104
|
stringified_errors = [str(error) for error in errors]
|
|
107
|
-
return
|
|
105
|
+
return stringified_errors
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# Copyright (c) Saga Inc.
|
|
2
2
|
# Distributed under the terms of the GNU Affero General Public License v3.0 License.
|
|
3
3
|
|
|
4
|
-
from .manager import get_preview_manager
|
|
5
4
|
from .handlers import StreamlitPreviewHandler
|
|
6
5
|
|
|
7
|
-
__all__ = ['
|
|
6
|
+
__all__ = ['StreamlitPreviewHandler']
|
|
@@ -1,17 +1,16 @@
|
|
|
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
|
-
from typing import Literal, TypedDict
|
|
6
4
|
import uuid
|
|
7
|
-
from mito_ai.streamlit_preview.utils import
|
|
5
|
+
from mito_ai.streamlit_preview.utils import validate_request_body
|
|
8
6
|
import tornado
|
|
9
7
|
from jupyter_server.base.handlers import APIHandler
|
|
10
|
-
from mito_ai.streamlit_preview.manager import
|
|
11
|
-
from mito_ai.path_utils import
|
|
8
|
+
from mito_ai.streamlit_preview.manager import StreamlitPreviewManager
|
|
9
|
+
from mito_ai.path_utils import get_absolute_notebook_dir_path, get_absolute_notebook_path, get_absolute_app_path, does_app_path_exist
|
|
12
10
|
from mito_ai.utils.telemetry_utils import log_streamlit_app_conversion_error, log_streamlit_app_preview_failure, log_streamlit_app_preview_success
|
|
13
11
|
from mito_ai.completions.models import MessageType
|
|
14
12
|
from mito_ai.utils.error_classes import StreamlitConversionError, StreamlitPreviewError
|
|
13
|
+
from mito_ai.streamlit_conversion.streamlit_agent_handler import streamlit_handler
|
|
15
14
|
import traceback
|
|
16
15
|
|
|
17
16
|
|
|
@@ -20,7 +19,7 @@ class StreamlitPreviewHandler(APIHandler):
|
|
|
20
19
|
|
|
21
20
|
def initialize(self) -> None:
|
|
22
21
|
"""Initialize the handler."""
|
|
23
|
-
self.preview_manager =
|
|
22
|
+
self.preview_manager = StreamlitPreviewManager()
|
|
24
23
|
|
|
25
24
|
@tornado.web.authenticated
|
|
26
25
|
async def post(self) -> None:
|
|
@@ -32,15 +31,24 @@ class StreamlitPreviewHandler(APIHandler):
|
|
|
32
31
|
|
|
33
32
|
# Ensure app exists
|
|
34
33
|
absolute_notebook_path = get_absolute_notebook_path(notebook_path)
|
|
35
|
-
|
|
34
|
+
absolute_notebook_dir_path = get_absolute_notebook_dir_path(absolute_notebook_path)
|
|
35
|
+
absolute_app_path = get_absolute_app_path(absolute_notebook_dir_path)
|
|
36
|
+
app_path_exists = does_app_path_exist(absolute_app_path)
|
|
37
|
+
|
|
38
|
+
if not app_path_exists or force_recreate:
|
|
39
|
+
if not app_path_exists:
|
|
40
|
+
print("[Mito AI] App path not found, generating streamlit code")
|
|
41
|
+
else:
|
|
42
|
+
print("[Mito AI] Force recreating streamlit app")
|
|
43
|
+
|
|
44
|
+
await streamlit_handler(absolute_notebook_path, edit_prompt)
|
|
36
45
|
|
|
37
46
|
# Start preview
|
|
38
47
|
# TODO: There's a bug here where when the user rebuilds and already running app. Instead of
|
|
39
48
|
# creating a new process, we should update the existing process. The app displayed to the user
|
|
40
49
|
# does update, but that's just because of hot reloading when we overwrite the app.py file.
|
|
41
50
|
preview_id = str(uuid.uuid4())
|
|
42
|
-
|
|
43
|
-
port = self.preview_manager.start_streamlit_preview(absolute_app_directory, preview_id)
|
|
51
|
+
port = self.preview_manager.start_streamlit_preview(absolute_notebook_dir_path, preview_id)
|
|
44
52
|
|
|
45
53
|
# Return success response
|
|
46
54
|
self.finish({
|
|
@@ -1,10 +1,8 @@
|
|
|
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
|
|
@@ -107,7 +105,7 @@ class StreamlitPreviewManager:
|
|
|
107
105
|
if response.status_code == 200:
|
|
108
106
|
return True
|
|
109
107
|
except requests.exceptions.RequestException as e:
|
|
110
|
-
|
|
108
|
+
self.log.info(f"Waiting for app to be ready...")
|
|
111
109
|
pass
|
|
112
110
|
|
|
113
111
|
time.sleep(1)
|
|
@@ -123,7 +121,7 @@ class StreamlitPreviewManager:
|
|
|
123
121
|
Returns:
|
|
124
122
|
True if stopped successfully, False if not found
|
|
125
123
|
"""
|
|
126
|
-
|
|
124
|
+
self.log.info(f"Stopping preview {preview_id}")
|
|
127
125
|
with self._lock:
|
|
128
126
|
if preview_id not in self._previews:
|
|
129
127
|
return False
|
|
@@ -150,10 +148,3 @@ class StreamlitPreviewManager:
|
|
|
150
148
|
"""Get a preview process by ID."""
|
|
151
149
|
with self._lock:
|
|
152
150
|
return self._previews.get(preview_id)
|
|
153
|
-
|
|
154
|
-
# Global instance
|
|
155
|
-
_preview_manager = StreamlitPreviewManager()
|
|
156
|
-
|
|
157
|
-
def get_preview_manager() -> StreamlitPreviewManager:
|
|
158
|
-
"""Get the global preview manager instance."""
|
|
159
|
-
return _preview_manager
|
|
@@ -2,8 +2,6 @@
|
|
|
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
|
-
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
5
|
from mito_ai.utils.error_classes import StreamlitPreviewError
|
|
8
6
|
|
|
9
7
|
|
|
@@ -19,24 +17,9 @@ def validate_request_body(body: Optional[dict]) -> Tuple[str, bool, str]:
|
|
|
19
17
|
force_recreate = body.get("force_recreate", False)
|
|
20
18
|
if not isinstance(force_recreate, bool):
|
|
21
19
|
raise StreamlitPreviewError("force_recreate must be a boolean", 400)
|
|
22
|
-
|
|
20
|
+
|
|
23
21
|
edit_prompt = body.get("edit_prompt", "")
|
|
24
22
|
if not isinstance(edit_prompt, str):
|
|
25
23
|
raise StreamlitPreviewError("edit_prompt must be a string", 400)
|
|
26
24
|
|
|
27
25
|
return notebook_path, force_recreate, edit_prompt
|
|
28
|
-
|
|
29
|
-
async def ensure_app_exists(absolute_notebook_path: AbsoluteNotebookPath, force_recreate: bool = False, edit_prompt: str = "") -> None:
|
|
30
|
-
"""Ensure app.py exists, generating it if necessary or if force_recreate is True."""
|
|
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)
|
|
35
|
-
|
|
36
|
-
if not app_path_exists or force_recreate:
|
|
37
|
-
if not app_path_exists:
|
|
38
|
-
print("[Mito AI] App path not found, generating streamlit code")
|
|
39
|
-
else:
|
|
40
|
-
print("[Mito AI] Force recreating streamlit app")
|
|
41
|
-
|
|
42
|
-
await streamlit_handler(absolute_notebook_path, edit_prompt)
|
|
@@ -200,6 +200,23 @@ st.write("Welcome to my application")""",
|
|
|
200
200
|
|
|
201
201
|
st.title("🚀 My App")
|
|
202
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")"""
|
|
203
220
|
)
|
|
204
221
|
])
|
|
205
222
|
def test_apply_search_replace(original_text, search_replace_pairs, expected_result):
|
|
@@ -220,7 +237,4 @@ def test_apply_search_replace_search_not_found():
|
|
|
220
237
|
apply_search_replace("st.title(\"My App\")", [("st.title(\"Not Found\")", "st.title(\"New Title\")")])
|
|
221
238
|
|
|
222
239
|
|
|
223
|
-
|
|
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\")")])
|
|
240
|
+
|
|
@@ -138,7 +138,8 @@ class TestStreamlitHandler:
|
|
|
138
138
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.generate_new_streamlit_code')
|
|
139
139
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.validate_app')
|
|
140
140
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.create_app_file')
|
|
141
|
-
|
|
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):
|
|
142
143
|
"""Test successful streamlit handler execution"""
|
|
143
144
|
# Mock notebook parsing
|
|
144
145
|
mock_notebook_data: List[dict] = [{"cells": [{"cell_type": "code", "source": ["import pandas"]}]}]
|
|
@@ -148,7 +149,7 @@ class TestStreamlitHandler:
|
|
|
148
149
|
mock_generate_code.return_value = "import streamlit\nst.title('Test')"
|
|
149
150
|
|
|
150
151
|
# Mock validation (no errors)
|
|
151
|
-
mock_validator.return_value =
|
|
152
|
+
mock_validator.return_value = []
|
|
152
153
|
|
|
153
154
|
# Use a relative path that will work cross-platform
|
|
154
155
|
notebook_path = AbsoluteNotebookPath("absolute/path/to/notebook.ipynb")
|
|
@@ -169,7 +170,8 @@ class TestStreamlitHandler:
|
|
|
169
170
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.generate_new_streamlit_code')
|
|
170
171
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.correct_error_in_generation')
|
|
171
172
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.validate_app')
|
|
172
|
-
|
|
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):
|
|
173
175
|
"""Test streamlit handler when max retries are exceeded"""
|
|
174
176
|
# Mock notebook parsing
|
|
175
177
|
mock_notebook_data: List[dict] = [{"cells": []}]
|
|
@@ -179,14 +181,15 @@ class TestStreamlitHandler:
|
|
|
179
181
|
mock_generate_code.return_value = "import streamlit\nst.title('Test')"
|
|
180
182
|
mock_correct_error.return_value = "import streamlit\nst.title('Fixed')"
|
|
181
183
|
|
|
182
|
-
# Mock validation (always errors) -
|
|
183
|
-
mock_validator.return_value =
|
|
184
|
+
# Mock validation (always errors) - validate_app returns List[str]
|
|
185
|
+
mock_validator.return_value = ["Persistent error"]
|
|
184
186
|
|
|
185
187
|
# Now it should raise an exception instead of returning a tuple
|
|
186
188
|
with pytest.raises(Exception):
|
|
187
189
|
await streamlit_handler(AbsoluteNotebookPath("notebook.ipynb"))
|
|
188
190
|
|
|
189
|
-
# 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
|
|
190
193
|
assert mock_correct_error.call_count == 5
|
|
191
194
|
|
|
192
195
|
@pytest.mark.asyncio
|
|
@@ -203,14 +206,14 @@ class TestStreamlitHandler:
|
|
|
203
206
|
# Mock code generation
|
|
204
207
|
mock_generate_code.return_value = "import streamlit\nst.title('Test')"
|
|
205
208
|
|
|
206
|
-
# Mock validation (no errors)
|
|
207
|
-
mock_validator.return_value =
|
|
209
|
+
# Mock validation (no errors) - validate_app returns List[str]
|
|
210
|
+
mock_validator.return_value = []
|
|
208
211
|
|
|
209
212
|
# Mock file creation failure - now it should raise an exception
|
|
210
213
|
mock_create_file.side_effect = Exception("Permission denied")
|
|
211
214
|
|
|
212
215
|
# Now it should raise an exception instead of returning a tuple
|
|
213
|
-
with pytest.raises(Exception
|
|
216
|
+
with pytest.raises(Exception):
|
|
214
217
|
await streamlit_handler(AbsoluteNotebookPath("notebook.ipynb"))
|
|
215
218
|
|
|
216
219
|
@pytest.mark.asyncio
|
|
@@ -7,7 +7,6 @@ from unittest.mock import patch, MagicMock
|
|
|
7
7
|
from mito_ai.streamlit_conversion.validate_streamlit_app import (
|
|
8
8
|
get_syntax_error,
|
|
9
9
|
get_runtime_errors,
|
|
10
|
-
check_for_errors,
|
|
11
10
|
validate_app
|
|
12
11
|
)
|
|
13
12
|
import pytest
|
|
@@ -20,33 +19,33 @@ class TestGetSyntaxError:
|
|
|
20
19
|
@pytest.mark.parametrize("code,expected_error,test_description", [
|
|
21
20
|
# Valid Python code should return no error
|
|
22
21
|
(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
"import streamlit\nst.title('Hello World')",
|
|
23
|
+
None,
|
|
24
|
+
"valid Python code"
|
|
26
25
|
),
|
|
27
26
|
# Invalid Python syntax should be caught
|
|
28
27
|
(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
"import streamlit\nst.title('Hello World'",
|
|
29
|
+
"SyntaxError",
|
|
30
|
+
"invalid Python code"
|
|
32
31
|
),
|
|
33
32
|
# Empty streamlit app is valid
|
|
34
33
|
(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
"",
|
|
35
|
+
None,
|
|
36
|
+
"empty code"
|
|
38
37
|
),
|
|
39
38
|
])
|
|
40
39
|
def test_get_syntax_error(self, code, expected_error, test_description):
|
|
41
40
|
"""Test syntax validation with various code inputs"""
|
|
42
41
|
error = get_syntax_error(code)
|
|
43
|
-
|
|
42
|
+
|
|
44
43
|
if expected_error is None:
|
|
45
44
|
assert error is None, f"Expected no error for {test_description}"
|
|
46
45
|
else:
|
|
47
46
|
assert error is not None, f"Expected error for {test_description}"
|
|
48
47
|
assert expected_error in error, f"Expected '{expected_error}' in error for {test_description}"
|
|
49
|
-
|
|
48
|
+
|
|
50
49
|
class TestGetRuntimeErrors:
|
|
51
50
|
"""Test cases for get_runtime_errors function"""
|
|
52
51
|
|
|
@@ -94,17 +93,20 @@ df=pd.read_csv('data.csv')
|
|
|
94
93
|
class TestValidateApp:
|
|
95
94
|
"""Test cases for validate_app function"""
|
|
96
95
|
|
|
97
|
-
@pytest.mark.parametrize("app_code,
|
|
96
|
+
@pytest.mark.parametrize("app_code,expected_has_errors,expected_error_message", [
|
|
98
97
|
("x=5", False, ""),
|
|
99
98
|
("1/0", True, "division by zero"),
|
|
100
|
-
|
|
99
|
+
# Syntax errors are caught during runtime
|
|
101
100
|
("", False, ""),
|
|
102
101
|
])
|
|
103
|
-
def test_validate_app(self, app_code,
|
|
102
|
+
def test_validate_app(self, app_code, expected_has_errors, expected_error_message):
|
|
104
103
|
"""Test the validate_app function"""
|
|
105
|
-
|
|
104
|
+
errors = validate_app(app_code, AbsoluteNotebookPath('/notebook.ipynb'))
|
|
106
105
|
|
|
107
|
-
|
|
108
|
-
assert
|
|
106
|
+
has_errors = len(errors) > 0
|
|
107
|
+
assert has_errors == expected_has_errors
|
|
108
|
+
if expected_error_message:
|
|
109
|
+
errors_str = str(errors)
|
|
110
|
+
assert expected_error_message in errors_str
|
|
109
111
|
|
|
110
112
|
|