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.

Files changed (55) hide show
  1. mito_ai/_version.py +1 -1
  2. mito_ai/app_deploy/handlers.py +3 -2
  3. mito_ai/completions/models.py +1 -0
  4. mito_ai/completions/prompt_builders/agent_execution_prompt.py +4 -0
  5. mito_ai/completions/prompt_builders/agent_system_message.py +1 -1
  6. mito_ai/path_utils.py +1 -1
  7. mito_ai/streamlit_conversion/search_replace_utils.py +4 -3
  8. mito_ai/streamlit_conversion/streamlit_agent_handler.py +6 -7
  9. mito_ai/streamlit_conversion/streamlit_utils.py +1 -2
  10. mito_ai/streamlit_conversion/validate_streamlit_app.py +2 -4
  11. mito_ai/streamlit_preview/__init__.py +1 -2
  12. mito_ai/streamlit_preview/handlers.py +17 -9
  13. mito_ai/streamlit_preview/manager.py +2 -11
  14. mito_ai/streamlit_preview/utils.py +1 -18
  15. mito_ai/tests/message_history/test_message_history_utils.py +1 -0
  16. mito_ai/tests/streamlit_conversion/test_apply_search_replace.py +18 -4
  17. mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +12 -9
  18. mito_ai/tests/streamlit_conversion/test_validate_streamlit_app.py +20 -18
  19. mito_ai/tests/streamlit_preview/test_streamlit_preview_handler.py +80 -52
  20. mito_ai/tests/streamlit_preview/test_streamlit_preview_manager.py +2 -11
  21. mito_ai/utils/telemetry_utils.py +9 -9
  22. {mito_ai-0.1.47.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +100 -100
  23. {mito_ai-0.1.47.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/package.json +2 -2
  24. {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
  25. 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
  26. mito_ai-0.1.48.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.5c7d84a45ddeb5704b61.js.map +1 -0
  27. 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
  28. 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
  29. {mito_ai-0.1.47.dist-info → mito_ai-0.1.48.dist-info}/METADATA +1 -1
  30. {mito_ai-0.1.47.dist-info → mito_ai-0.1.48.dist-info}/RECORD +54 -54
  31. mito_ai-0.1.47.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.2db61d2b629817845901.js.map +0 -1
  32. {mito_ai-0.1.47.data → mito_ai-0.1.48.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
  33. {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
  34. {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
  35. {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
  36. {mito_ai-0.1.47.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
  37. {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
  38. {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
  39. {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
  40. {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
  41. {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
  42. {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
  43. {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
  44. {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
  45. {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
  46. {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
  47. {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
  48. {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
  49. {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
  50. {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
  51. {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
  52. {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
  53. {mito_ai-0.1.47.dist-info → mito_ai-0.1.48.dist-info}/WHEEL +0 -0
  54. {mito_ai-0.1.47.dist-info → mito_ai-0.1.48.dist-info}/entry_points.txt +0 -0
  55. {mito_ai-0.1.47.dist-info → mito_ai-0.1.48.dist-info}/licenses/LICENSE +0 -0
mito_ai/_version.py CHANGED
@@ -1,4 +1,4 @@
1
1
  # This file is auto-generated by Hatchling. As such, do not:
2
2
  # - modify
3
3
  # - track in version control e.g. be sure to add to .gitignore
4
- __version__ = VERSION = '0.1.47'
4
+ __version__ = VERSION = '0.1.48'
@@ -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, does_app_path_exists, get_absolute_app_path, get_absolute_notebook_dir_path, get_absolute_notebook_path
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 = does_app_path_exists(app_path)
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",
@@ -97,6 +97,7 @@ class AgentExecutionMetadata():
97
97
  threadId: ThreadID
98
98
  input: str
99
99
  aiOptimizedCells: List[AIOptimizedCell]
100
+ activeCellId: str
100
101
  isChromeBrowser: bool
101
102
  base64EncodedActiveCellOutput: Optional[str] = None
102
103
  variables: Optional[List[str]] = None
@@ -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 does_app_path_exists(app_path: AbsoluteAppPath) -> bool:
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
- raise StreamlitConversionError(f"Search text found {count} times (must be unique): {repr(search_text)}", error_code=500)
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, Optional, Tuple, cast
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
- has_validation_error, errors = validate_app(streamlit_code, notebook_path)
124
+ errors = validate_app(streamlit_code, notebook_path)
126
125
  tries = 0
127
- while has_validation_error and tries < 5:
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
- has_validation_error, errors = validate_app(streamlit_code, notebook_path)
130
+ errors = validate_app(streamlit_code, notebook_path)
132
131
 
133
- if has_validation_error:
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 has_validation_error:
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, Tuple, Any
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) -> Tuple[bool, List[str]]:
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 has_validation_error, stringified_errors
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__ = ['get_preview_manager', 'StreamlitPreviewHandler']
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 ensure_app_exists, validate_request_body
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 get_preview_manager
11
- from mito_ai.path_utils import AbsoluteNotebookPath, get_absolute_notebook_dir_path, get_absolute_notebook_path
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 = get_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
- await ensure_app_exists(absolute_notebook_path, force_recreate, edit_prompt)
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
- absolute_app_directory = get_absolute_notebook_dir_path(absolute_notebook_path)
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
- print(f"Waiting for app to be ready...")
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
- print(f"Stopping preview {preview_id}")
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)
@@ -112,6 +112,7 @@ PROMPT_BUILDER_TEST_CASES = [
112
112
  input=TEST_INPUT,
113
113
  promptType="agent:execution",
114
114
  threadId=ThreadID("test-thread-id"),
115
+ activeCellId="cell1",
115
116
  isChromeBrowser=True
116
117
  )
117
118
  ),
@@ -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
- def test_apply_search_replace_multiple_matches():
224
- """Test that ValueError is raised when search text is found multiple times."""
225
- with pytest.raises(StreamlitConversionError, match="Search text found 2 times"):
226
- apply_search_replace("st.write(\"Hello\")\nst.write(\"Hello\")", [("st.write(\"Hello\")", "st.write(\"Hi\")")])
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
- async def test_streamlit_handler_success(self, mock_create_file, mock_validator, mock_generate_code, mock_parse):
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 = (False, "")
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
- async def test_streamlit_handler_max_retries_exceeded(self, mock_validator, mock_correct_error, mock_generate_code, mock_parse):
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) - Return list of errors as expected by validate_app
183
- mock_validator.return_value = (True, ["Persistent error"])
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 (max retries)
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 = (False, "")
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, match="Permission denied"):
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
- "import streamlit\nst.title('Hello World')",
24
- None,
25
- "valid Python code"
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
- "import streamlit\nst.title('Hello World'",
30
- "SyntaxError",
31
- "invalid Python code"
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
- None,
37
- "empty code"
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,expected_has_validation_error,expected_error_message", [
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
- ("print('Hello World'", True, "SyntaxError"),
99
+ # Syntax errors are caught during runtime
101
100
  ("", False, ""),
102
101
  ])
103
- def test_validate_app(self, app_code, expected_has_validation_error, expected_error_message):
102
+ def test_validate_app(self, app_code, expected_has_errors, expected_error_message):
104
103
  """Test the validate_app function"""
105
- has_validation_error, errors = validate_app(app_code, AbsoluteNotebookPath('/notebook.ipynb'))
104
+ errors = validate_app(app_code, AbsoluteNotebookPath('/notebook.ipynb'))
106
105
 
107
- assert has_validation_error == expected_has_validation_error
108
- assert expected_error_message in str(errors)
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