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.
Files changed (74) hide show
  1. mito_ai/_version.py +1 -1
  2. mito_ai/app_deploy/app_deploy_utils.py +28 -9
  3. mito_ai/app_deploy/handlers.py +123 -84
  4. mito_ai/app_deploy/models.py +19 -12
  5. mito_ai/completions/models.py +6 -1
  6. mito_ai/completions/prompt_builders/agent_execution_prompt.py +13 -1
  7. mito_ai/completions/prompt_builders/agent_system_message.py +63 -4
  8. mito_ai/completions/prompt_builders/chat_system_message.py +4 -0
  9. mito_ai/completions/prompt_builders/prompt_constants.py +1 -0
  10. mito_ai/completions/prompt_builders/utils.py +13 -0
  11. mito_ai/path_utils.py +70 -0
  12. mito_ai/streamlit_conversion/agent_utils.py +4 -201
  13. mito_ai/streamlit_conversion/prompts/prompt_constants.py +142 -152
  14. mito_ai/streamlit_conversion/prompts/streamlit_error_correction_prompt.py +3 -3
  15. mito_ai/streamlit_conversion/prompts/streamlit_finish_todo_prompt.py +2 -2
  16. mito_ai/streamlit_conversion/prompts/update_existing_app_prompt.py +2 -2
  17. mito_ai/streamlit_conversion/search_replace_utils.py +94 -0
  18. mito_ai/streamlit_conversion/streamlit_agent_handler.py +35 -46
  19. mito_ai/streamlit_conversion/streamlit_utils.py +13 -75
  20. mito_ai/streamlit_conversion/validate_streamlit_app.py +6 -21
  21. mito_ai/streamlit_preview/__init__.py +1 -2
  22. mito_ai/streamlit_preview/handlers.py +54 -85
  23. mito_ai/streamlit_preview/manager.py +11 -18
  24. mito_ai/streamlit_preview/utils.py +12 -28
  25. mito_ai/tests/deploy_app/test_app_deploy_utils.py +22 -4
  26. mito_ai/tests/message_history/test_message_history_utils.py +3 -0
  27. mito_ai/tests/streamlit_conversion/test_apply_search_replace.py +240 -0
  28. mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +40 -60
  29. mito_ai/tests/streamlit_conversion/test_streamlit_utils.py +26 -29
  30. mito_ai/tests/streamlit_conversion/test_validate_streamlit_app.py +25 -20
  31. mito_ai/tests/streamlit_preview/test_streamlit_preview_handler.py +87 -57
  32. mito_ai/tests/streamlit_preview/test_streamlit_preview_manager.py +27 -40
  33. mito_ai/user/handlers.py +15 -3
  34. mito_ai/utils/create.py +17 -1
  35. mito_ai/utils/error_classes.py +42 -0
  36. mito_ai/utils/message_history_utils.py +3 -1
  37. mito_ai/utils/telemetry_utils.py +78 -13
  38. {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +100 -100
  39. {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/package.json +2 -2
  40. {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
  41. 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
  42. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.8f1845da6bf2b128c049.js.map +1 -0
  43. 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
  44. 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
  45. {mito_ai-0.1.46.dist-info → mito_ai-0.1.49.dist-info}/METADATA +1 -1
  46. {mito_ai-0.1.46.dist-info → mito_ai-0.1.49.dist-info}/RECORD +71 -69
  47. mito_ai/tests/streamlit_conversion/test_apply_patch_to_text.py +0 -368
  48. mito_ai/tests/streamlit_conversion/test_fix_diff_headers.py +0 -533
  49. mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.20f12766ecd3d430568e.js.map +0 -1
  50. /mito_ai/streamlit_conversion/{streamlit_system_prompt.py → prompts/streamlit_system_prompt.py} +0 -0
  51. {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
  52. {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
  53. {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
  54. {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
  55. {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
  56. {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
  57. {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
  58. {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
  59. {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
  60. {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
  61. {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
  62. {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
  63. {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
  64. {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
  65. {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
  66. {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
  67. {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
  68. {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
  69. {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
  70. {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
  71. {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
  72. {mito_ai-0.1.46.dist-info → mito_ai-0.1.49.dist-info}/WHEEL +0 -0
  73. {mito_ai-0.1.46.dist-info → mito_ai-0.1.49.dist-info}/entry_points.txt +0 -0
  74. {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.streamlit_conversion.streamlit_utils import get_app_path
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
- from mito_ai.streamlit_preview.manager import get_preview_manager
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 = get_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
- is_valid, error_msg, notebook_path, force_recreate, edit_prompt = validate_request_body(body)
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
- resolved_notebook_path = self._resolve_notebook_path(notebook_path)
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
- success, error_msg = await ensure_app_exists(resolved_notebook_path, force_recreate, edit_prompt)
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
- if not success:
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
- resolved_app_directory = os.path.dirname(resolved_notebook_path)
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({"id": preview_id, "port": port, "url": f"http://localhost:{port}"})
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"Error in streamlit preview handler: {e}")
84
+ print(f"Exception in streamlit preview handler: {e}")
119
85
  self.set_status(500)
120
- self.finish({"error": str(e)})
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: str, preview_id: str) -> Tuple[bool, str, Optional[int]]:
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", 'app.py', # Since we run this command from the app_directory, we always just run app.py
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.py is saved
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
- return False, "Streamlit app failed to start", None
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 True, "Preview started successfully", port
94
+ return port
94
95
 
95
96
  except Exception as e:
96
97
  self.log.error(f"Error starting streamlit preview: {e}")
97
- return False, f"Failed to start preview: {str(e)}", None
98
+ raise StreamlitPreviewError(f"Failed to start preview: {str(e)}", 500)
98
99
 
99
100
  def _wait_for_app_ready(self, port: int, timeout: int = 30) -> bool:
100
101
  """Wait for streamlit app to be ready on the given port."""
@@ -106,7 +107,7 @@ class StreamlitPreviewManager:
106
107
  if response.status_code == 200:
107
108
  return True
108
109
  except requests.exceptions.RequestException as e:
109
- print(f"Error waiting for app to be ready: {e}")
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
- print(f"Stopping preview {preview_id}")
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 os
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[bool, str, Optional[str], bool, str]:
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
- return False, "Invalid or missing JSON body", None, False, ""
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
- return False, "Missing notebook_path parameter", None, False, ""
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
- return False, "force_recreate must be a boolean", None, False, ""
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
- return False, "edit_prompt must be a string", None, False, ""
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 True, ""
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
+