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.

Files changed (54) hide show
  1. mito_ai/__init__.py +6 -4
  2. mito_ai/_version.py +1 -1
  3. mito_ai/anthropic_client.py +3 -10
  4. mito_ai/app_builder/handlers.py +89 -11
  5. mito_ai/app_builder/models.py +3 -0
  6. mito_ai/auth/README.md +18 -0
  7. mito_ai/auth/__init__.py +6 -0
  8. mito_ai/auth/handlers.py +96 -0
  9. mito_ai/auth/urls.py +13 -0
  10. mito_ai/completions/completion_handlers/chat_completion_handler.py +2 -2
  11. mito_ai/completions/models.py +7 -6
  12. mito_ai/completions/prompt_builders/agent_execution_prompt.py +8 -3
  13. mito_ai/completions/prompt_builders/agent_system_message.py +21 -7
  14. mito_ai/completions/prompt_builders/chat_prompt.py +18 -11
  15. mito_ai/completions/prompt_builders/utils.py +53 -10
  16. mito_ai/constants.py +11 -1
  17. mito_ai/streamlit_conversion/streamlit_agent_handler.py +112 -0
  18. mito_ai/streamlit_conversion/streamlit_system_prompt.py +42 -0
  19. mito_ai/streamlit_conversion/streamlit_utils.py +96 -0
  20. mito_ai/streamlit_conversion/validate_and_run_streamlit_code.py +207 -0
  21. mito_ai/tests/providers/test_stream_mito_server_utils.py +140 -0
  22. mito_ai/tests/streamlit_conversion/__init__.py +3 -0
  23. mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +265 -0
  24. mito_ai/tests/streamlit_conversion/test_streamlit_utils.py +197 -0
  25. mito_ai/tests/streamlit_conversion/test_validate_and_run_streamlit_code.py +418 -0
  26. mito_ai/tests/test_constants.py +18 -3
  27. mito_ai/utils/anthropic_utils.py +18 -70
  28. mito_ai/utils/gemini_utils.py +22 -73
  29. mito_ai/utils/mito_server_utils.py +147 -4
  30. mito_ai/utils/open_ai_utils.py +18 -107
  31. {mito_ai-0.1.36.data → mito_ai-0.1.37.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +100 -100
  32. {mito_ai-0.1.36.data → mito_ai-0.1.37.data}/data/share/jupyter/labextensions/mito_ai/package.json +2 -2
  33. {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
  34. 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
  35. mito_ai-0.1.37.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.831f63b48760c7119b9b.js.map +1 -0
  36. 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
  37. mito_ai-0.1.37.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.93ecc9bc0edba61535cc.js.map +1 -0
  38. 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
  39. mito_ai-0.1.37.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js.map +1 -0
  40. {mito_ai-0.1.36.dist-info → mito_ai-0.1.37.dist-info}/METADATA +1 -1
  41. {mito_ai-0.1.36.dist-info → mito_ai-0.1.37.dist-info}/RECORD +51 -38
  42. mito_ai-0.1.36.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.a20772bc113422d0f505.js.map +0 -1
  43. mito_ai-0.1.36.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.5c9333902dce30642119.js.map +0 -1
  44. mito_ai-0.1.36.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.76efcc5c3be4056457ee.js.map +0 -1
  45. {mito_ai-0.1.36.data → mito_ai-0.1.37.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
  46. {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
  47. {mito_ai-0.1.36.data → mito_ai-0.1.37.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
  48. {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
  49. {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
  50. {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
  51. {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
  52. {mito_ai-0.1.36.dist-info → mito_ai-0.1.37.dist-info}/WHEEL +0 -0
  53. {mito_ai-0.1.36.dist-info → mito_ai-0.1.37.dist-info}/entry_points.txt +0 -0
  54. {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
@@ -3,8 +3,11 @@
3
3
 
4
4
  from typing import Any
5
5
  import pytest
6
- from mito_ai.constants import ACTIVE_BASE_URL, MITO_PROD_BASE_URL, MITO_DEV_BASE_URL
7
- from mito_ai.constants import MITO_STREAMLIT_DEV_BASE_URL, MITO_STREAMLIT_TEST_BASE_URL, ACTIVE_STREAMLIT_BASE_URL
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 == MITO_STREAMLIT_TEST_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
@@ -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
- http_client, http_client_timeout = _create_http_client(timeout, max_retries)
114
- start_time = time.time()
115
- chunk_queue: asyncio.Queue[str] = asyncio.Queue()
116
- fetch_complete = False
117
- def chunk_callback(chunk: bytes) -> None:
118
- try:
119
- chunk_str = chunk.decode('utf-8')
120
- asyncio.create_task(chunk_queue.put(chunk_str))
121
- except Exception as e:
122
- print(f"Error processing Anthropic streaming chunk: {str(e)}")
123
- fetch_future = None
124
- try:
125
- fetch_future = http_client.fetch(
126
- MITO_ANTHROPIC_URL,
127
- method="POST",
128
- headers=headers,
129
- body=json.dumps(data),
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,
@@ -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: Optional[Callable[[Union[CompletionReply, CompletionStreamChunk]], None]]
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
- http_client, http_client_timeout = _create_http_client(timeout, max_retries)
87
- start_time = time.time()
88
- chunk_queue: asyncio.Queue[str] = asyncio.Queue()
89
- fetch_complete = False
90
- def chunk_callback(chunk: bytes) -> None:
91
- try:
92
- chunk_str = chunk.decode('utf-8')
93
- asyncio.create_task(chunk_queue.put(chunk_str))
94
- except Exception as e:
95
- print(f"Error processing Gemini streaming chunk: {str(e)}")
96
- fetch_future = None
97
- try:
98
- fetch_future = http_client.fetch(
99
- MITO_GEMINI_URL,
100
- method="POST",
101
- headers=headers,
102
- body=json.dumps(data),
103
- request_timeout=http_client_timeout,
104
- streaming_callback=chunk_callback
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,