mito-ai 0.1.46__py3-none-any.whl → 0.1.48__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of mito-ai might be problematic. Click here for more details.

Files changed (72) hide show
  1. mito_ai/_version.py +1 -1
  2. mito_ai/app_deploy/handlers.py +98 -77
  3. mito_ai/app_deploy/models.py +16 -12
  4. mito_ai/completions/models.py +5 -1
  5. mito_ai/completions/prompt_builders/agent_execution_prompt.py +10 -1
  6. mito_ai/completions/prompt_builders/agent_system_message.py +63 -4
  7. mito_ai/completions/prompt_builders/chat_system_message.py +4 -0
  8. mito_ai/completions/prompt_builders/prompt_constants.py +1 -0
  9. mito_ai/completions/prompt_builders/utils.py +14 -0
  10. mito_ai/path_utils.py +56 -0
  11. mito_ai/streamlit_conversion/agent_utils.py +4 -201
  12. mito_ai/streamlit_conversion/prompts/prompt_constants.py +142 -152
  13. mito_ai/streamlit_conversion/prompts/streamlit_error_correction_prompt.py +3 -3
  14. mito_ai/streamlit_conversion/prompts/streamlit_finish_todo_prompt.py +2 -2
  15. mito_ai/streamlit_conversion/prompts/update_existing_app_prompt.py +2 -2
  16. mito_ai/streamlit_conversion/search_replace_utils.py +94 -0
  17. mito_ai/streamlit_conversion/streamlit_agent_handler.py +35 -46
  18. mito_ai/streamlit_conversion/streamlit_utils.py +12 -66
  19. mito_ai/streamlit_conversion/validate_streamlit_app.py +6 -21
  20. mito_ai/streamlit_preview/__init__.py +1 -2
  21. mito_ai/streamlit_preview/handlers.py +53 -85
  22. mito_ai/streamlit_preview/manager.py +7 -16
  23. mito_ai/streamlit_preview/utils.py +8 -28
  24. mito_ai/tests/message_history/test_message_history_utils.py +1 -0
  25. mito_ai/tests/streamlit_conversion/test_apply_search_replace.py +240 -0
  26. mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +39 -60
  27. mito_ai/tests/streamlit_conversion/test_streamlit_utils.py +26 -29
  28. mito_ai/tests/streamlit_conversion/test_validate_streamlit_app.py +25 -20
  29. mito_ai/tests/streamlit_preview/test_streamlit_preview_handler.py +81 -56
  30. mito_ai/tests/streamlit_preview/test_streamlit_preview_manager.py +24 -37
  31. mito_ai/user/handlers.py +15 -3
  32. mito_ai/utils/create.py +17 -1
  33. mito_ai/utils/error_classes.py +42 -0
  34. mito_ai/utils/message_history_utils.py +3 -1
  35. mito_ai/utils/telemetry_utils.py +78 -13
  36. {mito_ai-0.1.46.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +100 -100
  37. {mito_ai-0.1.46.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/package.json +2 -2
  38. {mito_ai-0.1.46.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/package.json.orig +1 -1
  39. mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.20f12766ecd3d430568e.js → mito_ai-0.1.48.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.5c7d84a45ddeb5704b61.js +1515 -449
  40. mito_ai-0.1.48.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.5c7d84a45ddeb5704b61.js.map +1 -0
  41. mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.54126ab6511271265443.js → mito_ai-0.1.48.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.045d65d1de6fde3f3b72.js +18 -18
  42. mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.54126ab6511271265443.js.map → mito_ai-0.1.48.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.045d65d1de6fde3f3b72.js.map +1 -1
  43. {mito_ai-0.1.46.dist-info → mito_ai-0.1.48.dist-info}/METADATA +1 -1
  44. {mito_ai-0.1.46.dist-info → mito_ai-0.1.48.dist-info}/RECORD +69 -67
  45. mito_ai/tests/streamlit_conversion/test_apply_patch_to_text.py +0 -368
  46. mito_ai/tests/streamlit_conversion/test_fix_diff_headers.py +0 -533
  47. mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.20f12766ecd3d430568e.js.map +0 -1
  48. /mito_ai/streamlit_conversion/{streamlit_system_prompt.py → prompts/streamlit_system_prompt.py} +0 -0
  49. {mito_ai-0.1.46.data → mito_ai-0.1.48.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
  50. {mito_ai-0.1.46.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/toolbar-buttons.json +0 -0
  51. {mito_ai-0.1.46.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js +0 -0
  52. {mito_ai-0.1.46.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js.map +0 -0
  53. {mito_ai-0.1.46.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
  54. {mito_ai-0.1.46.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js +0 -0
  55. {mito_ai-0.1.46.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js.map +0 -0
  56. {mito_ai-0.1.46.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_apis_signOut_mjs-node_module-75790d.688c25857e7b81b1740f.js +0 -0
  57. {mito_ai-0.1.46.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_apis_signOut_mjs-node_module-75790d.688c25857e7b81b1740f.js.map +0 -0
  58. {mito_ai-0.1.46.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_tokenProvider_tokenProvider_-72f1c8.a917210f057fcfe224ad.js +0 -0
  59. {mito_ai-0.1.46.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_tokenProvider_tokenProvider_-72f1c8.a917210f057fcfe224ad.js.map +0 -0
  60. {mito_ai-0.1.46.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js +0 -0
  61. {mito_ai-0.1.46.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js.map +0 -0
  62. {mito_ai-0.1.46.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js +0 -0
  63. {mito_ai-0.1.46.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js.map +0 -0
  64. {mito_ai-0.1.46.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_react-dom_client_js-node_modules_aws-amplify_ui-react_dist_styles_css.b43d4249e4d3dac9ad7b.js +0 -0
  65. {mito_ai-0.1.46.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_react-dom_client_js-node_modules_aws-amplify_ui-react_dist_styles_css.b43d4249e4d3dac9ad7b.js.map +0 -0
  66. {mito_ai-0.1.46.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js +0 -0
  67. {mito_ai-0.1.46.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js.map +0 -0
  68. {mito_ai-0.1.46.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js +0 -0
  69. {mito_ai-0.1.46.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js.map +0 -0
  70. {mito_ai-0.1.46.dist-info → mito_ai-0.1.48.dist-info}/WHEEL +0 -0
  71. {mito_ai-0.1.46.dist-info → mito_ai-0.1.48.dist-info}/entry_points.txt +0 -0
  72. {mito_ai-0.1.46.dist-info → mito_ai-0.1.48.dist-info}/licenses/LICENSE +0 -0
@@ -1,16 +1,15 @@
1
1
  # Copyright (c) Saga Inc.
2
2
  # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
3
 
4
- import os
5
4
  import socket
6
5
  import subprocess
7
- import tempfile
8
6
  import time
9
7
  import threading
10
8
  import requests
11
9
  from typing import Dict, Optional, Tuple
12
10
  from dataclasses import dataclass
13
11
  from mito_ai.logger import get_logger
12
+ from mito_ai.utils.error_classes import StreamlitPreviewError
14
13
 
15
14
 
16
15
  @dataclass
@@ -37,7 +36,7 @@ class StreamlitPreviewManager:
37
36
 
38
37
  return port
39
38
 
40
- def start_streamlit_preview(self, app_directory: str, preview_id: str) -> Tuple[bool, str, Optional[int]]:
39
+ def start_streamlit_preview(self, app_directory: str, preview_id: str) -> int:
41
40
  """Start a streamlit preview process.
42
41
 
43
42
  Args:
@@ -80,7 +79,7 @@ class StreamlitPreviewManager:
80
79
  if not ready:
81
80
  proc.terminate()
82
81
  proc.wait()
83
- return False, "Streamlit app failed to start", None
82
+ raise StreamlitPreviewError("Streamlit app failed to start as app is not ready", 500)
84
83
 
85
84
  # Register the process
86
85
  with self._lock:
@@ -90,11 +89,11 @@ class StreamlitPreviewManager:
90
89
  )
91
90
 
92
91
  self.log.info(f"Started streamlit preview {preview_id} on port {port}")
93
- return True, "Preview started successfully", port
92
+ return port
94
93
 
95
94
  except Exception as e:
96
95
  self.log.error(f"Error starting streamlit preview: {e}")
97
- return False, f"Failed to start preview: {str(e)}", None
96
+ raise StreamlitPreviewError(f"Failed to start preview: {str(e)}", 500)
98
97
 
99
98
  def _wait_for_app_ready(self, port: int, timeout: int = 30) -> bool:
100
99
  """Wait for streamlit app to be ready on the given port."""
@@ -106,7 +105,7 @@ class StreamlitPreviewManager:
106
105
  if response.status_code == 200:
107
106
  return True
108
107
  except requests.exceptions.RequestException as e:
109
- print(f"Error waiting for app to be ready: {e}")
108
+ self.log.info(f"Waiting for app to be ready...")
110
109
  pass
111
110
 
112
111
  time.sleep(1)
@@ -122,7 +121,7 @@ class StreamlitPreviewManager:
122
121
  Returns:
123
122
  True if stopped successfully, False if not found
124
123
  """
125
- print(f"Stopping preview {preview_id}")
124
+ self.log.info(f"Stopping preview {preview_id}")
126
125
  with self._lock:
127
126
  if preview_id not in self._previews:
128
127
  return False
@@ -149,11 +148,3 @@ class StreamlitPreviewManager:
149
148
  """Get a preview process by ID."""
150
149
  with self._lock:
151
150
  return self._previews.get(preview_id)
152
-
153
- # Global instance
154
- _preview_manager = StreamlitPreviewManager()
155
-
156
-
157
- def get_preview_manager() -> StreamlitPreviewManager:
158
- """Get the global preview manager instance."""
159
- return _preview_manager
@@ -2,44 +2,24 @@
2
2
  # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
3
 
4
4
  from typing import Tuple, Optional
5
- import 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, 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)
18
16
 
19
17
  force_recreate = body.get("force_recreate", False)
20
18
  if not isinstance(force_recreate, bool):
21
- return False, "force_recreate must be a boolean", None, False, ""
22
-
19
+ raise StreamlitPreviewError("force_recreate must be a boolean", 400)
20
+
23
21
  edit_prompt = body.get("edit_prompt", "")
24
22
  if not isinstance(edit_prompt, str):
25
- 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}"
23
+ raise StreamlitPreviewError("edit_prompt must be a string", 400)
44
24
 
45
- return True, ""
25
+ return notebook_path, force_recreate, 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
  ),
@@ -0,0 +1,240 @@
1
+ # Copyright (c) Saga Inc.
2
+ # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
+
4
+ from mito_ai.utils.error_classes import StreamlitConversionError
5
+ import pytest
6
+ from mito_ai.streamlit_conversion.search_replace_utils import apply_search_replace
7
+
8
+
9
+ @pytest.mark.parametrize("original_text,search_replace_pairs,expected_result", [
10
+ # Test case 1: Simple title change
11
+ (
12
+ """import streamlit as st
13
+
14
+ st.markdown(\"\"\"
15
+ <style>
16
+ #MainMenu {visibility: hidden;}
17
+ .stAppDeployButton {display:none;}
18
+ footer {visibility: hidden;}
19
+ .stMainBlockContainer {padding: 2rem 1rem 2rem 1rem;}
20
+ </style>
21
+ \"\"\", unsafe_allow_html=True)
22
+
23
+ st.title("Simple Calculation")
24
+
25
+ x = 5
26
+ y = 10
27
+ result = x + y
28
+
29
+ st.write(f"x = {x}")
30
+ st.write(f"y = {y}")
31
+ st.write(f"x + y = {result}")""",
32
+ [("st.title(\"Simple Calculation\")", "st.title(\"Math Examples\")")],
33
+ """import streamlit as st
34
+
35
+ st.markdown(\"\"\"
36
+ <style>
37
+ #MainMenu {visibility: hidden;}
38
+ .stAppDeployButton {display:none;}
39
+ footer {visibility: hidden;}
40
+ .stMainBlockContainer {padding: 2rem 1rem 2rem 1rem;}
41
+ </style>
42
+ \"\"\", unsafe_allow_html=True)
43
+
44
+ st.title("Math Examples")
45
+
46
+ x = 5
47
+ y = 10
48
+ result = x + y
49
+
50
+ st.write(f"x = {x}")
51
+ st.write(f"y = {y}")
52
+ st.write(f"x + y = {result}")"""
53
+ ),
54
+
55
+ # Test case 2: Add new content
56
+ (
57
+ """import streamlit as st
58
+
59
+ st.title("My App")""",
60
+ [("st.title(\"My App\")", """st.title("My App")
61
+ st.header("Welcome")
62
+ st.write("This is a test app")""")],
63
+ """import streamlit as st
64
+
65
+ st.title("My App")
66
+ st.header("Welcome")
67
+ st.write("This is a test app")"""
68
+ ),
69
+
70
+ # Test case 3: Remove lines
71
+ (
72
+ """import streamlit as st
73
+
74
+ st.header("Welcome")
75
+ st.title("My App")
76
+ st.write("This is a test app")""",
77
+ [("""st.header("Welcome")
78
+ st.title("My App")
79
+ st.write("This is a test app")""", "st.title(\"My App\")")],
80
+ """import streamlit as st
81
+
82
+ st.title("My App")"""
83
+ ),
84
+
85
+ # Test case 4: Multiple replacements
86
+ (
87
+ """import streamlit as st
88
+
89
+ st.title("Old Title")
90
+ x = 5
91
+ y = 10
92
+ st.write("Old message")""",
93
+ [
94
+ ("st.title(\"Old Title\")", "st.title(\"New Title\")"),
95
+ ("st.write(\"Old message\")", "st.write(\"New message\")")
96
+ ],
97
+ """import streamlit as st
98
+
99
+ st.title("New Title")
100
+ x = 5
101
+ y = 10
102
+ st.write("New message")"""
103
+ ),
104
+
105
+ # Test case 5: Empty search/replace pairs
106
+ (
107
+ """import streamlit as st
108
+
109
+ st.title("My App")""",
110
+ [],
111
+ """import streamlit as st
112
+
113
+ st.title("My App")"""
114
+ ),
115
+
116
+ # Test case 6: Complex replacement with context
117
+ (
118
+ """import streamlit as st
119
+
120
+ # This is a comment
121
+ st.title("Old Title")
122
+ # Another comment
123
+ x = 5
124
+ y = 10
125
+ # Final comment""",
126
+ [("""# This is a comment
127
+ st.title("Old Title")
128
+ # Another comment""", """# This is a comment
129
+ st.title("New Title")
130
+ # Another comment""")],
131
+ """import streamlit as st
132
+
133
+ # This is a comment
134
+ st.title("New Title")
135
+ # Another comment
136
+ x = 5
137
+ y = 10
138
+ # Final comment"""
139
+ ),
140
+
141
+ # Test case 7: Replace multiple consecutive lines
142
+ (
143
+ """import streamlit as st
144
+
145
+ st.title("My App")
146
+ st.write("Line 1")
147
+ st.write("Line 2")
148
+ st.write("Line 3")
149
+
150
+ x = 5""",
151
+ [("""st.write("Line 1")
152
+ st.write("Line 2")
153
+ st.write("Line 3")""", "st.write(\"New content\")")],
154
+ """import streamlit as st
155
+
156
+ st.title("My App")
157
+ st.write("New content")
158
+
159
+ x = 5"""
160
+ ),
161
+
162
+ # Test case 8: Add lines at the beginning
163
+ (
164
+ """import streamlit as st
165
+
166
+ st.title("My App")""",
167
+ [("import streamlit as st", """import pandas as pd
168
+ import streamlit as st""")],
169
+ """import pandas as pd
170
+ import streamlit as st
171
+
172
+ st.title("My App")"""
173
+ ),
174
+
175
+ # Test case 9: Add lines at the end
176
+ (
177
+ """import streamlit as st
178
+
179
+ st.title("My App")""",
180
+ [("st.title(\"My App\")", """st.title("My App")
181
+
182
+ st.write("Footer content")
183
+ st.write("More footer")""")],
184
+ """import streamlit as st
185
+
186
+ st.title("My App")
187
+
188
+ st.write("Footer content")
189
+ st.write("More footer")"""
190
+ ),
191
+
192
+ # Test case 10: Add emoji to streamlit app title
193
+ (
194
+ """import streamlit as st
195
+
196
+ st.title("My App")
197
+ st.write("Welcome to my application")""",
198
+ [("st.title(\"My App\")", "st.title(\"🚀 My App\")")],
199
+ """import streamlit as st
200
+
201
+ st.title("🚀 My App")
202
+ st.write("Welcome to my application")"""
203
+ ),
204
+
205
+ # Test case 11: Only replace first occurrence when search text exists multiple times
206
+ (
207
+ """import streamlit as st
208
+
209
+ st.write("Hello World")
210
+ st.title("My App")
211
+ st.write("Hello World")
212
+ st.write("Another message")""",
213
+ [("st.write(\"Hello World\")", "st.write(\"Hi There\")")],
214
+ """import streamlit as st
215
+
216
+ st.write("Hi There")
217
+ st.title("My App")
218
+ st.write("Hello World")
219
+ st.write("Another message")"""
220
+ )
221
+ ])
222
+ def test_apply_search_replace(original_text, search_replace_pairs, expected_result):
223
+ """Test the apply_search_replace function with various search/replace scenarios."""
224
+ result = apply_search_replace(original_text, search_replace_pairs)
225
+
226
+ print(f"Original text: {repr(original_text)}")
227
+ print(f"Search/replace pairs: {search_replace_pairs}")
228
+ print(f"Expected result: {repr(expected_result)}")
229
+ print(f"Result: {repr(result)}")
230
+
231
+ assert result == expected_result
232
+
233
+
234
+ def test_apply_search_replace_search_not_found():
235
+ """Test that ValueError is raised when search text is not found."""
236
+ with pytest.raises(StreamlitConversionError, match="Search text not found"):
237
+ apply_search_replace("st.title(\"My App\")", [("st.title(\"Not Found\")", "st.title(\"New Title\")")])
238
+
239
+
240
+
@@ -12,7 +12,7 @@ from mito_ai.streamlit_conversion.streamlit_agent_handler import (
12
12
  correct_error_in_generation,
13
13
  streamlit_handler
14
14
  )
15
- from mito_ai.streamlit_conversion.streamlit_utils import clean_directory_check
15
+ from mito_ai.path_utils import AbsoluteAppPath, AbsoluteNotebookPath, get_absolute_app_path, get_absolute_notebook_dir_path, get_absolute_notebook_path
16
16
 
17
17
  # Add this line to enable async support
18
18
  pytest_plugins = ('pytest_asyncio',)
@@ -102,14 +102,12 @@ class TestCorrectErrorInGeneration:
102
102
  @patch('mito_ai.streamlit_conversion.agent_utils.stream_anthropic_completion_from_mito_server')
103
103
  async def test_correct_error_in_generation_success(self, mock_stream):
104
104
  """Test successful error correction"""
105
- mock_response = """```unified_diff
106
- --- a/app.py
107
- +++ b/app.py
108
- @@ -1,1 +1,1 @@
109
- -import streamlit
110
- -st.title('Test')
111
- +import streamlit
112
- +st.title('Fixed')
105
+ mock_response = """```search_replace
106
+ >>>>>>> SEARCH
107
+ st.title('Test')
108
+ =======
109
+ st.title('Fixed')
110
+ <<<<<<< REPLACE
113
111
  ```"""
114
112
  async def mock_async_gen():
115
113
  for item in [mock_response]:
@@ -117,8 +115,8 @@ class TestCorrectErrorInGeneration:
117
115
 
118
116
  mock_stream.return_value = mock_async_gen()
119
117
 
120
- result = await correct_error_in_generation("ImportError: No module named 'pandas'", "import streamlit\nst.title('Test')")
121
-
118
+ result = await correct_error_in_generation("ImportError: No module named 'pandas'", "import streamlit\nst.title('Test')\n")
119
+
122
120
  expected_code = "import streamlit\nst.title('Fixed')\n"
123
121
  assert result == expected_code
124
122
 
@@ -140,8 +138,8 @@ class TestStreamlitHandler:
140
138
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.generate_new_streamlit_code')
141
139
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.validate_app')
142
140
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.create_app_file')
143
- @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.clean_directory_check')
144
- async def test_streamlit_handler_success(self, mock_clean_directory, 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):
145
143
  """Test successful streamlit handler execution"""
146
144
  # Mock notebook parsing
147
145
  mock_notebook_data: List[dict] = [{"cells": [{"cell_type": "code", "source": ["import pandas"]}]}]
@@ -151,35 +149,29 @@ class TestStreamlitHandler:
151
149
  mock_generate_code.return_value = "import streamlit\nst.title('Test')"
152
150
 
153
151
  # Mock validation (no errors)
154
- mock_validator.return_value = (False, "")
155
-
156
- # Mock file creation
157
- mock_create_file.return_value = (True, "/path/to/app.py", "File created successfully")
158
-
159
- # Mock clean directory check (no-op)
160
- mock_clean_directory.return_value = None
152
+ mock_validator.return_value = []
161
153
 
162
154
  # Use a relative path that will work cross-platform
163
- notebook_path = "notebook.ipynb"
164
- result = await streamlit_handler(notebook_path)
155
+ notebook_path = AbsoluteNotebookPath("absolute/path/to/notebook.ipynb")
165
156
 
166
- assert result[0] is True
167
- assert "File created successfully" in result[2]
157
+ # Construct the expected app path using the same method as the production code
158
+ app_directory = get_absolute_notebook_dir_path(notebook_path)
159
+ expected_app_path = get_absolute_app_path(app_directory)
160
+ await streamlit_handler(notebook_path)
168
161
 
169
162
  # Verify calls
170
163
  mock_parse.assert_called_once_with(notebook_path)
171
164
  mock_generate_code.assert_called_once_with(mock_notebook_data)
172
165
  mock_validator.assert_called_once_with("import streamlit\nst.title('Test')", notebook_path)
173
- # get_app_directory converts relative paths to absolute, so expect the absolute path directory
174
- expected_app_dir = os.path.dirname(os.path.abspath(notebook_path))
175
- mock_create_file.assert_called_once_with(expected_app_dir, "import streamlit\nst.title('Test')")
166
+ mock_create_file.assert_called_once_with(expected_app_path, "import streamlit\nst.title('Test')")
176
167
 
177
168
  @pytest.mark.asyncio
178
169
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
179
170
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.generate_new_streamlit_code')
180
171
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.correct_error_in_generation')
181
172
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.validate_app')
182
- 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):
183
175
  """Test streamlit handler when max retries are exceeded"""
184
176
  # Mock notebook parsing
185
177
  mock_notebook_data: List[dict] = [{"cells": []}]
@@ -189,16 +181,15 @@ class TestStreamlitHandler:
189
181
  mock_generate_code.return_value = "import streamlit\nst.title('Test')"
190
182
  mock_correct_error.return_value = "import streamlit\nst.title('Fixed')"
191
183
 
192
- # Mock validation (always errors) - Return list of errors as expected by validate_app
193
- mock_validator.return_value = (True, ["Persistent error"])
184
+ # Mock validation (always errors) - validate_app returns List[str]
185
+ mock_validator.return_value = ["Persistent error"]
194
186
 
195
- result = await streamlit_handler("notebook.ipynb")
196
-
197
- # Verify the result indicates failure
198
- assert result[0] is False
199
- assert "Error generating streamlit code by agent" in result[2]
187
+ # Now it should raise an exception instead of returning a tuple
188
+ with pytest.raises(Exception):
189
+ await streamlit_handler(AbsoluteNotebookPath("notebook.ipynb"))
200
190
 
201
- # Verify that error correction was called 5 times (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
202
193
  assert mock_correct_error.call_count == 5
203
194
 
204
195
  @pytest.mark.asyncio
@@ -206,8 +197,7 @@ class TestStreamlitHandler:
206
197
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.generate_new_streamlit_code')
207
198
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.validate_app')
208
199
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.create_app_file')
209
- @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.clean_directory_check')
210
- async def test_streamlit_handler_file_creation_failure(self, mock_clean_directory, mock_create_file, mock_validator, mock_generate_code, mock_parse):
200
+ async def test_streamlit_handler_file_creation_failure(self, mock_create_file, mock_validator, mock_generate_code, mock_parse):
211
201
  """Test streamlit handler when file creation fails"""
212
202
  # Mock notebook parsing
213
203
  mock_notebook_data: List[dict] = [{"cells": []}]
@@ -216,38 +206,30 @@ class TestStreamlitHandler:
216
206
  # Mock code generation
217
207
  mock_generate_code.return_value = "import streamlit\nst.title('Test')"
218
208
 
219
- # Mock validation (no errors)
220
- mock_validator.return_value = (False, "")
221
-
222
- # Mock file creation failure
223
- mock_create_file.return_value = (False, None, "Permission denied")
209
+ # Mock validation (no errors) - validate_app returns List[str]
210
+ mock_validator.return_value = []
224
211
 
225
- # Mock clean directory check (no-op)
226
- mock_clean_directory.return_value = None
212
+ # Mock file creation failure - now it should raise an exception
213
+ mock_create_file.side_effect = Exception("Permission denied")
227
214
 
228
- result = await streamlit_handler("notebook.ipynb")
229
-
230
- assert result[0] is False
231
- assert "Permission denied" in result[2]
215
+ # Now it should raise an exception instead of returning a tuple
216
+ with pytest.raises(Exception):
217
+ await streamlit_handler(AbsoluteNotebookPath("notebook.ipynb"))
232
218
 
233
219
  @pytest.mark.asyncio
234
220
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
235
- @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.clean_directory_check')
236
- async def test_streamlit_handler_parse_notebook_exception(self, mock_clean_directory, mock_parse):
221
+ async def test_streamlit_handler_parse_notebook_exception(self, mock_parse):
237
222
  """Test streamlit handler when notebook parsing fails"""
238
- # Mock clean directory check (no-op)
239
- mock_clean_directory.return_value = None
240
223
 
241
224
  mock_parse.side_effect = FileNotFoundError("Notebook not found")
242
225
 
243
226
  with pytest.raises(FileNotFoundError, match="Notebook not found"):
244
- await streamlit_handler("notebook.ipynb")
227
+ await streamlit_handler(AbsoluteNotebookPath("notebook.ipynb"))
245
228
 
246
229
  @pytest.mark.asyncio
247
230
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
248
231
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.generate_new_streamlit_code')
249
- @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.clean_directory_check')
250
- async def test_streamlit_handler_generation_exception(self, mock_clean_directory, mock_generate_code, mock_parse):
232
+ async def test_streamlit_handler_generation_exception(self, mock_generate_code, mock_parse):
251
233
  """Test streamlit handler when code generation fails"""
252
234
  # Mock notebook parsing
253
235
  mock_notebook_data: List[dict] = [{"cells": []}]
@@ -256,11 +238,8 @@ class TestStreamlitHandler:
256
238
  # Mock code generation failure
257
239
  mock_generate_code.side_effect = Exception("Generation failed")
258
240
 
259
- # Mock clean directory check (no-op)
260
- mock_clean_directory.return_value = None
261
-
262
241
  with pytest.raises(Exception, match="Generation failed"):
263
- await streamlit_handler("notebook.ipynb")
242
+ await streamlit_handler(AbsoluteNotebookPath("notebook.ipynb"))
264
243
 
265
244
 
266
245