mito-ai 0.1.36__py3-none-any.whl → 0.1.37__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.
- mito_ai/__init__.py +6 -4
- mito_ai/_version.py +1 -1
- mito_ai/anthropic_client.py +3 -10
- mito_ai/app_builder/handlers.py +89 -11
- mito_ai/app_builder/models.py +3 -0
- mito_ai/auth/README.md +18 -0
- mito_ai/auth/__init__.py +6 -0
- mito_ai/auth/handlers.py +96 -0
- mito_ai/auth/urls.py +13 -0
- mito_ai/completions/completion_handlers/chat_completion_handler.py +2 -2
- mito_ai/completions/models.py +7 -6
- mito_ai/completions/prompt_builders/agent_execution_prompt.py +8 -3
- mito_ai/completions/prompt_builders/agent_system_message.py +21 -7
- mito_ai/completions/prompt_builders/chat_prompt.py +18 -11
- mito_ai/completions/prompt_builders/utils.py +53 -10
- mito_ai/constants.py +11 -1
- mito_ai/streamlit_conversion/streamlit_agent_handler.py +112 -0
- mito_ai/streamlit_conversion/streamlit_system_prompt.py +42 -0
- mito_ai/streamlit_conversion/streamlit_utils.py +96 -0
- mito_ai/streamlit_conversion/validate_and_run_streamlit_code.py +207 -0
- mito_ai/tests/providers/test_stream_mito_server_utils.py +140 -0
- mito_ai/tests/streamlit_conversion/__init__.py +3 -0
- mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +265 -0
- mito_ai/tests/streamlit_conversion/test_streamlit_utils.py +197 -0
- mito_ai/tests/streamlit_conversion/test_validate_and_run_streamlit_code.py +418 -0
- mito_ai/tests/test_constants.py +18 -3
- mito_ai/utils/anthropic_utils.py +18 -70
- mito_ai/utils/gemini_utils.py +22 -73
- mito_ai/utils/mito_server_utils.py +147 -4
- mito_ai/utils/open_ai_utils.py +18 -107
- {mito_ai-0.1.36.data → mito_ai-0.1.37.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +100 -100
- {mito_ai-0.1.36.data → mito_ai-0.1.37.data}/data/share/jupyter/labextensions/mito_ai/package.json +2 -2
- {mito_ai-0.1.36.data → mito_ai-0.1.37.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/package.json.orig +1 -1
- mito_ai-0.1.36.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.a20772bc113422d0f505.js → mito_ai-0.1.37.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.831f63b48760c7119b9b.js +1165 -539
- mito_ai-0.1.37.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.831f63b48760c7119b9b.js.map +1 -0
- mito_ai-0.1.36.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.5c9333902dce30642119.js → mito_ai-0.1.37.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.93ecc9bc0edba61535cc.js +18 -14
- mito_ai-0.1.37.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.93ecc9bc0edba61535cc.js.map +1 -0
- mito_ai-0.1.36.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.76efcc5c3be4056457ee.js → mito_ai-0.1.37.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js +6 -2
- mito_ai-0.1.37.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js.map +1 -0
- {mito_ai-0.1.36.dist-info → mito_ai-0.1.37.dist-info}/METADATA +1 -1
- {mito_ai-0.1.36.dist-info → mito_ai-0.1.37.dist-info}/RECORD +51 -38
- mito_ai-0.1.36.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.a20772bc113422d0f505.js.map +0 -1
- mito_ai-0.1.36.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.5c9333902dce30642119.js.map +0 -1
- mito_ai-0.1.36.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.76efcc5c3be4056457ee.js.map +0 -1
- {mito_ai-0.1.36.data → mito_ai-0.1.37.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
- {mito_ai-0.1.36.data → mito_ai-0.1.37.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/toolbar-buttons.json +0 -0
- {mito_ai-0.1.36.data → mito_ai-0.1.37.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
- {mito_ai-0.1.36.data → mito_ai-0.1.37.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.9795f79265ddb416864b.js +0 -0
- {mito_ai-0.1.36.data → mito_ai-0.1.37.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.9795f79265ddb416864b.js.map +0 -0
- {mito_ai-0.1.36.data → mito_ai-0.1.37.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js +0 -0
- {mito_ai-0.1.36.data → mito_ai-0.1.37.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js.map +0 -0
- {mito_ai-0.1.36.dist-info → mito_ai-0.1.37.dist-info}/WHEEL +0 -0
- {mito_ai-0.1.36.dist-info → mito_ai-0.1.37.dist-info}/entry_points.txt +0 -0
- {mito_ai-0.1.36.dist-info → mito_ai-0.1.37.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
# Copyright (c) Saga Inc.
|
|
2
|
+
# Distributed under the terms of the GNU Affero General Public License v3.0 License.
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
import tempfile
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
import time
|
|
9
|
+
import ast
|
|
10
|
+
import importlib.util
|
|
11
|
+
from unittest.mock import patch, MagicMock, mock_open
|
|
12
|
+
from mito_ai.streamlit_conversion.validate_and_run_streamlit_code import (
|
|
13
|
+
StreamlitValidator,
|
|
14
|
+
streamlit_code_validator
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TestStreamlitValidator:
|
|
19
|
+
"""Test cases for StreamlitValidator class"""
|
|
20
|
+
|
|
21
|
+
def test_validate_syntax_valid_code(self):
|
|
22
|
+
"""Test syntax validation with valid Python code"""
|
|
23
|
+
validator = StreamlitValidator()
|
|
24
|
+
code = "import streamlit\nst.title('Hello World')"
|
|
25
|
+
|
|
26
|
+
is_valid, message = validator.validate_syntax(code)
|
|
27
|
+
|
|
28
|
+
assert is_valid is True
|
|
29
|
+
assert "Syntax is valid" in message
|
|
30
|
+
|
|
31
|
+
def test_validate_syntax_invalid_code(self):
|
|
32
|
+
"""Test syntax validation with invalid Python code"""
|
|
33
|
+
validator = StreamlitValidator()
|
|
34
|
+
code = "import streamlit\nst.title('Hello World' # Missing closing parenthesis"
|
|
35
|
+
|
|
36
|
+
is_valid, message = validator.validate_syntax(code)
|
|
37
|
+
|
|
38
|
+
assert is_valid is False
|
|
39
|
+
assert "Syntax error" in message
|
|
40
|
+
|
|
41
|
+
def test_validate_syntax_empty_code(self):
|
|
42
|
+
"""Test syntax validation with empty code"""
|
|
43
|
+
validator = StreamlitValidator()
|
|
44
|
+
code = ""
|
|
45
|
+
|
|
46
|
+
is_valid, message = validator.validate_syntax(code)
|
|
47
|
+
|
|
48
|
+
assert is_valid is True
|
|
49
|
+
assert "Syntax is valid" in message
|
|
50
|
+
|
|
51
|
+
def test_create_temp_app(self):
|
|
52
|
+
"""Test creating temporary app file"""
|
|
53
|
+
validator = StreamlitValidator()
|
|
54
|
+
code = "import streamlit\nst.title('Test')"
|
|
55
|
+
|
|
56
|
+
app_path = validator.create_temp_app(code)
|
|
57
|
+
|
|
58
|
+
assert validator.temp_dir is not None
|
|
59
|
+
assert os.path.exists(validator.temp_dir)
|
|
60
|
+
assert app_path.endswith("app.py")
|
|
61
|
+
assert os.path.exists(app_path)
|
|
62
|
+
|
|
63
|
+
# Check file content
|
|
64
|
+
with open(app_path, 'r') as f:
|
|
65
|
+
content = f.read()
|
|
66
|
+
assert content == code
|
|
67
|
+
|
|
68
|
+
# Cleanup
|
|
69
|
+
validator.cleanup()
|
|
70
|
+
|
|
71
|
+
def test_create_temp_app_empty_code(self):
|
|
72
|
+
"""Test creating temporary app file with empty code"""
|
|
73
|
+
validator = StreamlitValidator()
|
|
74
|
+
code = ""
|
|
75
|
+
|
|
76
|
+
app_path = validator.create_temp_app(code)
|
|
77
|
+
|
|
78
|
+
assert os.path.exists(app_path)
|
|
79
|
+
|
|
80
|
+
with open(app_path, 'r') as f:
|
|
81
|
+
content = f.read()
|
|
82
|
+
assert content == ""
|
|
83
|
+
|
|
84
|
+
# Cleanup
|
|
85
|
+
validator.cleanup()
|
|
86
|
+
|
|
87
|
+
@patch('subprocess.Popen')
|
|
88
|
+
def test_start_streamlit_app_success(self, mock_popen):
|
|
89
|
+
"""Test successful Streamlit app startup"""
|
|
90
|
+
validator = StreamlitValidator(port=8502)
|
|
91
|
+
app_path = "/tmp/test/app.py"
|
|
92
|
+
|
|
93
|
+
# Mock successful subprocess
|
|
94
|
+
mock_process = MagicMock()
|
|
95
|
+
mock_popen.return_value = mock_process
|
|
96
|
+
|
|
97
|
+
success, message = validator.start_streamlit_app(app_path)
|
|
98
|
+
|
|
99
|
+
assert success is True
|
|
100
|
+
assert "Streamlit app started" in message
|
|
101
|
+
assert validator.process == mock_process
|
|
102
|
+
|
|
103
|
+
# Verify subprocess was called with correct arguments
|
|
104
|
+
mock_popen.assert_called_once()
|
|
105
|
+
call_args = mock_popen.call_args[0][0]
|
|
106
|
+
assert "streamlit" in call_args
|
|
107
|
+
assert "run" in call_args
|
|
108
|
+
assert app_path in call_args
|
|
109
|
+
assert "8502" in call_args
|
|
110
|
+
|
|
111
|
+
@patch('subprocess.Popen')
|
|
112
|
+
def test_start_streamlit_app_failure(self, mock_popen):
|
|
113
|
+
"""Test Streamlit app startup failure"""
|
|
114
|
+
validator = StreamlitValidator()
|
|
115
|
+
app_path = "/tmp/test/app.py"
|
|
116
|
+
|
|
117
|
+
# Mock subprocess failure
|
|
118
|
+
mock_popen.side_effect = Exception("Failed to start process")
|
|
119
|
+
|
|
120
|
+
success, message = validator.start_streamlit_app(app_path)
|
|
121
|
+
|
|
122
|
+
assert success is False
|
|
123
|
+
assert "Failed to start Streamlit" in message
|
|
124
|
+
assert validator.process is None
|
|
125
|
+
|
|
126
|
+
@patch('requests.get')
|
|
127
|
+
def test_wait_for_app_success(self, mock_get):
|
|
128
|
+
"""Test waiting for app to be ready successfully"""
|
|
129
|
+
validator = StreamlitValidator(port=8502, timeout=5)
|
|
130
|
+
|
|
131
|
+
# Mock successful HTTP response
|
|
132
|
+
mock_response = MagicMock()
|
|
133
|
+
mock_response.status_code = 200
|
|
134
|
+
mock_get.return_value = mock_response
|
|
135
|
+
|
|
136
|
+
success, message = validator.wait_for_app()
|
|
137
|
+
|
|
138
|
+
assert success is True
|
|
139
|
+
assert "App is running successfully" in message
|
|
140
|
+
mock_get.assert_called_with("http://localhost:8502", timeout=5)
|
|
141
|
+
|
|
142
|
+
@patch('requests.get')
|
|
143
|
+
def test_wait_for_app_http_error(self, mock_get):
|
|
144
|
+
"""Test waiting for app with HTTP error"""
|
|
145
|
+
validator = StreamlitValidator(port=8501, timeout=5)
|
|
146
|
+
|
|
147
|
+
# Mock HTTP error response
|
|
148
|
+
mock_response = MagicMock()
|
|
149
|
+
mock_response.status_code = 500
|
|
150
|
+
mock_get.return_value = mock_response
|
|
151
|
+
|
|
152
|
+
success, message = validator.wait_for_app()
|
|
153
|
+
|
|
154
|
+
assert success is False
|
|
155
|
+
assert "App failed to start within timeout" in message
|
|
156
|
+
|
|
157
|
+
@patch('subprocess.Popen')
|
|
158
|
+
def test_check_for_errors_process_running(self, mock_popen):
|
|
159
|
+
"""Test error checking when process is running"""
|
|
160
|
+
validator = StreamlitValidator()
|
|
161
|
+
|
|
162
|
+
# Mock running process
|
|
163
|
+
mock_process = MagicMock()
|
|
164
|
+
mock_process.poll.return_value = None # Process is running
|
|
165
|
+
validator.process = mock_process
|
|
166
|
+
|
|
167
|
+
success, message = validator.check_for_errors()
|
|
168
|
+
|
|
169
|
+
assert success is True
|
|
170
|
+
assert "App is running without errors" in message
|
|
171
|
+
|
|
172
|
+
@patch('subprocess.Popen')
|
|
173
|
+
def test_check_for_errors_process_crashed(self, mock_popen):
|
|
174
|
+
"""Test error checking when process has crashed"""
|
|
175
|
+
validator = StreamlitValidator()
|
|
176
|
+
|
|
177
|
+
# Mock crashed process
|
|
178
|
+
mock_process = MagicMock()
|
|
179
|
+
mock_process.poll.return_value = 1 # Process has exited
|
|
180
|
+
mock_process.communicate.return_value = ("stdout", "stderr error message")
|
|
181
|
+
validator.process = mock_process
|
|
182
|
+
|
|
183
|
+
success, message = validator.check_for_errors()
|
|
184
|
+
|
|
185
|
+
assert success is False
|
|
186
|
+
assert "App crashed" in message
|
|
187
|
+
assert "stderr error message" in message
|
|
188
|
+
|
|
189
|
+
@patch('subprocess.Popen')
|
|
190
|
+
def test_check_for_errors_process_crashed_with_warnings(self, mock_popen):
|
|
191
|
+
"""Test error checking when process crashed but only has warnings"""
|
|
192
|
+
validator = StreamlitValidator()
|
|
193
|
+
|
|
194
|
+
# Mock crashed process with only warnings
|
|
195
|
+
mock_process = MagicMock()
|
|
196
|
+
mock_process.poll.return_value = 1
|
|
197
|
+
mock_process.communicate.return_value = ("stdout", "missing ScriptRunContext warning")
|
|
198
|
+
validator.process = mock_process
|
|
199
|
+
|
|
200
|
+
success, message = validator.check_for_errors()
|
|
201
|
+
|
|
202
|
+
assert success is True
|
|
203
|
+
assert "App is running without errors" in message
|
|
204
|
+
|
|
205
|
+
def test_check_for_errors_no_process(self):
|
|
206
|
+
"""Test error checking when no process exists"""
|
|
207
|
+
validator = StreamlitValidator()
|
|
208
|
+
|
|
209
|
+
success, message = validator.check_for_errors()
|
|
210
|
+
|
|
211
|
+
assert success is False
|
|
212
|
+
assert "No process found" in message
|
|
213
|
+
|
|
214
|
+
@patch('subprocess.Popen')
|
|
215
|
+
def test_cleanup_with_process(self, mock_popen):
|
|
216
|
+
"""Test cleanup with running process"""
|
|
217
|
+
validator = StreamlitValidator()
|
|
218
|
+
|
|
219
|
+
# Mock process
|
|
220
|
+
mock_process = MagicMock()
|
|
221
|
+
validator.process = mock_process
|
|
222
|
+
validator.temp_dir = "/tmp/test_dir"
|
|
223
|
+
|
|
224
|
+
# Mock directory exists
|
|
225
|
+
with patch('os.path.exists', return_value=True):
|
|
226
|
+
with patch('shutil.rmtree') as mock_rmtree:
|
|
227
|
+
validator.cleanup()
|
|
228
|
+
|
|
229
|
+
mock_process.terminate.assert_called_once()
|
|
230
|
+
mock_process.wait.assert_called_once()
|
|
231
|
+
mock_rmtree.assert_called_once_with("/tmp/test_dir")
|
|
232
|
+
|
|
233
|
+
assert validator.process is None
|
|
234
|
+
assert validator.temp_dir is None # type: ignore[unreachable]
|
|
235
|
+
|
|
236
|
+
def test_cleanup_without_process(self):
|
|
237
|
+
"""Test cleanup without process"""
|
|
238
|
+
validator = StreamlitValidator()
|
|
239
|
+
|
|
240
|
+
# Should not raise any exceptions
|
|
241
|
+
validator.cleanup()
|
|
242
|
+
|
|
243
|
+
assert validator.process is None
|
|
244
|
+
assert validator.temp_dir is None
|
|
245
|
+
|
|
246
|
+
@patch('subprocess.Popen')
|
|
247
|
+
@patch('requests.get')
|
|
248
|
+
def test_validate_app_success(self, mock_get, mock_popen):
|
|
249
|
+
"""Test complete validation pipeline success"""
|
|
250
|
+
validator = StreamlitValidator(port=8501, timeout=5)
|
|
251
|
+
code = "import streamlit\nst.title('Hello World')"
|
|
252
|
+
|
|
253
|
+
# Mock successful subprocess
|
|
254
|
+
mock_process = MagicMock()
|
|
255
|
+
mock_process.poll.return_value = None
|
|
256
|
+
mock_popen.return_value = mock_process
|
|
257
|
+
|
|
258
|
+
# Mock successful HTTP response
|
|
259
|
+
mock_response = MagicMock()
|
|
260
|
+
mock_response.status_code = 200
|
|
261
|
+
mock_get.return_value = mock_response
|
|
262
|
+
|
|
263
|
+
results = validator.validate_app(code)
|
|
264
|
+
|
|
265
|
+
assert results['syntax_valid'] is True
|
|
266
|
+
assert results['app_starts'] is True
|
|
267
|
+
assert results['app_responsive'] is True
|
|
268
|
+
assert len(results['errors']) == 0
|
|
269
|
+
|
|
270
|
+
def test_validate_app_syntax_error(self):
|
|
271
|
+
"""Test validation pipeline with syntax error"""
|
|
272
|
+
validator = StreamlitValidator()
|
|
273
|
+
code = "import streamlit\nst.title('Hello World' # Missing parenthesis"
|
|
274
|
+
|
|
275
|
+
results = validator.validate_app(code)
|
|
276
|
+
|
|
277
|
+
assert results['syntax_valid'] is False
|
|
278
|
+
assert results['app_starts'] is False
|
|
279
|
+
assert results['app_responsive'] is False
|
|
280
|
+
assert len(results['errors']) == 1
|
|
281
|
+
assert "Syntax error" in results['errors'][0]
|
|
282
|
+
|
|
283
|
+
@patch('subprocess.Popen')
|
|
284
|
+
def test_validate_app_startup_failure(self, mock_popen):
|
|
285
|
+
"""Test validation pipeline with startup failure"""
|
|
286
|
+
validator = StreamlitValidator()
|
|
287
|
+
code = "import streamlit\nst.title('Hello World')"
|
|
288
|
+
|
|
289
|
+
# Mock startup failure
|
|
290
|
+
mock_popen.side_effect = Exception("Failed to start")
|
|
291
|
+
|
|
292
|
+
results = validator.validate_app(code)
|
|
293
|
+
|
|
294
|
+
assert results['syntax_valid'] is True
|
|
295
|
+
assert results['app_starts'] is False
|
|
296
|
+
assert results['app_responsive'] is False
|
|
297
|
+
assert len(results['errors']) == 1
|
|
298
|
+
assert "Failed to start Streamlit" in results['errors'][0]
|
|
299
|
+
|
|
300
|
+
@patch('subprocess.Popen')
|
|
301
|
+
@patch('requests.get')
|
|
302
|
+
def test_validate_app_runtime_error(self, mock_get, mock_popen):
|
|
303
|
+
"""Test validation pipeline with runtime error"""
|
|
304
|
+
validator = StreamlitValidator(port=8501, timeout=5)
|
|
305
|
+
code = "import streamlit\nst.title('Hello World')"
|
|
306
|
+
|
|
307
|
+
# Mock successful startup
|
|
308
|
+
mock_process = MagicMock()
|
|
309
|
+
mock_process.poll.return_value = 1 # Process crashed
|
|
310
|
+
mock_process.communicate.return_value = ("stdout", "Runtime error occurred")
|
|
311
|
+
mock_popen.return_value = mock_process
|
|
312
|
+
|
|
313
|
+
# Mock successful HTTP response
|
|
314
|
+
mock_response = MagicMock()
|
|
315
|
+
mock_response.status_code = 200
|
|
316
|
+
mock_get.return_value = mock_response
|
|
317
|
+
|
|
318
|
+
results = validator.validate_app(code)
|
|
319
|
+
|
|
320
|
+
assert results['syntax_valid'] is True
|
|
321
|
+
assert results['app_starts'] is True
|
|
322
|
+
assert results['app_responsive'] is True
|
|
323
|
+
assert len(results['errors']) == 1
|
|
324
|
+
assert "App crashed" in results['errors'][0]
|
|
325
|
+
|
|
326
|
+
def test_validate_app_exception_handling(self):
|
|
327
|
+
"""Test validation pipeline exception handling"""
|
|
328
|
+
validator = StreamlitValidator()
|
|
329
|
+
code = "import streamlit\nst.title('Hello World')"
|
|
330
|
+
|
|
331
|
+
# Mock an exception during validation
|
|
332
|
+
with patch.object(validator, 'validate_syntax', side_effect=Exception("Unexpected error")):
|
|
333
|
+
results = validator.validate_app(code)
|
|
334
|
+
|
|
335
|
+
assert results['syntax_valid'] is False
|
|
336
|
+
assert results['app_starts'] is False
|
|
337
|
+
assert results['app_responsive'] is False
|
|
338
|
+
assert len(results['errors']) == 1
|
|
339
|
+
assert "Validation error" in results['errors'][0]
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
class TestStreamlitCodeValidator:
|
|
343
|
+
"""Test cases for streamlit_code_validator function"""
|
|
344
|
+
|
|
345
|
+
def test_streamlit_code_validator_success(self):
|
|
346
|
+
"""Test successful code validation"""
|
|
347
|
+
code = "import streamlit\nst.title('Hello World')"
|
|
348
|
+
|
|
349
|
+
with patch('mito_ai.streamlit_conversion.validate_and_run_streamlit_code.StreamlitValidator') as mock_validator_class:
|
|
350
|
+
mock_validator = MagicMock()
|
|
351
|
+
mock_validator_class.return_value = mock_validator
|
|
352
|
+
|
|
353
|
+
mock_validator.validate_app.return_value = {
|
|
354
|
+
'syntax_valid': True,
|
|
355
|
+
'app_starts': True,
|
|
356
|
+
'app_responsive': True,
|
|
357
|
+
'errors': []
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
has_error, message = streamlit_code_validator(code)
|
|
361
|
+
|
|
362
|
+
assert has_error is False
|
|
363
|
+
assert "Errors found" not in message
|
|
364
|
+
mock_validator.validate_app.assert_called_once_with(code)
|
|
365
|
+
|
|
366
|
+
def test_streamlit_code_validator_error_in_code(self):
|
|
367
|
+
"""Test code validation when code contains 'error'"""
|
|
368
|
+
code = "error in the code"
|
|
369
|
+
|
|
370
|
+
has_error, message = streamlit_code_validator(code)
|
|
371
|
+
|
|
372
|
+
assert has_error is True
|
|
373
|
+
assert "Errors found" in message
|
|
374
|
+
|
|
375
|
+
def test_streamlit_code_validator_empty_code(self):
|
|
376
|
+
"""Test code validation with empty code"""
|
|
377
|
+
code = ""
|
|
378
|
+
|
|
379
|
+
with patch('mito_ai.streamlit_conversion.validate_and_run_streamlit_code.StreamlitValidator') as mock_validator_class:
|
|
380
|
+
mock_validator = MagicMock()
|
|
381
|
+
mock_validator_class.return_value = mock_validator
|
|
382
|
+
|
|
383
|
+
mock_validator.validate_app.return_value = {
|
|
384
|
+
'syntax_valid': True,
|
|
385
|
+
'app_starts': True,
|
|
386
|
+
'app_responsive': True,
|
|
387
|
+
'errors': []
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
has_error, message = streamlit_code_validator(code)
|
|
391
|
+
|
|
392
|
+
assert has_error is False
|
|
393
|
+
assert "Errors found" not in message
|
|
394
|
+
|
|
395
|
+
def test_streamlit_code_validator_multiple_errors(self):
|
|
396
|
+
"""Test code validation with multiple errors"""
|
|
397
|
+
code = "import streamlit\nst.title('Hello World')"
|
|
398
|
+
|
|
399
|
+
with patch('mito_ai.streamlit_conversion.validate_and_run_streamlit_code.StreamlitValidator') as mock_validator_class:
|
|
400
|
+
mock_validator = MagicMock()
|
|
401
|
+
mock_validator_class.return_value = mock_validator
|
|
402
|
+
|
|
403
|
+
mock_validator.validate_app.return_value = {
|
|
404
|
+
'syntax_valid': False,
|
|
405
|
+
'app_starts': False,
|
|
406
|
+
'app_responsive': False,
|
|
407
|
+
'errors': [
|
|
408
|
+
'Syntax error: invalid syntax',
|
|
409
|
+
'App failed to start'
|
|
410
|
+
]
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
has_error, message = streamlit_code_validator(code)
|
|
414
|
+
|
|
415
|
+
assert has_error is True
|
|
416
|
+
assert "Errors found" in message
|
|
417
|
+
assert "Syntax error" in message
|
|
418
|
+
assert "App failed to start" in message
|
mito_ai/tests/test_constants.py
CHANGED
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
|
|
4
4
|
from typing import Any
|
|
5
5
|
import pytest
|
|
6
|
-
from mito_ai.constants import
|
|
7
|
-
|
|
6
|
+
from mito_ai.constants import (
|
|
7
|
+
ACTIVE_BASE_URL, MITO_PROD_BASE_URL, MITO_DEV_BASE_URL,
|
|
8
|
+
MITO_STREAMLIT_DEV_BASE_URL, MITO_STREAMLIT_TEST_BASE_URL, ACTIVE_STREAMLIT_BASE_URL,
|
|
9
|
+
COGNITO_CONFIG_DEV, ACTIVE_COGNITO_CONFIG,
|
|
10
|
+
)
|
|
8
11
|
|
|
9
12
|
|
|
10
13
|
def test_prod_lambda_url() -> Any:
|
|
@@ -29,4 +32,16 @@ def test_testenv_streamlit_url() -> Any:
|
|
|
29
32
|
|
|
30
33
|
def test_streamlit_active_base_url() -> Any:
|
|
31
34
|
"""Make sure that the active streamlit base url is correct"""
|
|
32
|
-
assert ACTIVE_STREAMLIT_BASE_URL ==
|
|
35
|
+
assert ACTIVE_STREAMLIT_BASE_URL == MITO_STREAMLIT_DEV_BASE_URL
|
|
36
|
+
|
|
37
|
+
def test_cognito_config() -> Any:
|
|
38
|
+
"""Make sure that the Cognito configuration is correct"""
|
|
39
|
+
expected_config = {
|
|
40
|
+
'TOKEN_ENDPOINT': 'https://mito-app-auth.auth.us-east-1.amazoncognito.com/oauth2/token',
|
|
41
|
+
'CLIENT_ID': '6ara3u3l8sss738hrhbq1qtiqf',
|
|
42
|
+
'CLIENT_SECRET': '',
|
|
43
|
+
'REDIRECT_URI': 'http://localhost:8888/lab'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
assert COGNITO_CONFIG_DEV == expected_config
|
|
47
|
+
assert ACTIVE_COGNITO_CONFIG == COGNITO_CONFIG_DEV
|
mito_ai/utils/anthropic_utils.py
CHANGED
|
@@ -8,7 +8,7 @@ import anthropic
|
|
|
8
8
|
from typing import Any, Dict, List, Optional, Union, AsyncGenerator, Tuple, Callable, cast
|
|
9
9
|
|
|
10
10
|
from anthropic.types import MessageParam, Message, TextBlock, ToolUnionParam
|
|
11
|
-
from mito_ai.utils.mito_server_utils import get_response_from_mito_server
|
|
11
|
+
from mito_ai.utils.mito_server_utils import get_response_from_mito_server, stream_response_from_mito_server
|
|
12
12
|
from mito_ai.utils.provider_utils import does_message_require_fast_model
|
|
13
13
|
from openai.types.chat import ChatCompletionMessageParam
|
|
14
14
|
from mito_ai.completions.models import AgentResponse, MessageType, ResponseFormatInfo, CompletionReply, CompletionStreamChunk, CompletionItem
|
|
@@ -110,75 +110,23 @@ async def stream_anthropic_completion_from_mito_server(
|
|
|
110
110
|
data, headers = _prepare_anthropic_request_data_and_headers(
|
|
111
111
|
model, max_tokens, temperature, system, messages, message_type, None, None, stream
|
|
112
112
|
)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
request_timeout=http_client_timeout,
|
|
131
|
-
streaming_callback=chunk_callback
|
|
132
|
-
)
|
|
133
|
-
async def wait_for_fetch() -> None:
|
|
134
|
-
try:
|
|
135
|
-
await fetch_future
|
|
136
|
-
nonlocal fetch_complete
|
|
137
|
-
fetch_complete = True
|
|
138
|
-
print("Anthropic fetch completed")
|
|
139
|
-
except Exception as e:
|
|
140
|
-
print(f"Error in Anthropic fetch: {str(e)}")
|
|
141
|
-
raise
|
|
142
|
-
fetch_task = asyncio.create_task(wait_for_fetch())
|
|
143
|
-
while not (fetch_complete and chunk_queue.empty()):
|
|
144
|
-
try:
|
|
145
|
-
chunk = await asyncio.wait_for(chunk_queue.get(), timeout=0.1)
|
|
146
|
-
if reply_fn and message_id:
|
|
147
|
-
reply_fn(CompletionStreamChunk(
|
|
148
|
-
parent_id=message_id,
|
|
149
|
-
chunk=CompletionItem(
|
|
150
|
-
content=chunk,
|
|
151
|
-
isIncomplete=True,
|
|
152
|
-
token=message_id,
|
|
153
|
-
),
|
|
154
|
-
done=False,
|
|
155
|
-
))
|
|
156
|
-
yield chunk
|
|
157
|
-
except asyncio.TimeoutError:
|
|
158
|
-
if fetch_complete and chunk_queue.empty():
|
|
159
|
-
break
|
|
160
|
-
continue
|
|
161
|
-
print(f"\nAnthropic stream completed in {time.time() - start_time:.2f} seconds")
|
|
162
|
-
if reply_fn and message_id:
|
|
163
|
-
reply_fn(CompletionStreamChunk(
|
|
164
|
-
parent_id=message_id,
|
|
165
|
-
chunk=CompletionItem(
|
|
166
|
-
content="",
|
|
167
|
-
isIncomplete=False,
|
|
168
|
-
token=message_id,
|
|
169
|
-
),
|
|
170
|
-
done=True,
|
|
171
|
-
))
|
|
172
|
-
except Exception as e:
|
|
173
|
-
print(f"\nAnthropic stream failed after {time.time() - start_time:.2f} seconds with error: {str(e)}")
|
|
174
|
-
if fetch_future:
|
|
175
|
-
try:
|
|
176
|
-
await fetch_future
|
|
177
|
-
except Exception:
|
|
178
|
-
pass
|
|
179
|
-
raise
|
|
180
|
-
finally:
|
|
181
|
-
http_client.close()
|
|
113
|
+
# Use the unified streaming function
|
|
114
|
+
# If the reply_fn and message_id are empty, this function still handles those requests. This is particularly needed for the streamlit dashboard functionality
|
|
115
|
+
actual_reply_fn = reply_fn if reply_fn is not None else (lambda x: None)
|
|
116
|
+
actual_message_id = message_id if message_id is not None else ""
|
|
117
|
+
async for chunk in stream_response_from_mito_server(
|
|
118
|
+
url=MITO_ANTHROPIC_URL,
|
|
119
|
+
headers=headers,
|
|
120
|
+
data=data,
|
|
121
|
+
timeout=timeout,
|
|
122
|
+
max_retries=max_retries,
|
|
123
|
+
message_type=message_type,
|
|
124
|
+
reply_fn=actual_reply_fn,
|
|
125
|
+
message_id=actual_message_id,
|
|
126
|
+
chunk_processor=None,
|
|
127
|
+
provider_name="Claude",
|
|
128
|
+
):
|
|
129
|
+
yield chunk
|
|
182
130
|
|
|
183
131
|
def get_anthropic_completion_function_params(
|
|
184
132
|
message_type: MessageType,
|
mito_ai/utils/gemini_utils.py
CHANGED
|
@@ -5,7 +5,7 @@ import asyncio
|
|
|
5
5
|
import json
|
|
6
6
|
import time
|
|
7
7
|
from typing import Any, Dict, List, Optional, Callable, Union, AsyncGenerator, Tuple
|
|
8
|
-
from mito_ai.utils.mito_server_utils import get_response_from_mito_server
|
|
8
|
+
from mito_ai.utils.mito_server_utils import get_response_from_mito_server, stream_response_from_mito_server
|
|
9
9
|
from mito_ai.completions.models import AgentResponse, CompletionReply, CompletionStreamChunk, CompletionItem, MessageType
|
|
10
10
|
from mito_ai.constants import MITO_GEMINI_URL
|
|
11
11
|
from mito_ai.utils.provider_utils import does_message_require_fast_model
|
|
@@ -80,80 +80,29 @@ async def stream_gemini_completion_from_mito_server(
|
|
|
80
80
|
contents: List[Dict[str, Any]],
|
|
81
81
|
message_type: MessageType,
|
|
82
82
|
message_id: str,
|
|
83
|
-
reply_fn:
|
|
83
|
+
reply_fn: Callable[[Union[CompletionReply, CompletionStreamChunk]], None]
|
|
84
84
|
) -> AsyncGenerator[str, None]:
|
|
85
85
|
data, headers = _prepare_gemini_request_data_and_headers(model, contents, message_type, stream=True)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
async def wait_for_fetch() -> None:
|
|
107
|
-
try:
|
|
108
|
-
await fetch_future
|
|
109
|
-
nonlocal fetch_complete
|
|
110
|
-
fetch_complete = True
|
|
111
|
-
print("Gemini fetch completed")
|
|
112
|
-
except Exception as e:
|
|
113
|
-
print(f"Error in Gemini fetch: {str(e)}")
|
|
114
|
-
raise
|
|
115
|
-
fetch_task = asyncio.create_task(wait_for_fetch())
|
|
116
|
-
while not (fetch_complete and chunk_queue.empty()):
|
|
117
|
-
try:
|
|
118
|
-
chunk = await asyncio.wait_for(chunk_queue.get(), timeout=0.1)
|
|
119
|
-
clean_chunk = chunk.strip('"')
|
|
120
|
-
decoded_chunk = clean_chunk.encode().decode('unicode_escape')
|
|
121
|
-
if reply_fn and message_id:
|
|
122
|
-
reply_fn(CompletionStreamChunk(
|
|
123
|
-
parent_id=message_id,
|
|
124
|
-
chunk=CompletionItem(
|
|
125
|
-
content=decoded_chunk,
|
|
126
|
-
isIncomplete=True,
|
|
127
|
-
token=message_id,
|
|
128
|
-
),
|
|
129
|
-
done=False,
|
|
130
|
-
))
|
|
131
|
-
yield chunk
|
|
132
|
-
except asyncio.TimeoutError:
|
|
133
|
-
if fetch_complete and chunk_queue.empty():
|
|
134
|
-
break
|
|
135
|
-
continue
|
|
136
|
-
print(f"\nGemini stream completed in {time.time() - start_time:.2f} seconds")
|
|
137
|
-
if reply_fn and message_id:
|
|
138
|
-
reply_fn(CompletionStreamChunk(
|
|
139
|
-
parent_id=message_id,
|
|
140
|
-
chunk=CompletionItem(
|
|
141
|
-
content="",
|
|
142
|
-
isIncomplete=False,
|
|
143
|
-
token=message_id,
|
|
144
|
-
),
|
|
145
|
-
done=True,
|
|
146
|
-
))
|
|
147
|
-
except Exception as e:
|
|
148
|
-
print(f"\nGemini stream failed after {time.time() - start_time:.2f} seconds with error: {str(e)}")
|
|
149
|
-
if fetch_future:
|
|
150
|
-
try:
|
|
151
|
-
await fetch_future
|
|
152
|
-
except Exception:
|
|
153
|
-
pass
|
|
154
|
-
raise
|
|
155
|
-
finally:
|
|
156
|
-
http_client.close()
|
|
86
|
+
|
|
87
|
+
# Define chunk processor for Gemini's special processing
|
|
88
|
+
def gemini_chunk_processor(chunk: str) -> str:
|
|
89
|
+
clean_chunk = chunk.strip('"')
|
|
90
|
+
return clean_chunk.encode().decode('unicode_escape')
|
|
91
|
+
|
|
92
|
+
# Use the unified streaming function with Gemini's chunk processor
|
|
93
|
+
async for chunk in stream_response_from_mito_server(
|
|
94
|
+
url=MITO_GEMINI_URL,
|
|
95
|
+
headers=headers,
|
|
96
|
+
data=data,
|
|
97
|
+
timeout=timeout,
|
|
98
|
+
max_retries=max_retries,
|
|
99
|
+
message_type=message_type,
|
|
100
|
+
reply_fn=reply_fn,
|
|
101
|
+
message_id=message_id,
|
|
102
|
+
chunk_processor=gemini_chunk_processor,
|
|
103
|
+
provider_name="Gemini",
|
|
104
|
+
):
|
|
105
|
+
yield chunk
|
|
157
106
|
|
|
158
107
|
def get_gemini_completion_function_params(
|
|
159
108
|
message_type: MessageType,
|