mito-ai 0.1.46__py3-none-any.whl → 0.1.49__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mito_ai/_version.py +1 -1
- mito_ai/app_deploy/app_deploy_utils.py +28 -9
- mito_ai/app_deploy/handlers.py +123 -84
- mito_ai/app_deploy/models.py +19 -12
- mito_ai/completions/models.py +6 -1
- mito_ai/completions/prompt_builders/agent_execution_prompt.py +13 -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 +13 -0
- mito_ai/path_utils.py +70 -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 +13 -75
- mito_ai/streamlit_conversion/validate_streamlit_app.py +6 -21
- mito_ai/streamlit_preview/__init__.py +1 -2
- mito_ai/streamlit_preview/handlers.py +54 -85
- mito_ai/streamlit_preview/manager.py +11 -18
- mito_ai/streamlit_preview/utils.py +12 -28
- mito_ai/tests/deploy_app/test_app_deploy_utils.py +22 -4
- mito_ai/tests/message_history/test_message_history_utils.py +3 -0
- mito_ai/tests/streamlit_conversion/test_apply_search_replace.py +240 -0
- mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +40 -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 +87 -57
- mito_ai/tests/streamlit_preview/test_streamlit_preview_manager.py +27 -40
- 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.49.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +100 -100
- {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/package.json +2 -2
- {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/package.json.orig +1 -1
- mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.20f12766ecd3d430568e.js → mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.8f1845da6bf2b128c049.js +3571 -1442
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.8f1845da6bf2b128c049.js.map +1 -0
- mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.54126ab6511271265443.js → mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.8b24b5b3b93f95205b56.js +24 -24
- mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.54126ab6511271265443.js.map → mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.8b24b5b3b93f95205b56.js.map +1 -1
- {mito_ai-0.1.46.dist-info → mito_ai-0.1.49.dist-info}/METADATA +1 -1
- {mito_ai-0.1.46.dist-info → mito_ai-0.1.49.dist-info}/RECORD +71 -69
- 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.49.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/toolbar-buttons.json +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js.map +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js.map +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_apis_signOut_mjs-node_module-75790d.688c25857e7b81b1740f.js +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_apis_signOut_mjs-node_module-75790d.688c25857e7b81b1740f.js.map +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_tokenProvider_tokenProvider_-72f1c8.a917210f057fcfe224ad.js +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_tokenProvider_tokenProvider_-72f1c8.a917210f057fcfe224ad.js.map +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js.map +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js.map +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_react-dom_client_js-node_modules_aws-amplify_ui-react_dist_styles_css.b43d4249e4d3dac9ad7b.js +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_react-dom_client_js-node_modules_aws-amplify_ui-react_dist_styles_css.b43d4249e4d3dac9ad7b.js.map +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js.map +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js.map +0 -0
- {mito_ai-0.1.46.dist-info → mito_ai-0.1.49.dist-info}/WHEEL +0 -0
- {mito_ai-0.1.46.dist-info → mito_ai-0.1.49.dist-info}/entry_points.txt +0 -0
- {mito_ai-0.1.46.dist-info → mito_ai-0.1.49.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,18 +1,17 @@
|
|
|
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
|
-
import tempfile
|
|
6
4
|
import uuid
|
|
7
|
-
from mito_ai.
|
|
8
|
-
from mito_ai.streamlit_preview.utils import ensure_app_exists, validate_request_body
|
|
5
|
+
from mito_ai.streamlit_preview.utils import validate_request_body
|
|
9
6
|
import tornado
|
|
10
7
|
from jupyter_server.base.handlers import APIHandler
|
|
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, get_app_file_name
|
|
10
|
+
from mito_ai.utils.telemetry_utils import log_streamlit_app_conversion_error, log_streamlit_app_preview_failure, log_streamlit_app_preview_success
|
|
11
|
+
from mito_ai.completions.models import MessageType
|
|
12
|
+
from mito_ai.utils.error_classes import StreamlitConversionError, StreamlitPreviewError
|
|
11
13
|
from mito_ai.streamlit_conversion.streamlit_agent_handler import streamlit_handler
|
|
12
|
-
|
|
13
|
-
from mito_ai.utils.create import initialize_user
|
|
14
|
-
from typing import Tuple, Optional
|
|
15
|
-
|
|
14
|
+
import traceback
|
|
16
15
|
|
|
17
16
|
|
|
18
17
|
class StreamlitPreviewHandler(APIHandler):
|
|
@@ -20,61 +19,7 @@ class StreamlitPreviewHandler(APIHandler):
|
|
|
20
19
|
|
|
21
20
|
def initialize(self) -> None:
|
|
22
21
|
"""Initialize the handler."""
|
|
23
|
-
self.preview_manager =
|
|
24
|
-
|
|
25
|
-
def _resolve_notebook_path(self, notebook_path: str) -> str:
|
|
26
|
-
"""
|
|
27
|
-
Resolve the notebook path to an absolute path that can be found by the backend.
|
|
28
|
-
|
|
29
|
-
This method handles path resolution issues that can occur in different environments:
|
|
30
|
-
|
|
31
|
-
1. **Test Environment**: Playwright tests create temporary directories with complex
|
|
32
|
-
paths like 'mitoai_ui_tests-app_builde-ab3a5-n-Test-Preview-as-Streamlit-chromium/'
|
|
33
|
-
that the backend can't directly access.
|
|
34
|
-
|
|
35
|
-
2. **JupyterHub/Cloud Deployments**: In cloud environments, users may have notebooks
|
|
36
|
-
in subdirectories that aren't immediately accessible from the server root.
|
|
37
|
-
|
|
38
|
-
3. **Docker Containers**: When running in containers, the working directory and
|
|
39
|
-
file paths may not align with what the frontend reports.
|
|
40
|
-
|
|
41
|
-
4. **Multi-user Environments**: In enterprise deployments, users may have notebooks
|
|
42
|
-
in user-specific directories that require path resolution.
|
|
43
|
-
|
|
44
|
-
The method tries multiple strategies:
|
|
45
|
-
1. If the path is already absolute, return it as-is
|
|
46
|
-
2. Try to resolve relative to the Jupyter server's root directory
|
|
47
|
-
3. Search recursively through subdirectories for a file with the same name
|
|
48
|
-
4. Return the original path if not found (will cause a clear error message)
|
|
49
|
-
|
|
50
|
-
Args:
|
|
51
|
-
notebook_path (str): The notebook path from the frontend (may be relative or absolute)
|
|
52
|
-
|
|
53
|
-
Returns:
|
|
54
|
-
str: The resolved absolute path to the notebook file
|
|
55
|
-
"""
|
|
56
|
-
# If the path is already absolute, return it
|
|
57
|
-
if os.path.isabs(notebook_path):
|
|
58
|
-
return notebook_path
|
|
59
|
-
|
|
60
|
-
# Get the Jupyter server's root directory
|
|
61
|
-
server_root = self.settings.get("server_root_dir", os.getcwd())
|
|
62
|
-
|
|
63
|
-
# Try to find the notebook file in the server root
|
|
64
|
-
resolved_path = os.path.join(server_root, notebook_path)
|
|
65
|
-
if os.path.exists(resolved_path):
|
|
66
|
-
return resolved_path
|
|
67
|
-
|
|
68
|
-
# If not found, try to find it in subdirectories
|
|
69
|
-
# This handles cases where the notebook is in a subdirectory that the frontend
|
|
70
|
-
# doesn't know about, or where the path structure differs between frontend and backend
|
|
71
|
-
for root, dirs, files in os.walk(server_root):
|
|
72
|
-
if os.path.basename(notebook_path) in files:
|
|
73
|
-
return os.path.join(root, os.path.basename(notebook_path))
|
|
74
|
-
|
|
75
|
-
# If still not found, return the original path (will cause a clear error)
|
|
76
|
-
# This ensures we get a meaningful error message rather than a generic "file not found"
|
|
77
|
-
return os.path.join(os.getcwd(), notebook_path)
|
|
22
|
+
self.preview_manager = StreamlitPreviewManager()
|
|
78
23
|
|
|
79
24
|
@tornado.web.authenticated
|
|
80
25
|
async def post(self) -> None:
|
|
@@ -82,42 +27,66 @@ class StreamlitPreviewHandler(APIHandler):
|
|
|
82
27
|
try:
|
|
83
28
|
# Parse and validate request
|
|
84
29
|
body = self.get_json_body()
|
|
85
|
-
|
|
86
|
-
if not is_valid or not notebook_path:
|
|
87
|
-
self.set_status(400)
|
|
88
|
-
self.finish({"error": error_msg})
|
|
89
|
-
return
|
|
30
|
+
notebook_path, notebook_id, force_recreate, edit_prompt = validate_request_body(body)
|
|
90
31
|
|
|
91
32
|
# Ensure app exists
|
|
92
|
-
|
|
33
|
+
absolute_notebook_path = get_absolute_notebook_path(notebook_path)
|
|
34
|
+
absolute_notebook_dir_path = get_absolute_notebook_dir_path(absolute_notebook_path)
|
|
35
|
+
app_file_name = get_app_file_name(notebook_id)
|
|
36
|
+
absolute_app_path = get_absolute_app_path(absolute_notebook_dir_path, app_file_name)
|
|
37
|
+
app_path_exists = does_app_path_exist(absolute_app_path)
|
|
93
38
|
|
|
94
|
-
|
|
39
|
+
if not app_path_exists or force_recreate:
|
|
40
|
+
if not app_path_exists:
|
|
41
|
+
print("[Mito AI] App path not found, generating streamlit code")
|
|
42
|
+
else:
|
|
43
|
+
print("[Mito AI] Force recreating streamlit app")
|
|
95
44
|
|
|
96
|
-
|
|
97
|
-
self.set_status(500)
|
|
98
|
-
self.finish({"error": error_msg})
|
|
99
|
-
return
|
|
45
|
+
await streamlit_handler(absolute_notebook_path, app_file_name, edit_prompt)
|
|
100
46
|
|
|
101
47
|
# Start preview
|
|
102
48
|
# TODO: There's a bug here where when the user rebuilds and already running app. Instead of
|
|
103
49
|
# creating a new process, we should update the existing process. The app displayed to the user
|
|
104
50
|
# does update, but that's just because of hot reloading when we overwrite the app.py file.
|
|
105
51
|
preview_id = str(uuid.uuid4())
|
|
106
|
-
|
|
107
|
-
success, message, port = self.preview_manager.start_streamlit_preview(resolved_app_directory, preview_id)
|
|
108
|
-
|
|
109
|
-
if not success:
|
|
110
|
-
self.set_status(500)
|
|
111
|
-
self.finish({"error": f"Failed to start preview: {message}"})
|
|
112
|
-
return
|
|
52
|
+
port = self.preview_manager.start_streamlit_preview(absolute_notebook_dir_path, app_file_name, preview_id)
|
|
113
53
|
|
|
114
54
|
# Return success response
|
|
115
|
-
self.finish({
|
|
116
|
-
|
|
55
|
+
self.finish({
|
|
56
|
+
"type": 'success',
|
|
57
|
+
"id": preview_id,
|
|
58
|
+
"port": port,
|
|
59
|
+
"url": f"http://localhost:{port}"
|
|
60
|
+
})
|
|
61
|
+
log_streamlit_app_preview_success('mito_server_key', MessageType.STREAMLIT_CONVERSION, edit_prompt)
|
|
62
|
+
|
|
63
|
+
except StreamlitConversionError as e:
|
|
64
|
+
print(e)
|
|
65
|
+
self.set_status(e.error_code)
|
|
66
|
+
error_message = str(e)
|
|
67
|
+
formatted_traceback = traceback.format_exc()
|
|
68
|
+
self.finish({"error": error_message})
|
|
69
|
+
log_streamlit_app_conversion_error(
|
|
70
|
+
'mito_server_key',
|
|
71
|
+
MessageType.STREAMLIT_CONVERSION,
|
|
72
|
+
error_message,
|
|
73
|
+
formatted_traceback,
|
|
74
|
+
edit_prompt,
|
|
75
|
+
)
|
|
76
|
+
except StreamlitPreviewError as e:
|
|
77
|
+
print(e)
|
|
78
|
+
error_message = str(e)
|
|
79
|
+
formatted_traceback = traceback.format_exc()
|
|
80
|
+
self.set_status(e.error_code)
|
|
81
|
+
self.finish({"error": error_message})
|
|
82
|
+
log_streamlit_app_preview_failure('mito_server_key', MessageType.STREAMLIT_CONVERSION, error_message, formatted_traceback, edit_prompt)
|
|
117
83
|
except Exception as e:
|
|
118
|
-
print(f"
|
|
84
|
+
print(f"Exception in streamlit preview handler: {e}")
|
|
119
85
|
self.set_status(500)
|
|
120
|
-
|
|
86
|
+
error_message = str(e)
|
|
87
|
+
formatted_traceback = traceback.format_exc()
|
|
88
|
+
self.finish({"error": error_message})
|
|
89
|
+
log_streamlit_app_preview_failure('mito_server_key', MessageType.STREAMLIT_CONVERSION, error_message, formatted_traceback, "")
|
|
121
90
|
|
|
122
91
|
@tornado.web.authenticated
|
|
123
92
|
def delete(self, preview_id: str) -> None:
|
|
@@ -1,16 +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
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.path_utils import AbsoluteNotebookDirPath, AppFileName
|
|
13
|
+
from mito_ai.utils.error_classes import StreamlitPreviewError
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
@dataclass
|
|
@@ -37,7 +37,7 @@ class StreamlitPreviewManager:
|
|
|
37
37
|
|
|
38
38
|
return port
|
|
39
39
|
|
|
40
|
-
def start_streamlit_preview(self, app_directory:
|
|
40
|
+
def start_streamlit_preview(self, app_directory: AbsoluteNotebookDirPath, app_file_name: AppFileName, preview_id: str) -> int:
|
|
41
41
|
"""Start a streamlit preview process.
|
|
42
42
|
|
|
43
43
|
Args:
|
|
@@ -47,6 +47,7 @@ class StreamlitPreviewManager:
|
|
|
47
47
|
Returns:
|
|
48
48
|
Tuple of (success, message, port)
|
|
49
49
|
"""
|
|
50
|
+
|
|
50
51
|
try:
|
|
51
52
|
|
|
52
53
|
# Get free port
|
|
@@ -54,12 +55,12 @@ class StreamlitPreviewManager:
|
|
|
54
55
|
|
|
55
56
|
# Start streamlit process
|
|
56
57
|
cmd = [
|
|
57
|
-
"streamlit", "run",
|
|
58
|
+
"streamlit", "run", app_file_name,
|
|
58
59
|
"--server.port", str(port),
|
|
59
60
|
"--server.headless", "true",
|
|
60
61
|
"--server.address", "localhost",
|
|
61
62
|
"--server.enableXsrfProtection", "false",
|
|
62
|
-
"--server.runOnSave", "true", # auto-reload when app
|
|
63
|
+
"--server.runOnSave", "true", # auto-reload when app is saved
|
|
63
64
|
"--logger.level", "error"
|
|
64
65
|
]
|
|
65
66
|
|
|
@@ -80,7 +81,7 @@ class StreamlitPreviewManager:
|
|
|
80
81
|
if not ready:
|
|
81
82
|
proc.terminate()
|
|
82
83
|
proc.wait()
|
|
83
|
-
|
|
84
|
+
raise StreamlitPreviewError("Streamlit app failed to start as app is not ready", 500)
|
|
84
85
|
|
|
85
86
|
# Register the process
|
|
86
87
|
with self._lock:
|
|
@@ -90,11 +91,11 @@ class StreamlitPreviewManager:
|
|
|
90
91
|
)
|
|
91
92
|
|
|
92
93
|
self.log.info(f"Started streamlit preview {preview_id} on port {port}")
|
|
93
|
-
return
|
|
94
|
+
return port
|
|
94
95
|
|
|
95
96
|
except Exception as e:
|
|
96
97
|
self.log.error(f"Error starting streamlit preview: {e}")
|
|
97
|
-
|
|
98
|
+
raise StreamlitPreviewError(f"Failed to start preview: {str(e)}", 500)
|
|
98
99
|
|
|
99
100
|
def _wait_for_app_ready(self, port: int, timeout: int = 30) -> bool:
|
|
100
101
|
"""Wait for streamlit app to be ready on the given port."""
|
|
@@ -106,7 +107,7 @@ class StreamlitPreviewManager:
|
|
|
106
107
|
if response.status_code == 200:
|
|
107
108
|
return True
|
|
108
109
|
except requests.exceptions.RequestException as e:
|
|
109
|
-
|
|
110
|
+
self.log.info(f"Waiting for app to be ready...")
|
|
110
111
|
pass
|
|
111
112
|
|
|
112
113
|
time.sleep(1)
|
|
@@ -122,7 +123,7 @@ class StreamlitPreviewManager:
|
|
|
122
123
|
Returns:
|
|
123
124
|
True if stopped successfully, False if not found
|
|
124
125
|
"""
|
|
125
|
-
|
|
126
|
+
self.log.info(f"Stopping preview {preview_id}")
|
|
126
127
|
with self._lock:
|
|
127
128
|
if preview_id not in self._previews:
|
|
128
129
|
return False
|
|
@@ -149,11 +150,3 @@ class StreamlitPreviewManager:
|
|
|
149
150
|
"""Get a preview process by ID."""
|
|
150
151
|
with self._lock:
|
|
151
152
|
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,28 @@
|
|
|
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, 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)
|
|
16
|
+
|
|
17
|
+
notebook_id = body.get("notebook_id")
|
|
18
|
+
if not notebook_id:
|
|
19
|
+
raise StreamlitPreviewError("Missing notebook_id parameter", 400)
|
|
18
20
|
|
|
19
21
|
force_recreate = body.get("force_recreate", False)
|
|
20
22
|
if not isinstance(force_recreate, bool):
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
raise StreamlitPreviewError("force_recreate must be a boolean", 400)
|
|
24
|
+
|
|
23
25
|
edit_prompt = body.get("edit_prompt", "")
|
|
24
26
|
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}"
|
|
27
|
+
raise StreamlitPreviewError("edit_prompt must be a string", 400)
|
|
44
28
|
|
|
45
|
-
return
|
|
29
|
+
return notebook_path, notebook_id, force_recreate, edit_prompt
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import zipfile
|
|
5
5
|
import logging
|
|
6
6
|
from mito_ai.app_deploy.app_deploy_utils import add_files_to_zip
|
|
7
|
+
from mito_ai.path_utils import AbsoluteNotebookDirPath
|
|
7
8
|
|
|
8
9
|
class TestAddFilesToZip:
|
|
9
10
|
"""Test cases for add_files_to_zip helper function"""
|
|
@@ -17,13 +18,30 @@ class TestAddFilesToZip:
|
|
|
17
18
|
f2.write_text("file2 content")
|
|
18
19
|
|
|
19
20
|
zip_path = tmp_path / "test.zip"
|
|
20
|
-
add_files_to_zip(str(zip_path), str(tmp_path), ["file1.txt", "file2.txt"])
|
|
21
|
+
add_files_to_zip(str(zip_path), AbsoluteNotebookDirPath(str(tmp_path)), ["file1.txt", "file2.txt"], 'test-app-file-name.py')
|
|
21
22
|
|
|
22
23
|
with zipfile.ZipFile(zip_path, "r") as zf:
|
|
23
24
|
names = zf.namelist()
|
|
24
25
|
assert "file1.txt" in names
|
|
25
26
|
assert "file2.txt" in names
|
|
26
27
|
assert len(names) == 2
|
|
28
|
+
|
|
29
|
+
def test_renames_app_file(self, tmp_path):
|
|
30
|
+
"""Ensure individual files are added correctly to the zip"""
|
|
31
|
+
# Create files
|
|
32
|
+
f1 = tmp_path / "original-file-name.py"
|
|
33
|
+
f1.write_text("file1 content")
|
|
34
|
+
f2 = tmp_path / "file2.txt"
|
|
35
|
+
f2.write_text("file2 content")
|
|
36
|
+
|
|
37
|
+
zip_path = tmp_path / "test.zip"
|
|
38
|
+
add_files_to_zip(str(zip_path), AbsoluteNotebookDirPath(str(tmp_path)), ["original-file-name.py", "file2.txt"], 'original-file-name.py')
|
|
39
|
+
|
|
40
|
+
with zipfile.ZipFile(zip_path, "r") as zf:
|
|
41
|
+
names = zf.namelist()
|
|
42
|
+
assert "app.py" in names
|
|
43
|
+
assert "file2.txt" in names
|
|
44
|
+
assert len(names) == 2
|
|
27
45
|
|
|
28
46
|
def test_directories_added_recursively(self, tmp_path):
|
|
29
47
|
"""Ensure directories are added recursively with correct relative paths"""
|
|
@@ -35,7 +53,7 @@ class TestAddFilesToZip:
|
|
|
35
53
|
(subfolder / "nested2.txt").write_text("nested2 content")
|
|
36
54
|
|
|
37
55
|
zip_path = tmp_path / "test.zip"
|
|
38
|
-
add_files_to_zip(str(zip_path), str(tmp_path), ["folder"])
|
|
56
|
+
add_files_to_zip(str(zip_path), AbsoluteNotebookDirPath(str(tmp_path)), ["folder"], 'test-app.py')
|
|
39
57
|
|
|
40
58
|
with zipfile.ZipFile(zip_path, "r") as zf:
|
|
41
59
|
names = zf.namelist()
|
|
@@ -46,7 +64,7 @@ class TestAddFilesToZip:
|
|
|
46
64
|
"""Ensure missing files do not break the function and warning is logged"""
|
|
47
65
|
caplog.set_level(logging.WARNING)
|
|
48
66
|
zip_path = tmp_path / "test.zip"
|
|
49
|
-
add_files_to_zip(str(zip_path), str(tmp_path), ["does_not_exist.txt"], logger=logging.getLogger())
|
|
67
|
+
add_files_to_zip(str(zip_path), AbsoluteNotebookDirPath(str(tmp_path)), ["does_not_exist.txt"], 'test-app.py', logger=logging.getLogger())
|
|
50
68
|
|
|
51
69
|
# Zip should exist but be empty
|
|
52
70
|
with zipfile.ZipFile(zip_path, "r") as zf:
|
|
@@ -63,7 +81,7 @@ class TestAddFilesToZip:
|
|
|
63
81
|
(folder / "nested.txt").write_text("nested content")
|
|
64
82
|
|
|
65
83
|
zip_path = tmp_path / "test.zip"
|
|
66
|
-
add_files_to_zip(str(zip_path), str(tmp_path), ["file.txt", "folder"])
|
|
84
|
+
add_files_to_zip(str(zip_path), AbsoluteNotebookDirPath(str(tmp_path)), ["file.txt", "folder"], 'test-app.py')
|
|
67
85
|
|
|
68
86
|
with zipfile.ZipFile(zip_path, "r") as zf:
|
|
69
87
|
names = zf.namelist()
|
|
@@ -106,12 +106,15 @@ PROMPT_BUILDER_TEST_CASES = [
|
|
|
106
106
|
AgentExecutionMetadata(
|
|
107
107
|
variables=TEST_VARIABLES,
|
|
108
108
|
files=TEST_FILES,
|
|
109
|
+
notebookPath='/test-notebook-path.ipynb',
|
|
110
|
+
notebookID='test-notebook-id',
|
|
109
111
|
aiOptimizedCells=[
|
|
110
112
|
AIOptimizedCell(cell_type="code", id="cell1", code=TEST_CODE)
|
|
111
113
|
],
|
|
112
114
|
input=TEST_INPUT,
|
|
113
115
|
promptType="agent:execution",
|
|
114
116
|
threadId=ThreadID("test-thread-id"),
|
|
117
|
+
activeCellId="cell1",
|
|
115
118
|
isChromeBrowser=True
|
|
116
119
|
)
|
|
117
120
|
),
|
|
@@ -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
|
+
|