mito-ai 0.1.39__py3-none-any.whl → 0.1.41__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 +12 -6
- mito_ai/_version.py +1 -1
- mito_ai/app_builder/handlers.py +1 -2
- mito_ai/completions/handlers.py +1 -1
- mito_ai/completions/message_history.py +9 -1
- mito_ai/completions/models.py +1 -1
- mito_ai/completions/prompt_builders/agent_execution_prompt.py +2 -0
- mito_ai/completions/prompt_builders/agent_smart_debug_prompt.py +8 -0
- mito_ai/completions/prompt_builders/agent_system_message.py +17 -0
- mito_ai/constants.py +3 -2
- mito_ai/file_uploads/__init__.py +3 -0
- mito_ai/file_uploads/handlers.py +225 -0
- mito_ai/file_uploads/urls.py +21 -0
- mito_ai/openai_client.py +1 -1
- mito_ai/tests/file_uploads/__init__.py +2 -0
- mito_ai/tests/file_uploads/test_handlers.py +267 -0
- mito_ai/tests/message_history/test_message_history_utils.py +57 -4
- mito_ai/utils/mito_server_utils.py +7 -0
- mito_ai/utils/server_limits.py +1 -1
- mito_ai/utils/telemetry_utils.py +26 -9
- {mito_ai-0.1.39.data → mito_ai-0.1.41.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +102 -100
- {mito_ai-0.1.39.data → mito_ai-0.1.41.data}/data/share/jupyter/labextensions/mito_ai/package.json +4 -2
- {mito_ai-0.1.39.data → mito_ai-0.1.41.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/package.json.orig +3 -1
- mito_ai-0.1.39.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.16b532b655cd2906e04a.js → mito_ai-0.1.41.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.01a962c68c8fae380f30.js +1782 -891
- mito_ai-0.1.41.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.01a962c68c8fae380f30.js.map +1 -0
- mito_ai-0.1.41.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_aws-amplify_core_dist_esm_singleton_apis_fetchAuthSession_mjs.182232e7bc6311fe4528.js +63 -0
- mito_ai-0.1.41.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_aws-amplify_core_dist_esm_singleton_apis_fetchAuthSession_mjs.182232e7bc6311fe4528.js.map +1 -0
- mito_ai-0.1.39.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.606207904e6aaa42b1bf.js → mito_ai-0.1.41.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.9a70f033717ba8689564.js +49 -25
- mito_ai-0.1.41.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.9a70f033717ba8689564.js.map +1 -0
- mito_ai-0.1.41.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_tokenProvider_tokenProvider_mjs.16430abf3466c3153f59.js +4574 -0
- mito_ai-0.1.41.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_tokenProvider_tokenProvider_mjs.16430abf3466c3153f59.js.map +1 -0
- mito_ai-0.1.41.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_core_dist_esm_singleton_Amplify_mjs.3c0035b95fe369aede82.js +2345 -0
- mito_ai-0.1.41.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_core_dist_esm_singleton_Amplify_mjs.3c0035b95fe369aede82.js.map +1 -0
- mito_ai-0.1.41.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_core_dist_esm_singleton_apis_fetchAuthSession_mjs-node_modul-758875.dc495fd682071d97070c.js +7498 -0
- mito_ai-0.1.41.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_core_dist_esm_singleton_apis_fetchAuthSession_mjs-node_modul-758875.dc495fd682071d97070c.js.map +1 -0
- mito_ai-0.1.41.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js +1021 -0
- mito_ai-0.1.41.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js.map +1 -0
- mito_ai-0.1.41.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.61289bff0db44828605b.js +60178 -0
- mito_ai-0.1.41.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.61289bff0db44828605b.js.map +1 -0
- {mito_ai-0.1.39.dist-info → mito_ai-0.1.41.dist-info}/METADATA +1 -1
- {mito_ai-0.1.39.dist-info → mito_ai-0.1.41.dist-info}/RECORD +53 -36
- mito_ai-0.1.39.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.16b532b655cd2906e04a.js.map +0 -1
- mito_ai-0.1.39.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.606207904e6aaa42b1bf.js.map +0 -1
- {mito_ai-0.1.39.data → mito_ai-0.1.41.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
- {mito_ai-0.1.39.data → mito_ai-0.1.41.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/toolbar-buttons.json +0 -0
- {mito_ai-0.1.39.data → mito_ai-0.1.41.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
- {mito_ai-0.1.39.data → mito_ai-0.1.41.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js +0 -0
- {mito_ai-0.1.39.data → mito_ai-0.1.41.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js.map +0 -0
- {mito_ai-0.1.39.data → mito_ai-0.1.41.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.9795f79265ddb416864b.js +0 -0
- {mito_ai-0.1.39.data → mito_ai-0.1.41.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.9795f79265ddb416864b.js.map +0 -0
- {mito_ai-0.1.39.data → mito_ai-0.1.41.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js +0 -0
- {mito_ai-0.1.39.data → mito_ai-0.1.41.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.39.dist-info → mito_ai-0.1.41.dist-info}/WHEEL +0 -0
- {mito_ai-0.1.39.dist-info → mito_ai-0.1.41.dist-info}/entry_points.txt +0 -0
- {mito_ai-0.1.39.dist-info → mito_ai-0.1.41.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,267 @@
|
|
|
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
|
+
import pytest
|
|
7
|
+
from unittest.mock import Mock, patch
|
|
8
|
+
import tornado.web
|
|
9
|
+
from tornado.httputil import HTTPServerRequest
|
|
10
|
+
from tornado.web import Application
|
|
11
|
+
|
|
12
|
+
from mito_ai.file_uploads.handlers import FileUploadHandler
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture
|
|
16
|
+
def temp_dir():
|
|
17
|
+
"""Create a temporary directory for test files."""
|
|
18
|
+
temp_dir = tempfile.mkdtemp()
|
|
19
|
+
original_cwd = os.getcwd()
|
|
20
|
+
os.chdir(temp_dir)
|
|
21
|
+
yield temp_dir
|
|
22
|
+
os.chdir(original_cwd)
|
|
23
|
+
# Clean up temporary files
|
|
24
|
+
for file in os.listdir(temp_dir):
|
|
25
|
+
os.remove(os.path.join(temp_dir, file))
|
|
26
|
+
os.rmdir(temp_dir)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pytest.fixture
|
|
30
|
+
def handler():
|
|
31
|
+
"""Create a FileUploadHandler instance for testing."""
|
|
32
|
+
app = Application()
|
|
33
|
+
request = HTTPServerRequest(method="POST", uri="/upload")
|
|
34
|
+
|
|
35
|
+
# Mock the connection to avoid Tornado's assertion
|
|
36
|
+
request.connection = Mock()
|
|
37
|
+
|
|
38
|
+
handler = FileUploadHandler(app, request)
|
|
39
|
+
|
|
40
|
+
# Mock methods properly to avoid mypy errors
|
|
41
|
+
handler.write = Mock() # type: ignore
|
|
42
|
+
handler.finish = Mock() # type: ignore
|
|
43
|
+
handler.set_status = Mock() # type: ignore
|
|
44
|
+
handler.get_argument = Mock() # type: ignore
|
|
45
|
+
|
|
46
|
+
# Mock authentication for Jupyter server
|
|
47
|
+
handler._jupyter_current_user = "test_user" # type: ignore
|
|
48
|
+
|
|
49
|
+
return handler
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_validate_file_upload_success(handler):
|
|
53
|
+
"""Test successful file upload validation."""
|
|
54
|
+
handler.request.files = {"file": [Mock(filename="test.csv", body=b"data")]} # type: ignore
|
|
55
|
+
result = handler._validate_file_upload()
|
|
56
|
+
assert result is True
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_validate_file_upload_failure(handler):
|
|
60
|
+
"""Test file upload validation when no file is present."""
|
|
61
|
+
handler.request.files = {} # type: ignore
|
|
62
|
+
result = handler._validate_file_upload()
|
|
63
|
+
assert result is False
|
|
64
|
+
handler.set_status.assert_called_with(400)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_regular_upload_success(handler, temp_dir):
|
|
68
|
+
"""Test successful regular (non-chunked) file upload."""
|
|
69
|
+
filename = "test.csv"
|
|
70
|
+
file_data = b"test,data\n1,2"
|
|
71
|
+
notebook_dir = temp_dir
|
|
72
|
+
|
|
73
|
+
handler._handle_regular_upload(filename, file_data, notebook_dir)
|
|
74
|
+
|
|
75
|
+
# Verify file was written
|
|
76
|
+
file_path = os.path.join(notebook_dir, filename)
|
|
77
|
+
with open(file_path, "rb") as f:
|
|
78
|
+
content = f.read()
|
|
79
|
+
assert content == file_data
|
|
80
|
+
|
|
81
|
+
# Verify response
|
|
82
|
+
handler.write.assert_called_with(
|
|
83
|
+
{"success": True, "filename": filename, "path": file_path}
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_chunked_upload_first_chunk(handler, temp_dir):
|
|
88
|
+
"""Test handling first chunk of a chunked upload."""
|
|
89
|
+
filename = "large_file.csv"
|
|
90
|
+
file_data = b"chunk1_data"
|
|
91
|
+
chunk_number = "1"
|
|
92
|
+
total_chunks = "3"
|
|
93
|
+
notebook_dir = temp_dir
|
|
94
|
+
|
|
95
|
+
handler._handle_chunked_upload(
|
|
96
|
+
filename, file_data, chunk_number, total_chunks, notebook_dir
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Verify chunk was saved (check temp dir structure)
|
|
100
|
+
assert filename in handler._temp_dirs
|
|
101
|
+
temp_dir_path = handler._temp_dirs[filename]["temp_dir"]
|
|
102
|
+
chunk_file = os.path.join(temp_dir_path, "chunk_1")
|
|
103
|
+
assert os.path.exists(chunk_file)
|
|
104
|
+
|
|
105
|
+
# Verify response indicates chunk received but not complete
|
|
106
|
+
handler.write.assert_called_with(
|
|
107
|
+
{
|
|
108
|
+
"success": True,
|
|
109
|
+
"chunk_received": True,
|
|
110
|
+
"chunk_number": 1,
|
|
111
|
+
"total_chunks": 3,
|
|
112
|
+
}
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_chunked_upload_completion(handler, temp_dir):
|
|
117
|
+
"""Test completing a chunked upload when all chunks are received."""
|
|
118
|
+
filename = "large_file.csv"
|
|
119
|
+
total_chunks = 2
|
|
120
|
+
notebook_dir = temp_dir
|
|
121
|
+
|
|
122
|
+
# Process first chunk
|
|
123
|
+
handler._handle_chunked_upload(
|
|
124
|
+
filename, b"chunk1_data", "1", str(total_chunks), notebook_dir
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Process final chunk
|
|
128
|
+
handler._handle_chunked_upload(
|
|
129
|
+
filename, b"chunk2_data", "2", str(total_chunks), notebook_dir
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Verify final file was created
|
|
133
|
+
file_path = os.path.join(notebook_dir, filename)
|
|
134
|
+
assert os.path.exists(file_path)
|
|
135
|
+
with open(file_path, "rb") as f:
|
|
136
|
+
content = f.read()
|
|
137
|
+
assert content == b"chunk1_datachunk2_data"
|
|
138
|
+
|
|
139
|
+
# Verify temp dir was cleaned up
|
|
140
|
+
assert filename not in handler._temp_dirs
|
|
141
|
+
|
|
142
|
+
# Verify completion response
|
|
143
|
+
handler.write.assert_called_with(
|
|
144
|
+
{
|
|
145
|
+
"success": True,
|
|
146
|
+
"filename": filename,
|
|
147
|
+
"path": file_path,
|
|
148
|
+
"chunk_complete": True,
|
|
149
|
+
}
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_error_handling(handler):
|
|
154
|
+
"""Test error handling in upload process."""
|
|
155
|
+
error_message = "Test error message"
|
|
156
|
+
status_code = 500
|
|
157
|
+
|
|
158
|
+
handler._handle_error(error_message, status_code)
|
|
159
|
+
|
|
160
|
+
handler.set_status.assert_called_with(status_code)
|
|
161
|
+
handler.write.assert_called_with({"error": error_message})
|
|
162
|
+
handler.finish.assert_called_once()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@patch("mito_ai.file_uploads.handlers.FileUploadHandler._validate_file_upload")
|
|
166
|
+
def test_post_method_regular_upload(mock_validate, handler):
|
|
167
|
+
"""Test POST method for regular upload."""
|
|
168
|
+
mock_validate.return_value = True
|
|
169
|
+
handler.request.files = {"file": [Mock(filename="test.csv", body=b"data")]} # type: ignore
|
|
170
|
+
handler.get_argument.return_value = None # No chunk parameters
|
|
171
|
+
|
|
172
|
+
handler.post()
|
|
173
|
+
|
|
174
|
+
mock_validate.assert_called_once()
|
|
175
|
+
handler.finish.assert_called_once()
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@patch("mito_ai.file_uploads.handlers.FileUploadHandler._validate_file_upload")
|
|
179
|
+
def test_post_method_chunked_upload(mock_validate, handler):
|
|
180
|
+
"""Test POST method for chunked upload."""
|
|
181
|
+
mock_validate.return_value = True
|
|
182
|
+
handler.request.files = {"file": [Mock(filename="test.csv", body=b"data")]} # type: ignore
|
|
183
|
+
handler.get_argument.side_effect = lambda name, default=None: {
|
|
184
|
+
"chunk_number": "1",
|
|
185
|
+
"total_chunks": "3",
|
|
186
|
+
}.get(name, default)
|
|
187
|
+
|
|
188
|
+
handler.post()
|
|
189
|
+
|
|
190
|
+
mock_validate.assert_called_once()
|
|
191
|
+
handler.finish.assert_called_once()
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def test_are_all_chunks_received_true(handler, temp_dir):
|
|
195
|
+
"""Test that all chunks are detected when present."""
|
|
196
|
+
filename = "test.csv"
|
|
197
|
+
total_chunks = 2
|
|
198
|
+
|
|
199
|
+
# Manually set up the temp dir structure
|
|
200
|
+
temp_dir_path = tempfile.mkdtemp(prefix=f"mito_upload_{filename}_")
|
|
201
|
+
handler._temp_dirs[filename] = {
|
|
202
|
+
"temp_dir": temp_dir_path,
|
|
203
|
+
"total_chunks": total_chunks,
|
|
204
|
+
"received_chunks": {1, 2},
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
result = handler._are_all_chunks_received(filename, total_chunks)
|
|
208
|
+
assert result is True
|
|
209
|
+
|
|
210
|
+
# Clean up
|
|
211
|
+
import shutil
|
|
212
|
+
|
|
213
|
+
shutil.rmtree(temp_dir_path)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def test_are_all_chunks_received_false(handler, temp_dir):
|
|
217
|
+
"""Test that missing chunks are detected."""
|
|
218
|
+
filename = "test.csv"
|
|
219
|
+
total_chunks = 2
|
|
220
|
+
|
|
221
|
+
# Manually set up the temp dir structure with only one chunk
|
|
222
|
+
temp_dir_path = tempfile.mkdtemp(prefix=f"mito_upload_{filename}_")
|
|
223
|
+
handler._temp_dirs[filename] = {
|
|
224
|
+
"temp_dir": temp_dir_path,
|
|
225
|
+
"total_chunks": total_chunks,
|
|
226
|
+
"received_chunks": {1}, # Only chunk 1 received
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
result = handler._are_all_chunks_received(filename, total_chunks)
|
|
230
|
+
assert result is False
|
|
231
|
+
|
|
232
|
+
# Clean up
|
|
233
|
+
import shutil
|
|
234
|
+
|
|
235
|
+
shutil.rmtree(temp_dir_path)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def test_save_chunk(handler, temp_dir):
|
|
239
|
+
"""Test saving individual chunks."""
|
|
240
|
+
filename = "test.csv"
|
|
241
|
+
file_data = b"chunk_data"
|
|
242
|
+
chunk_number = 1
|
|
243
|
+
total_chunks = 3
|
|
244
|
+
|
|
245
|
+
# Mock the file operations to avoid filesystem issues
|
|
246
|
+
with patch("builtins.open", create=True) as mock_open:
|
|
247
|
+
mock_file = Mock()
|
|
248
|
+
mock_open.return_value.__enter__.return_value = mock_file
|
|
249
|
+
|
|
250
|
+
handler._save_chunk(filename, file_data, chunk_number, total_chunks)
|
|
251
|
+
|
|
252
|
+
# Verify temp dir was created in the handler's tracking
|
|
253
|
+
assert filename in handler._temp_dirs
|
|
254
|
+
temp_dir_path = handler._temp_dirs[filename]["temp_dir"]
|
|
255
|
+
|
|
256
|
+
# Verify the expected chunk filename was used
|
|
257
|
+
expected_chunk_filename = os.path.join(temp_dir_path, f"chunk_{chunk_number}")
|
|
258
|
+
mock_open.assert_called_with(expected_chunk_filename, "wb")
|
|
259
|
+
|
|
260
|
+
# Verify file data was written
|
|
261
|
+
mock_file.write.assert_called_with(file_data)
|
|
262
|
+
|
|
263
|
+
# Verify chunk was marked as received
|
|
264
|
+
assert chunk_number in handler._temp_dirs[filename]["received_chunks"]
|
|
265
|
+
|
|
266
|
+
# Clean up
|
|
267
|
+
del handler._temp_dirs[filename]
|
|
@@ -7,9 +7,9 @@ from openai.types.chat import ChatCompletionMessageParam
|
|
|
7
7
|
from mito_ai.utils.message_history_utils import trim_sections_from_message_content, trim_old_messages
|
|
8
8
|
from mito_ai.completions.prompt_builders.chat_prompt import create_chat_prompt
|
|
9
9
|
from mito_ai.completions.prompt_builders.agent_execution_prompt import create_agent_execution_prompt
|
|
10
|
-
from mito_ai.completions.prompt_builders.agent_smart_debug_prompt import
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
from mito_ai.completions.prompt_builders.agent_smart_debug_prompt import create_agent_smart_debug_prompt
|
|
11
|
+
from unittest.mock import Mock, patch
|
|
12
|
+
from mito_ai.completions.message_history import GlobalMessageHistory, ChatThread
|
|
13
13
|
from mito_ai.completions.prompt_builders.smart_debug_prompt import create_error_prompt
|
|
14
14
|
from mito_ai.completions.prompt_builders.explain_code_prompt import create_explain_code_prompt
|
|
15
15
|
from mito_ai.completions.models import (
|
|
@@ -27,6 +27,9 @@ from mito_ai.completions.prompt_builders.prompt_constants import (
|
|
|
27
27
|
CONTENT_REMOVED_PLACEHOLDER,
|
|
28
28
|
)
|
|
29
29
|
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
30
33
|
# Standard test data for multiple tests
|
|
31
34
|
TEST_VARIABLES = ["'df': pd.DataFrame({'col1': [1, 2, 3], 'col2': [4, 5, 6]})"]
|
|
32
35
|
TEST_FILES = ["data.csv", "script.py"]
|
|
@@ -386,4 +389,54 @@ def test_trim_mixed_content_messages() -> None:
|
|
|
386
389
|
|
|
387
390
|
# Verify that the recent messages are untouched
|
|
388
391
|
assert trimmed_messages[1] == message_list[1]
|
|
389
|
-
assert trimmed_messages[2] == message_list[2]
|
|
392
|
+
assert trimmed_messages[2] == message_list[2]
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def test_get_display_history_calls_update_last_interaction() -> None:
|
|
396
|
+
"""Test that get_display_history calls _update_last_interaction when retrieving a thread."""
|
|
397
|
+
|
|
398
|
+
# Create a mock thread
|
|
399
|
+
thread_id = ThreadID("test-thread-id")
|
|
400
|
+
mock_thread = Mock(spec=ChatThread)
|
|
401
|
+
mock_thread.display_history = [{"role": "user", "content": "test message"}]
|
|
402
|
+
mock_thread.last_interaction_ts = 1234567890.0
|
|
403
|
+
|
|
404
|
+
# Create message history instance and add the mock thread
|
|
405
|
+
message_history = GlobalMessageHistory()
|
|
406
|
+
message_history._chat_threads = {thread_id: mock_thread}
|
|
407
|
+
|
|
408
|
+
# Mock the _update_last_interaction method
|
|
409
|
+
with patch.object(message_history, '_update_last_interaction') as mock_update:
|
|
410
|
+
with patch.object(message_history, '_save_thread_to_disk') as mock_save:
|
|
411
|
+
# Call get_display_history
|
|
412
|
+
result = message_history.get_display_history(thread_id)
|
|
413
|
+
|
|
414
|
+
# Verify _update_last_interaction was called with the thread
|
|
415
|
+
mock_update.assert_called_once_with(mock_thread)
|
|
416
|
+
|
|
417
|
+
# Verify _save_thread_to_disk was also called
|
|
418
|
+
mock_save.assert_called_once_with(mock_thread)
|
|
419
|
+
|
|
420
|
+
# Verify the result is correct
|
|
421
|
+
assert result == [{"role": "user", "content": "test message"}]
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def test_get_display_history_returns_empty_for_nonexistent_thread() -> None:
|
|
425
|
+
"""Test that get_display_history returns empty list for non-existent thread."""
|
|
426
|
+
from mito_ai.completions.message_history import GlobalMessageHistory
|
|
427
|
+
from mito_ai.completions.models import ThreadID
|
|
428
|
+
|
|
429
|
+
message_history = GlobalMessageHistory()
|
|
430
|
+
thread_id = ThreadID("nonexistent-thread-id")
|
|
431
|
+
|
|
432
|
+
# Mock the methods to ensure they're not called
|
|
433
|
+
with patch.object(message_history, '_update_last_interaction') as mock_update:
|
|
434
|
+
with patch.object(message_history, '_save_thread_to_disk') as mock_save:
|
|
435
|
+
result = message_history.get_display_history(thread_id)
|
|
436
|
+
|
|
437
|
+
# Verify methods were not called since thread doesn't exist
|
|
438
|
+
mock_update.assert_not_called()
|
|
439
|
+
mock_save.assert_not_called()
|
|
440
|
+
|
|
441
|
+
# Verify empty result
|
|
442
|
+
assert result == []
|
|
@@ -11,6 +11,7 @@ from tornado.httpclient import HTTPResponse
|
|
|
11
11
|
from mito_ai.constants import MITO_GEMINI_URL
|
|
12
12
|
from mito_ai.utils.utils import _create_http_client
|
|
13
13
|
|
|
14
|
+
MITO_ERROR_MARKER = "MITO_ERROR_MARKER:"
|
|
14
15
|
|
|
15
16
|
class ProviderCompletionException(Exception):
|
|
16
17
|
"""Custom exception for Mito server errors that converts well to CompletionError."""
|
|
@@ -179,6 +180,12 @@ async def stream_response_from_mito_server(
|
|
|
179
180
|
if chunk_processor:
|
|
180
181
|
processed_chunk = chunk_processor(chunk)
|
|
181
182
|
|
|
183
|
+
# Check if this chunk contains an error marker
|
|
184
|
+
if processed_chunk.startswith(MITO_ERROR_MARKER):
|
|
185
|
+
error_message = processed_chunk[len(MITO_ERROR_MARKER):]
|
|
186
|
+
print(f"Detected error in {provider_name} stream: {error_message}")
|
|
187
|
+
raise ProviderCompletionException(error_message, provider_name=provider_name)
|
|
188
|
+
|
|
182
189
|
if reply_fn is not None and message_id is not None:
|
|
183
190
|
# Send the chunk directly to the frontend
|
|
184
191
|
reply_fn(CompletionStreamChunk(
|
mito_ai/utils/server_limits.py
CHANGED
|
@@ -28,7 +28,7 @@ free tier, but running AI models is expensive, so we need to limit the usage
|
|
|
28
28
|
or we will no longer be able to provide this free tier.
|
|
29
29
|
"""
|
|
30
30
|
# Monthly chat completions limit for free tier users
|
|
31
|
-
OS_MONTHLY_AI_COMPLETIONS_LIMIT: Final[int] =
|
|
31
|
+
OS_MONTHLY_AI_COMPLETIONS_LIMIT: Final[int] = 150
|
|
32
32
|
|
|
33
33
|
# Monthly autocomplete limit for free tier users
|
|
34
34
|
OS_MONTHLY_AUTOCOMPLETE_LIMIT: Final[int] = 5000
|
mito_ai/utils/telemetry_utils.py
CHANGED
|
@@ -337,15 +337,6 @@ def log_db_connection_success(connection_type: str, schema: Dict[str, Any]) -> N
|
|
|
337
337
|
},
|
|
338
338
|
)
|
|
339
339
|
|
|
340
|
-
def log_ai_completion_retry(key_type: Literal['mito_server_key', 'user_key'], message_type: MessageType, error: BaseException) -> None:
|
|
341
|
-
log(MITO_AI_COMPLETION_RETRY, params={KEY_TYPE_PARAM: key_type, "message_type": message_type}, key_type=key_type, error=error)
|
|
342
|
-
|
|
343
|
-
def log_ai_completion_error(key_type: Literal['mito_server_key', 'user_key'], message_type: MessageType, error: BaseException) -> None:
|
|
344
|
-
log(MITO_AI_COMPLETION_ERROR, params={KEY_TYPE_PARAM: key_type, "message_type": message_type}, key_type=key_type, error=error)
|
|
345
|
-
|
|
346
|
-
def log_mito_server_free_tier_limit_reached(key_type: Literal['mito_server_key', 'user_key'], message_type: MessageType) -> None:
|
|
347
|
-
log(MITO_SERVER_FREE_TIER_LIMIT_REACHED, params={KEY_TYPE_PARAM: key_type, "message_type": message_type}, key_type=key_type)
|
|
348
|
-
|
|
349
340
|
def log_db_connection_error(connection_type: str, error_message: str) -> None:
|
|
350
341
|
log(
|
|
351
342
|
"mito_ai_db_connection_error",
|
|
@@ -354,7 +345,33 @@ def log_db_connection_error(connection_type: str, error_message: str) -> None:
|
|
|
354
345
|
"error_message": error_message,
|
|
355
346
|
}
|
|
356
347
|
)
|
|
348
|
+
|
|
349
|
+
def log_file_upload_attempt(
|
|
350
|
+
filename: str, file_extension: str, is_chunked: bool, total_chunks: int
|
|
351
|
+
) -> None:
|
|
352
|
+
log(
|
|
353
|
+
"mito_ai_file_upload_attempt",
|
|
354
|
+
params={
|
|
355
|
+
"filename": filename,
|
|
356
|
+
"file_extension": file_extension,
|
|
357
|
+
"is_chunked": is_chunked,
|
|
358
|
+
"total_chunks": total_chunks,
|
|
359
|
+
},
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
def log_file_upload_failure(error: str) -> None:
|
|
363
|
+
log("mito_ai_file_upload_failure", params={"error_message": error})
|
|
364
|
+
|
|
365
|
+
def log_ai_completion_retry(key_type: Literal['mito_server_key', 'user_key'], message_type: MessageType, error: BaseException) -> None:
|
|
366
|
+
log(MITO_AI_COMPLETION_RETRY, params={KEY_TYPE_PARAM: key_type, "message_type": message_type}, key_type=key_type, error=error)
|
|
357
367
|
|
|
368
|
+
def log_ai_completion_error(key_type: Literal['mito_server_key', 'user_key'], message_type: MessageType, error: BaseException) -> None:
|
|
369
|
+
log(MITO_AI_COMPLETION_ERROR, params={KEY_TYPE_PARAM: key_type, "message_type": message_type}, key_type=key_type, error=error)
|
|
370
|
+
|
|
371
|
+
def log_mito_server_free_tier_limit_reached(key_type: Literal['mito_server_key', 'user_key'], message_type: MessageType) -> None:
|
|
372
|
+
log(MITO_SERVER_FREE_TIER_LIMIT_REACHED, params={KEY_TYPE_PARAM: key_type, "message_type": message_type}, key_type=key_type)
|
|
373
|
+
|
|
374
|
+
|
|
358
375
|
#################################
|
|
359
376
|
# Streamlit Conversion
|
|
360
377
|
#################################
|