mito-ai 0.1.38__py3-none-any.whl → 0.1.39__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 (47) hide show
  1. mito_ai/__init__.py +8 -0
  2. mito_ai/_version.py +1 -1
  3. mito_ai/app_builder/handlers.py +16 -11
  4. mito_ai/completions/handlers.py +1 -1
  5. mito_ai/completions/prompt_builders/agent_system_message.py +18 -45
  6. mito_ai/completions/prompt_builders/chat_name_prompt.py +6 -6
  7. mito_ai/openai_client.py +1 -1
  8. mito_ai/streamlit_conversion/agent_utils.py +116 -0
  9. mito_ai/streamlit_conversion/prompts/prompt_constants.py +59 -0
  10. mito_ai/streamlit_conversion/prompts/prompt_utils.py +10 -0
  11. mito_ai/streamlit_conversion/prompts/streamlit_app_creation_prompt.py +45 -0
  12. mito_ai/streamlit_conversion/prompts/streamlit_error_correction_prompt.py +28 -0
  13. mito_ai/streamlit_conversion/prompts/streamlit_finish_todo_prompt.py +44 -0
  14. mito_ai/streamlit_conversion/streamlit_agent_handler.py +72 -42
  15. mito_ai/streamlit_conversion/streamlit_system_prompt.py +19 -17
  16. mito_ai/streamlit_conversion/streamlit_utils.py +43 -5
  17. mito_ai/streamlit_conversion/validate_streamlit_app.py +116 -0
  18. mito_ai/streamlit_preview/handlers.py +7 -4
  19. mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +153 -66
  20. mito_ai/tests/streamlit_conversion/test_validate_streamlit_app.py +119 -0
  21. mito_ai/tests/utils/test_anthropic_utils.py +2 -2
  22. mito_ai/utils/anthropic_utils.py +4 -4
  23. mito_ai/utils/open_ai_utils.py +0 -4
  24. {mito_ai-0.1.38.data → mito_ai-0.1.39.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +1 -1
  25. {mito_ai-0.1.38.data → mito_ai-0.1.39.data}/data/share/jupyter/labextensions/mito_ai/package.json +2 -2
  26. {mito_ai-0.1.38.data → mito_ai-0.1.39.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/package.json.orig +1 -1
  27. mito_ai-0.1.38.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.5d1d7c234e2dc7c9d97b.js → mito_ai-0.1.39.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.16b532b655cd2906e04a.js +262 -65
  28. mito_ai-0.1.39.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.16b532b655cd2906e04a.js.map +1 -0
  29. mito_ai-0.1.38.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.bcce4ea34631acf6dbbe.js → mito_ai-0.1.39.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.606207904e6aaa42b1bf.js +3 -3
  30. mito_ai-0.1.38.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.bcce4ea34631acf6dbbe.js.map → mito_ai-0.1.39.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.606207904e6aaa42b1bf.js.map +1 -1
  31. {mito_ai-0.1.38.dist-info → mito_ai-0.1.39.dist-info}/METADATA +4 -1
  32. {mito_ai-0.1.38.dist-info → mito_ai-0.1.39.dist-info}/RECORD +44 -38
  33. mito_ai/streamlit_conversion/validate_and_run_streamlit_code.py +0 -208
  34. mito_ai/tests/streamlit_conversion/test_validate_and_run_streamlit_code.py +0 -418
  35. mito_ai-0.1.38.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.5d1d7c234e2dc7c9d97b.js.map +0 -1
  36. {mito_ai-0.1.38.data → mito_ai-0.1.39.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
  37. {mito_ai-0.1.38.data → mito_ai-0.1.39.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/toolbar-buttons.json +0 -0
  38. {mito_ai-0.1.38.data → mito_ai-0.1.39.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
  39. {mito_ai-0.1.38.data → mito_ai-0.1.39.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js +0 -0
  40. {mito_ai-0.1.38.data → mito_ai-0.1.39.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js.map +0 -0
  41. {mito_ai-0.1.38.data → mito_ai-0.1.39.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.9795f79265ddb416864b.js +0 -0
  42. {mito_ai-0.1.38.data → mito_ai-0.1.39.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.9795f79265ddb416864b.js.map +0 -0
  43. {mito_ai-0.1.38.data → mito_ai-0.1.39.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js +0 -0
  44. {mito_ai-0.1.38.data → mito_ai-0.1.39.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js.map +0 -0
  45. {mito_ai-0.1.38.dist-info → mito_ai-0.1.39.dist-info}/WHEEL +0 -0
  46. {mito_ai-0.1.38.dist-info → mito_ai-0.1.39.dist-info}/entry_points.txt +0 -0
  47. {mito_ai-0.1.38.dist-info → mito_ai-0.1.39.dist-info}/licenses/LICENSE +0 -0
@@ -2,12 +2,12 @@
2
2
  # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
3
 
4
4
  import pytest
5
- from unittest.mock import patch, AsyncMock
5
+ from unittest.mock import patch, AsyncMock, MagicMock
6
6
  from mito_ai.streamlit_conversion.streamlit_agent_handler import (
7
7
  StreamlitCodeGeneration,
8
8
  streamlit_handler
9
9
  )
10
- from typing import cast
10
+ from mito_ai.streamlit_conversion.streamlit_utils import clean_directory_check
11
11
 
12
12
  # Add this line to enable async support
13
13
  pytest_plugins = ('pytest_asyncio',)
@@ -16,20 +16,24 @@ pytest_plugins = ('pytest_asyncio',)
16
16
  class TestStreamlitCodeGeneration:
17
17
  """Test cases for StreamlitCodeGeneration class"""
18
18
 
19
- def test_init(self):
19
+ @pytest.mark.asyncio
20
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.stream_anthropic_completion_from_mito_server')
21
+ async def test_init(self, mock_stream):
20
22
  """Test StreamlitCodeGeneration initialization"""
23
+ # Mock the async generator
24
+ async def mock_async_gen():
25
+ yield "Here's your code:\n```python\nimport streamlit\nst.title('Test')\n```"
26
+
27
+ mock_stream.return_value = mock_async_gen()
28
+
21
29
  notebook_data: dict = {"cells": [{"cell_type": "code", "source": ["import pandas"]}]}
22
- generator = StreamlitCodeGeneration(notebook_data)
23
-
24
- assert len(generator.messages) == 1
25
- assert generator.messages[0]["role"] == "user"
26
- # Access content properly as a list and cast to expected type
27
- content_list = cast(list, generator.messages[0]["content"])
28
- assert isinstance(content_list, list)
29
- assert len(content_list) > 0
30
- content_item = cast(dict, content_list[0])
31
- assert content_item["type"] == "text"
32
- assert "jupyter notebook content" in content_item["text"]
30
+ streamlit_code_handler = StreamlitCodeGeneration()
31
+
32
+ streamlit_code = await streamlit_code_handler.generate_streamlit_code(notebook_data)
33
+
34
+ assert streamlit_code is not None
35
+ assert len(streamlit_code) > 0
36
+ assert "import streamlit" in streamlit_code
33
37
 
34
38
  @pytest.mark.asyncio
35
39
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.stream_anthropic_completion_from_mito_server')
@@ -48,9 +52,9 @@ class TestStreamlitCodeGeneration:
48
52
  mock_stream.return_value = mock_async_gen()
49
53
 
50
54
  notebook_data: dict = {"cells": []}
51
- generator = StreamlitCodeGeneration(notebook_data)
55
+ streamlit_code_handler = StreamlitCodeGeneration()
52
56
 
53
- result = await generator.get_response_from_agent(generator.messages)
57
+ result = await streamlit_code_handler.generate_streamlit_code(notebook_data)
54
58
 
55
59
  assert result == expected_result
56
60
  mock_stream.assert_called_once()
@@ -63,28 +67,10 @@ class TestStreamlitCodeGeneration:
63
67
  mock_stream.side_effect = Exception("API Error")
64
68
 
65
69
  notebook_data: dict = {"cells": []}
66
- generator = StreamlitCodeGeneration(notebook_data)
70
+ streamlit_code_handler = StreamlitCodeGeneration()
67
71
 
68
72
  with pytest.raises(Exception, match="API Error"):
69
- await generator.get_response_from_agent(generator.messages)
70
-
71
- def test_add_agent_response_to_context(self):
72
- """Test adding agent response to message history"""
73
- notebook_data: dict = {"cells": []}
74
- generator = StreamlitCodeGeneration(notebook_data)
75
-
76
- initial_count = len(generator.messages)
77
- generator.add_agent_response_to_context("Test response")
78
-
79
- assert len(generator.messages) == initial_count + 1
80
- assert generator.messages[-1]["role"] == "assistant"
81
- # Access content properly as a list and cast to expected type
82
- content_list = cast(list, generator.messages[-1]["content"])
83
- assert isinstance(content_list, list)
84
- assert len(content_list) > 0
85
- content_item = cast(dict, content_list[0])
86
- assert content_item["type"] == "text"
87
- assert content_item["text"] == "Test response"
73
+ await streamlit_code_handler.generate_streamlit_code(notebook_data)
88
74
 
89
75
  @pytest.mark.asyncio
90
76
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.stream_anthropic_completion_from_mito_server')
@@ -99,22 +85,26 @@ class TestStreamlitCodeGeneration:
99
85
  mock_stream.return_value = mock_async_gen()
100
86
 
101
87
  notebook_data: dict = {"cells": []}
102
- generator = StreamlitCodeGeneration(notebook_data)
88
+ streamlit_code_handler = StreamlitCodeGeneration()
103
89
 
104
- result = await generator.generate_streamlit_code()
90
+ result = await streamlit_code_handler.generate_streamlit_code(notebook_data)
105
91
 
106
92
  expected_code = "import streamlit\nst.title('Hello')\n"
107
93
  assert result == expected_code
108
-
109
- # Check that response was added to context
110
- assert len(generator.messages) == 2
111
- assert generator.messages[-1]["role"] == "assistant"
112
94
 
113
95
  @pytest.mark.asyncio
114
96
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.stream_anthropic_completion_from_mito_server')
115
97
  async def test_correct_error_in_generation_success(self, mock_stream):
116
98
  """Test successful error correction"""
117
- mock_response = "Here's the corrected code:\n```python\nimport streamlit\nst.title('Fixed')\n```"
99
+ mock_response = """```unified_diff
100
+ --- a/app.py
101
+ +++ b/app.py
102
+ @@ -1,1 +1,1 @@
103
+ -import streamlit
104
+ -st.title('Test')
105
+ +import streamlit
106
+ +st.title('Fixed')
107
+ ```"""
118
108
  async def mock_async_gen():
119
109
  for item in [mock_response]:
120
110
  yield item
@@ -122,29 +112,23 @@ class TestStreamlitCodeGeneration:
122
112
  mock_stream.return_value = mock_async_gen()
123
113
 
124
114
  notebook_data: dict = {"cells": []}
125
- generator = StreamlitCodeGeneration(notebook_data)
115
+ streamlit_code_handler = StreamlitCodeGeneration()
126
116
 
127
- result = await generator.correct_error_in_generation("ImportError: No module named 'pandas'")
117
+ result = await streamlit_code_handler.correct_error_in_generation("ImportError: No module named 'pandas'", "import streamlit\nst.title('Test')")
128
118
 
129
119
  expected_code = "import streamlit\nst.title('Fixed')\n"
130
120
  assert result == expected_code
131
121
 
132
- # Check that error message and response were added to context
133
- assert len(generator.messages) == 3
134
- assert generator.messages[-2]["role"] == "user"
135
- assert generator.messages[-1]["role"] == "assistant"
136
-
137
122
  @pytest.mark.asyncio
138
123
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.stream_anthropic_completion_from_mito_server')
139
124
  async def test_correct_error_in_generation_exception(self, mock_stream):
140
125
  """Test exception handling in error correction"""
141
126
  mock_stream.side_effect = Exception("API Error")
142
127
 
143
- notebook_data: dict = {"cells": []}
144
- generator = StreamlitCodeGeneration(notebook_data)
128
+ streamlit_code_handler = StreamlitCodeGeneration()
145
129
 
146
130
  with pytest.raises(Exception, match="API Error"):
147
- await generator.correct_error_in_generation("Some error")
131
+ await streamlit_code_handler.correct_error_in_generation("Some error", "import streamlit\nst.title('Test')")
148
132
 
149
133
 
150
134
  class TestStreamlitHandler:
@@ -153,9 +137,10 @@ class TestStreamlitHandler:
153
137
  @pytest.mark.asyncio
154
138
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
155
139
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.StreamlitCodeGeneration')
156
- @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.streamlit_code_validator')
140
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.validate_app')
157
141
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.create_app_file')
158
- async def test_streamlit_handler_success(self, mock_create_file, mock_validator, mock_generator_class, mock_parse):
142
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.clean_directory_check')
143
+ async def test_streamlit_handler_success(self, mock_clean_directory, mock_create_file, mock_validator, mock_generator_class, mock_parse):
159
144
  """Test successful streamlit handler execution"""
160
145
  # Mock notebook parsing
161
146
  mock_notebook_data: dict = {"cells": [{"cell_type": "code", "source": ["import pandas"]}]}
@@ -170,7 +155,10 @@ class TestStreamlitHandler:
170
155
  mock_validator.return_value = (False, "")
171
156
 
172
157
  # Mock file creation
173
- mock_create_file.return_value = (True, "/path/to/app", "File created successfully")
158
+ mock_create_file.return_value = (True, "/path/to/app.py", "File created successfully")
159
+
160
+ # Mock clean directory check (no-op)
161
+ mock_clean_directory.return_value = None
174
162
 
175
163
  result = await streamlit_handler("/path/to/notebook.ipynb")
176
164
 
@@ -179,15 +167,15 @@ class TestStreamlitHandler:
179
167
 
180
168
  # Verify calls
181
169
  mock_parse.assert_called_once_with("/path/to/notebook.ipynb")
182
- mock_generator_class.assert_called_once_with(mock_notebook_data)
170
+ mock_generator_class.assert_called_once_with()
183
171
  mock_generator.generate_streamlit_code.assert_called_once()
184
- mock_validator.assert_called_once_with("import streamlit\nst.title('Test')")
172
+ mock_validator.assert_called_once_with("import streamlit\nst.title('Test')", "/path/to/notebook.ipynb")
185
173
  mock_create_file.assert_called_once_with("/path/to", "import streamlit\nst.title('Test')")
186
174
 
187
175
  @pytest.mark.asyncio
188
176
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
189
177
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.StreamlitCodeGeneration')
190
- @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.streamlit_code_validator')
178
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.validate_app')
191
179
  async def test_streamlit_handler_max_retries_exceeded(self, mock_validator, mock_generator_class, mock_parse):
192
180
  """Test streamlit handler when max retries are exceeded"""
193
181
  # Mock notebook parsing
@@ -200,10 +188,10 @@ class TestStreamlitHandler:
200
188
  mock_generator.correct_error_in_generation.return_value = "import streamlit\nst.title('Fixed')"
201
189
  mock_generator_class.return_value = mock_generator
202
190
 
203
- # Mock validation (always errors) - FIX: Return only 2 values
204
- mock_validator.return_value = (True, "Persistent error")
191
+ # Mock validation (always errors) - Return list of errors as expected by validate_app
192
+ mock_validator.return_value = (True, ["Persistent error"])
205
193
 
206
- result = await streamlit_handler("/path/to/notebook.ipynb")
194
+ result = await streamlit_handler("/notebook.ipynb")
207
195
 
208
196
  # Verify the result indicates failure
209
197
  assert result[0] is False
@@ -215,9 +203,10 @@ class TestStreamlitHandler:
215
203
  @pytest.mark.asyncio
216
204
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
217
205
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.StreamlitCodeGeneration')
218
- @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.streamlit_code_validator')
206
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.validate_app')
219
207
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.create_app_file')
220
- async def test_streamlit_handler_file_creation_failure(self, mock_create_file, mock_validator, mock_generator_class, mock_parse):
208
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.clean_directory_check')
209
+ async def test_streamlit_handler_file_creation_failure(self, mock_clean_directory, mock_create_file, mock_validator, mock_generator_class, mock_parse):
221
210
  """Test streamlit handler when file creation fails"""
222
211
  # Mock notebook parsing
223
212
  mock_notebook_data: dict = {"cells": []}
@@ -234,6 +223,9 @@ class TestStreamlitHandler:
234
223
  # Mock file creation failure
235
224
  mock_create_file.return_value = (False, None, "Permission denied")
236
225
 
226
+ # Mock clean directory check (no-op)
227
+ mock_clean_directory.return_value = None
228
+
237
229
  result = await streamlit_handler("/path/to/notebook.ipynb")
238
230
 
239
231
  assert result[0] is False
@@ -241,8 +233,12 @@ class TestStreamlitHandler:
241
233
 
242
234
  @pytest.mark.asyncio
243
235
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
244
- async def test_streamlit_handler_parse_notebook_exception(self, mock_parse):
236
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.clean_directory_check')
237
+ async def test_streamlit_handler_parse_notebook_exception(self, mock_clean_directory, mock_parse):
245
238
  """Test streamlit handler when notebook parsing fails"""
239
+ # Mock clean directory check (no-op)
240
+ mock_clean_directory.return_value = None
241
+
246
242
  mock_parse.side_effect = FileNotFoundError("Notebook not found")
247
243
 
248
244
  with pytest.raises(FileNotFoundError, match="Notebook not found"):
@@ -251,7 +247,8 @@ class TestStreamlitHandler:
251
247
  @pytest.mark.asyncio
252
248
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
253
249
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.StreamlitCodeGeneration')
254
- async def test_streamlit_handler_generation_exception(self, mock_generator_class, mock_parse):
250
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.clean_directory_check')
251
+ async def test_streamlit_handler_generation_exception(self, mock_clean_directory, mock_generator_class, mock_parse):
255
252
  """Test streamlit handler when code generation fails"""
256
253
  # Mock notebook parsing
257
254
  mock_notebook_data: dict = {"cells": []}
@@ -262,5 +259,95 @@ class TestStreamlitHandler:
262
259
  mock_generator.generate_streamlit_code.side_effect = Exception("Generation failed")
263
260
  mock_generator_class.return_value = mock_generator
264
261
 
262
+ # Mock clean directory check (no-op)
263
+ mock_clean_directory.return_value = None
264
+
265
265
  with pytest.raises(Exception, match="Generation failed"):
266
266
  await streamlit_handler("/path/to/notebook.ipynb")
267
+
268
+ @pytest.mark.asyncio
269
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
270
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.StreamlitCodeGeneration')
271
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.validate_app')
272
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.create_app_file')
273
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.clean_directory_check')
274
+ async def test_streamlit_handler_too_many_files_in_directory(self, mock_clean_directory, mock_create_file, mock_validator, mock_generator_class, mock_parse):
275
+ """Test streamlit handler when there are too many files in the directory"""
276
+ # Mock clean directory check to raise ValueError (simulating >10 files)
277
+ mock_clean_directory.side_effect = ValueError("Too many files in directory: 10 allowed but 15 present. Create a new directory and retry")
278
+
279
+ # The function should raise the ValueError before any other processing
280
+ with pytest.raises(ValueError, match="Too many files in directory: 10 allowed but 15 present. Create a new directory and retry"):
281
+ await streamlit_handler("/path/to/notebook.ipynb")
282
+
283
+ # Verify that clean_directory_check was called
284
+ mock_clean_directory.assert_called_once_with("/path/to/notebook.ipynb")
285
+
286
+ # Verify that no other functions were called since the error occurred early
287
+ mock_parse.assert_not_called()
288
+ mock_generator_class.assert_not_called()
289
+ mock_validator.assert_not_called()
290
+ mock_create_file.assert_not_called()
291
+
292
+
293
+ class TestCleanDirectoryCheck:
294
+ """Test cases for clean_directory_check function"""
295
+
296
+ @patch('mito_ai.streamlit_conversion.streamlit_utils.Path')
297
+ def test_clean_directory_check_under_limit(self, mock_path):
298
+ """Test clean_directory_check when directory has 10 or fewer files"""
299
+ # Mock the Path class and its methods
300
+ mock_path_instance = mock_path.return_value
301
+ mock_path_instance.resolve.return_value = mock_path_instance
302
+ mock_path_instance.parent = mock_path_instance
303
+
304
+ # Mock directory existence check
305
+ mock_path_instance.exists.return_value = True
306
+
307
+ # Mock directory contents with 8 files
308
+ mock_files = []
309
+ for i in range(8):
310
+ mock_file = MagicMock()
311
+ mock_file.is_file.return_value = True
312
+ mock_files.append(mock_file)
313
+
314
+ mock_path_instance.iterdir.return_value = mock_files
315
+
316
+ # Should not raise any exception
317
+ clean_directory_check('/path/to/notebook.ipynb')
318
+
319
+ # Verify calls
320
+ mock_path.assert_called_once_with('/path/to/notebook.ipynb')
321
+ mock_path_instance.resolve.assert_called_once()
322
+ mock_path_instance.exists.assert_called_once()
323
+ mock_path_instance.iterdir.assert_called_once()
324
+
325
+ @patch('mito_ai.streamlit_conversion.streamlit_utils.Path')
326
+ def test_clean_directory_check_over_limit(self, mock_path):
327
+ """Test clean_directory_check when directory has more than 10 files"""
328
+ # Mock the Path class and its methods
329
+ mock_path_instance = mock_path.return_value
330
+ mock_path_instance.resolve.return_value = mock_path_instance
331
+ mock_path_instance.parent = mock_path_instance
332
+
333
+ # Mock directory existence check
334
+ mock_path_instance.exists.return_value = True
335
+
336
+ # Mock directory contents with 15 files
337
+ mock_files = []
338
+ for i in range(15):
339
+ mock_file = MagicMock()
340
+ mock_file.is_file.return_value = True
341
+ mock_files.append(mock_file)
342
+
343
+ mock_path_instance.iterdir.return_value = mock_files
344
+
345
+ # Should raise ValueError
346
+ with pytest.raises(ValueError, match="Too many files in directory: 10 allowed but 15 present. Create a new directory and retry"):
347
+ clean_directory_check('/path/to/notebook.ipynb')
348
+
349
+ # Verify calls
350
+ mock_path.assert_called_once_with('/path/to/notebook.ipynb')
351
+ mock_path_instance.resolve.assert_called_once()
352
+ mock_path_instance.exists.assert_called_once()
353
+ mock_path_instance.iterdir.assert_called_once()
@@ -0,0 +1,119 @@
1
+ # Copyright (c) Saga Inc.
2
+ # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
+
4
+ import os
5
+ import tempfile
6
+ from unittest.mock import patch, MagicMock
7
+ from mito_ai.streamlit_conversion.validate_streamlit_app import (
8
+ StreamlitValidator,
9
+ validate_app
10
+ )
11
+ import pytest
12
+
13
+
14
+ class TestStreamlitValidator:
15
+ """Test cases for StreamlitValidator class"""
16
+
17
+ @pytest.mark.parametrize("code,expected_error,test_description", [
18
+ # Valid Python code should return no error
19
+ (
20
+ "import streamlit\nst.title('Hello World')",
21
+ None,
22
+ "valid Python code"
23
+ ),
24
+ # Invalid Python syntax should be caught
25
+ (
26
+ "import streamlit\nst.title('Hello World'",
27
+ "SyntaxError",
28
+ "invalid Python code"
29
+ ),
30
+ # Empty streamlit app is valid
31
+ (
32
+ "",
33
+ None,
34
+ "empty code"
35
+ ),
36
+ ])
37
+ def test_validate_syntax(self, code, expected_error, test_description):
38
+ """Test syntax validation with various code inputs"""
39
+ validator = StreamlitValidator()
40
+
41
+ error = validator.get_syntax_error(code)
42
+
43
+ if expected_error is None:
44
+ assert error is None, f"Expected no error for {test_description}"
45
+ else:
46
+ assert error is not None, f"Expected error for {test_description}"
47
+ assert expected_error in error, f"Expected '{expected_error}' in error for {test_description}"
48
+
49
+ @pytest.mark.parametrize("app_code,expected_error", [
50
+ ("x = 5", None),
51
+ ("1/0", "division by zero"),
52
+ ("", None)
53
+ ])
54
+ def test_get_runtime_errors(self, app_code, expected_error):
55
+ """Test getting runtime errors"""
56
+ validator = StreamlitValidator()
57
+
58
+ errors = validator.get_runtime_errors(app_code, '/app.py')
59
+
60
+ if expected_error is None:
61
+ assert errors is None
62
+ else:
63
+ errors_str = str(errors)
64
+ assert expected_error in errors_str
65
+
66
+ def test_get_runtime_errors_with_relative_path(self):
67
+ """Test getting runtime errors"""
68
+
69
+ app_code ="""
70
+ import streamlit as st
71
+ import pandas as pd
72
+
73
+ df=pd.read_csv('data.csv')
74
+ """
75
+ # Create a temporary csv file in the directory temp/data.csv
76
+ with tempfile.TemporaryDirectory() as temp_dir:
77
+ directory = 'app_directory'
78
+ csv_path = os.path.join(temp_dir, directory, "data.csv")
79
+
80
+ os.makedirs(os.path.join(temp_dir, directory), exist_ok=True)
81
+ app_path = os.path.join(temp_dir, directory, "app.py")
82
+
83
+ # Create the file if it doesn't exist
84
+ with open(csv_path, "w") as f:
85
+ f.write("name,age\nJohn,25\nJane,30")
86
+
87
+ validator = StreamlitValidator()
88
+ errors = validator.get_runtime_errors(app_code, app_path)
89
+ assert errors is None
90
+
91
+
92
+ @patch('subprocess.Popen')
93
+ def test_cleanup_with_process(self, mock_popen):
94
+ """Test cleanup with running process"""
95
+ validator = StreamlitValidator()
96
+ validator.temp_dir = "/tmp/test_dir"
97
+
98
+ # Mock directory exists
99
+ with patch('os.path.exists', return_value=True):
100
+ with patch('shutil.rmtree') as mock_rmtree:
101
+ validator.cleanup()
102
+
103
+ mock_rmtree.assert_called_once()
104
+
105
+
106
+ @pytest.mark.parametrize("app_code,expected_has_validation_error,expected_error_message", [
107
+ ("x=5", False, ""),
108
+ ("1/0", True, "division by zero"),
109
+ ("print('Hello World'", True, "SyntaxError"),
110
+ ("", False, ""),
111
+ ])
112
+ def test_streamlit_code_validator(self, app_code, expected_has_validation_error, expected_error_message):
113
+
114
+ has_validation_error, errors = validate_app(app_code, '/app.py')
115
+
116
+ assert has_validation_error == expected_has_validation_error
117
+ assert expected_error_message in str(errors)
118
+
119
+
@@ -5,7 +5,7 @@ import pytest
5
5
  import anthropic
6
6
  from typing import List, Dict, Any, Tuple, Union, cast
7
7
  from anthropic.types import MessageParam, ToolUnionParam, ToolParam
8
- from mito_ai.utils.anthropic_utils import _prepare_anthropic_request_data_and_headers
8
+ from mito_ai.utils.anthropic_utils import ANTHROPIC_TIMEOUT, _prepare_anthropic_request_data_and_headers
9
9
  from mito_ai.completions.models import MessageType
10
10
  from mito_ai.utils.schema import UJ_STATIC_USER_ID, UJ_USER_EMAIL
11
11
  from mito_ai.utils.db import get_user_field
@@ -52,7 +52,7 @@ def test_basic_request_preparation():
52
52
  )
53
53
 
54
54
  assert headers == {"Content-Type": "application/json"}
55
- assert data["timeout"] == 30
55
+ assert data["timeout"] == ANTHROPIC_TIMEOUT
56
56
  assert data["max_retries"] == 1
57
57
  assert data["email"] == "test@example.com"
58
58
  assert data["user_id"] == "test_user_id"
@@ -23,7 +23,7 @@ from mito_ai.constants import MITO_ANTHROPIC_URL
23
23
  __user_email: Optional[str] = None
24
24
  __user_id: Optional[str] = None
25
25
 
26
- timeout = 30
26
+ ANTHROPIC_TIMEOUT = 60
27
27
  max_retries = 1
28
28
 
29
29
  FAST_ANTHROPIC_MODEL = "claude-3-5-haiku-latest"
@@ -63,7 +63,7 @@ def _prepare_anthropic_request_data_and_headers(
63
63
  inner_data["stream"] = stream
64
64
  # Compose the outer data dict
65
65
  data = {
66
- "timeout": timeout,
66
+ "timeout": ANTHROPIC_TIMEOUT,
67
67
  "max_retries": max_retries,
68
68
  "email": __user_email,
69
69
  "user_id": __user_id,
@@ -90,7 +90,7 @@ async def get_anthropic_completion_from_mito_server(
90
90
  MITO_ANTHROPIC_URL,
91
91
  headers,
92
92
  data,
93
- timeout,
93
+ ANTHROPIC_TIMEOUT,
94
94
  max_retries,
95
95
  message_type,
96
96
  provider_name="Claude"
@@ -118,7 +118,7 @@ async def stream_anthropic_completion_from_mito_server(
118
118
  url=MITO_ANTHROPIC_URL,
119
119
  headers=headers,
120
120
  data=data,
121
- timeout=timeout,
121
+ timeout=ANTHROPIC_TIMEOUT,
122
122
  max_retries=max_retries,
123
123
  message_type=message_type,
124
124
  reply_fn=actual_reply_fn,
@@ -196,9 +196,5 @@ def get_open_ai_completion_function_params(
196
196
  "strict": True
197
197
  }
198
198
  }
199
-
200
- # o3-mini will error if we try setting the temperature
201
- if not model.startswith("o3"):
202
- completion_function_params["temperature"] = 0.0
203
199
 
204
200
  return completion_function_params
@@ -710,7 +710,7 @@
710
710
  "semver": {},
711
711
  "vscode-diff": {},
712
712
  "mito_ai": {
713
- "version": "0.1.38",
713
+ "version": "0.1.39",
714
714
  "singleton": true,
715
715
  "import": "/home/runner/work/mito/mito/mito-ai/lib/index.js"
716
716
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mito_ai",
3
- "version": "0.1.38",
3
+ "version": "0.1.39",
4
4
  "description": "AI chat for JupyterLab",
5
5
  "keywords": [
6
6
  "jupyter",
@@ -138,7 +138,7 @@
138
138
  "outputDir": "mito_ai/labextension",
139
139
  "schemaDir": "schema",
140
140
  "_build": {
141
- "load": "static/remoteEntry.bcce4ea34631acf6dbbe.js",
141
+ "load": "static/remoteEntry.606207904e6aaa42b1bf.js",
142
142
  "extension": "./extension",
143
143
  "style": "./style"
144
144
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mito_ai",
3
- "version": "0.1.38",
3
+ "version": "0.1.39",
4
4
  "description": "AI chat for JupyterLab",
5
5
  "keywords": [
6
6
  "jupyter",