mito-ai 0.1.33__py3-none-any.whl → 0.1.49__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.
Files changed (146) hide show
  1. mito_ai/__init__.py +49 -9
  2. mito_ai/_version.py +1 -1
  3. mito_ai/anthropic_client.py +142 -67
  4. mito_ai/{app_builder → app_deploy}/__init__.py +1 -1
  5. mito_ai/app_deploy/app_deploy_utils.py +44 -0
  6. mito_ai/app_deploy/handlers.py +345 -0
  7. mito_ai/{app_builder → app_deploy}/models.py +35 -22
  8. mito_ai/app_manager/__init__.py +4 -0
  9. mito_ai/app_manager/handlers.py +167 -0
  10. mito_ai/app_manager/models.py +71 -0
  11. mito_ai/app_manager/utils.py +24 -0
  12. mito_ai/auth/README.md +18 -0
  13. mito_ai/auth/__init__.py +6 -0
  14. mito_ai/auth/handlers.py +96 -0
  15. mito_ai/auth/urls.py +13 -0
  16. mito_ai/chat_history/handlers.py +63 -0
  17. mito_ai/chat_history/urls.py +32 -0
  18. mito_ai/completions/completion_handlers/agent_execution_handler.py +1 -1
  19. mito_ai/completions/completion_handlers/chat_completion_handler.py +4 -4
  20. mito_ai/completions/completion_handlers/utils.py +99 -37
  21. mito_ai/completions/handlers.py +57 -20
  22. mito_ai/completions/message_history.py +9 -1
  23. mito_ai/completions/models.py +31 -7
  24. mito_ai/completions/prompt_builders/agent_execution_prompt.py +21 -2
  25. mito_ai/completions/prompt_builders/agent_smart_debug_prompt.py +8 -0
  26. mito_ai/completions/prompt_builders/agent_system_message.py +115 -42
  27. mito_ai/completions/prompt_builders/chat_name_prompt.py +6 -6
  28. mito_ai/completions/prompt_builders/chat_prompt.py +18 -11
  29. mito_ai/completions/prompt_builders/chat_system_message.py +4 -0
  30. mito_ai/completions/prompt_builders/prompt_constants.py +23 -4
  31. mito_ai/completions/prompt_builders/utils.py +72 -10
  32. mito_ai/completions/providers.py +81 -47
  33. mito_ai/constants.py +25 -24
  34. mito_ai/file_uploads/__init__.py +3 -0
  35. mito_ai/file_uploads/handlers.py +248 -0
  36. mito_ai/file_uploads/urls.py +21 -0
  37. mito_ai/gemini_client.py +44 -48
  38. mito_ai/log/handlers.py +10 -3
  39. mito_ai/log/urls.py +3 -3
  40. mito_ai/openai_client.py +30 -44
  41. mito_ai/path_utils.py +70 -0
  42. mito_ai/streamlit_conversion/agent_utils.py +37 -0
  43. mito_ai/streamlit_conversion/prompts/prompt_constants.py +172 -0
  44. mito_ai/streamlit_conversion/prompts/prompt_utils.py +10 -0
  45. mito_ai/streamlit_conversion/prompts/streamlit_app_creation_prompt.py +46 -0
  46. mito_ai/streamlit_conversion/prompts/streamlit_error_correction_prompt.py +28 -0
  47. mito_ai/streamlit_conversion/prompts/streamlit_finish_todo_prompt.py +45 -0
  48. mito_ai/streamlit_conversion/prompts/streamlit_system_prompt.py +56 -0
  49. mito_ai/streamlit_conversion/prompts/update_existing_app_prompt.py +50 -0
  50. mito_ai/streamlit_conversion/search_replace_utils.py +94 -0
  51. mito_ai/streamlit_conversion/streamlit_agent_handler.py +144 -0
  52. mito_ai/streamlit_conversion/streamlit_utils.py +85 -0
  53. mito_ai/streamlit_conversion/validate_streamlit_app.py +105 -0
  54. mito_ai/streamlit_preview/__init__.py +6 -0
  55. mito_ai/streamlit_preview/handlers.py +111 -0
  56. mito_ai/streamlit_preview/manager.py +152 -0
  57. mito_ai/streamlit_preview/urls.py +22 -0
  58. mito_ai/streamlit_preview/utils.py +29 -0
  59. mito_ai/tests/chat_history/test_chat_history.py +211 -0
  60. mito_ai/tests/completions/completion_handlers_utils_test.py +190 -0
  61. mito_ai/tests/deploy_app/test_app_deploy_utils.py +89 -0
  62. mito_ai/tests/file_uploads/__init__.py +2 -0
  63. mito_ai/tests/file_uploads/test_handlers.py +282 -0
  64. mito_ai/tests/message_history/test_generate_short_chat_name.py +0 -4
  65. mito_ai/tests/message_history/test_message_history_utils.py +103 -23
  66. mito_ai/tests/open_ai_utils_test.py +18 -22
  67. mito_ai/tests/providers/test_anthropic_client.py +447 -0
  68. mito_ai/tests/providers/test_azure.py +2 -6
  69. mito_ai/tests/providers/test_capabilities.py +120 -0
  70. mito_ai/tests/{test_gemini_client.py → providers/test_gemini_client.py} +40 -36
  71. mito_ai/tests/providers/test_mito_server_utils.py +448 -0
  72. mito_ai/tests/providers/test_model_resolution.py +130 -0
  73. mito_ai/tests/providers/test_openai_client.py +57 -0
  74. mito_ai/tests/providers/test_provider_completion_exception.py +66 -0
  75. mito_ai/tests/providers/test_provider_limits.py +42 -0
  76. mito_ai/tests/providers/test_providers.py +382 -0
  77. mito_ai/tests/providers/test_retry_logic.py +389 -0
  78. mito_ai/tests/providers/test_stream_mito_server_utils.py +140 -0
  79. mito_ai/tests/providers/utils.py +85 -0
  80. mito_ai/tests/streamlit_conversion/__init__.py +3 -0
  81. mito_ai/tests/streamlit_conversion/test_apply_search_replace.py +240 -0
  82. mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +246 -0
  83. mito_ai/tests/streamlit_conversion/test_streamlit_utils.py +193 -0
  84. mito_ai/tests/streamlit_conversion/test_validate_streamlit_app.py +112 -0
  85. mito_ai/tests/streamlit_preview/test_streamlit_preview_handler.py +118 -0
  86. mito_ai/tests/streamlit_preview/test_streamlit_preview_manager.py +292 -0
  87. mito_ai/tests/test_constants.py +31 -3
  88. mito_ai/tests/test_telemetry.py +12 -0
  89. mito_ai/tests/user/__init__.py +2 -0
  90. mito_ai/tests/user/test_user.py +120 -0
  91. mito_ai/tests/utils/test_anthropic_utils.py +6 -6
  92. mito_ai/user/handlers.py +45 -0
  93. mito_ai/user/urls.py +21 -0
  94. mito_ai/utils/anthropic_utils.py +55 -121
  95. mito_ai/utils/create.py +17 -1
  96. mito_ai/utils/error_classes.py +42 -0
  97. mito_ai/utils/gemini_utils.py +39 -94
  98. mito_ai/utils/message_history_utils.py +7 -4
  99. mito_ai/utils/mito_server_utils.py +242 -0
  100. mito_ai/utils/open_ai_utils.py +38 -155
  101. mito_ai/utils/provider_utils.py +49 -0
  102. mito_ai/utils/server_limits.py +1 -1
  103. mito_ai/utils/telemetry_utils.py +137 -5
  104. {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +102 -100
  105. {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/package.json +4 -2
  106. {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/package.json.orig +3 -1
  107. {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/toolbar-buttons.json +2 -2
  108. mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.281f4b9af60d620c6fb1.js → mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.8f1845da6bf2b128c049.js +15948 -8403
  109. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.8f1845da6bf2b128c049.js.map +1 -0
  110. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js +198 -0
  111. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js.map +1 -0
  112. mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.4f1d00fd0c58fcc05d8d.js → mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.8b24b5b3b93f95205b56.js +58 -33
  113. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.8b24b5b3b93f95205b56.js.map +1 -0
  114. mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.06083e515de4862df010.js → mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js +10 -2
  115. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js.map +1 -0
  116. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_apis_signOut_mjs-node_module-75790d.688c25857e7b81b1740f.js +533 -0
  117. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_apis_signOut_mjs-node_module-75790d.688c25857e7b81b1740f.js.map +1 -0
  118. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_tokenProvider_tokenProvider_-72f1c8.a917210f057fcfe224ad.js +6941 -0
  119. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_tokenProvider_tokenProvider_-72f1c8.a917210f057fcfe224ad.js.map +1 -0
  120. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js +1021 -0
  121. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js.map +1 -0
  122. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js +59698 -0
  123. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js.map +1 -0
  124. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_react-dom_client_js-node_modules_aws-amplify_ui-react_dist_styles_css.b43d4249e4d3dac9ad7b.js +7440 -0
  125. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_react-dom_client_js-node_modules_aws-amplify_ui-react_dist_styles_css.b43d4249e4d3dac9ad7b.js.map +1 -0
  126. mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.9795f79265ddb416864b.js → mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js +2 -240
  127. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js.map +1 -0
  128. {mito_ai-0.1.33.dist-info → mito_ai-0.1.49.dist-info}/METADATA +5 -2
  129. mito_ai-0.1.49.dist-info/RECORD +205 -0
  130. mito_ai/app_builder/handlers.py +0 -218
  131. mito_ai/tests/providers_test.py +0 -438
  132. mito_ai/tests/test_anthropic_client.py +0 -270
  133. mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.281f4b9af60d620c6fb1.js.map +0 -1
  134. mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.4f1d00fd0c58fcc05d8d.js.map +0 -1
  135. mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.06083e515de4862df010.js.map +0 -1
  136. mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_html2canvas_dist_html2canvas_js.ea47e8c8c906197f8d19.js +0 -7842
  137. mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_html2canvas_dist_html2canvas_js.ea47e8c8c906197f8d19.js.map +0 -1
  138. mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.9795f79265ddb416864b.js.map +0 -1
  139. mito_ai-0.1.33.dist-info/RECORD +0 -134
  140. {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
  141. {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
  142. {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js +0 -0
  143. {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js.map +0 -0
  144. {mito_ai-0.1.33.dist-info → mito_ai-0.1.49.dist-info}/WHEEL +0 -0
  145. {mito_ai-0.1.33.dist-info → mito_ai-0.1.49.dist-info}/entry_points.txt +0 -0
  146. {mito_ai-0.1.33.dist-info → mito_ai-0.1.49.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,89 @@
1
+ # Copyright (c) Saga Inc.
2
+ # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
+
4
+ import zipfile
5
+ import logging
6
+ from mito_ai.app_deploy.app_deploy_utils import add_files_to_zip
7
+ from mito_ai.path_utils import AbsoluteNotebookDirPath
8
+
9
+ class TestAddFilesToZip:
10
+ """Test cases for add_files_to_zip helper function"""
11
+
12
+ def test_files_added_correctly(self, tmp_path):
13
+ """Ensure individual files are added correctly to the zip"""
14
+ # Create files
15
+ f1 = tmp_path / "file1.txt"
16
+ f1.write_text("file1 content")
17
+ f2 = tmp_path / "file2.txt"
18
+ f2.write_text("file2 content")
19
+
20
+ zip_path = tmp_path / "test.zip"
21
+ add_files_to_zip(str(zip_path), AbsoluteNotebookDirPath(str(tmp_path)), ["file1.txt", "file2.txt"], 'test-app-file-name.py')
22
+
23
+ with zipfile.ZipFile(zip_path, "r") as zf:
24
+ names = zf.namelist()
25
+ assert "file1.txt" in names
26
+ assert "file2.txt" in names
27
+ assert len(names) == 2
28
+
29
+ def test_renames_app_file(self, tmp_path):
30
+ """Ensure individual files are added correctly to the zip"""
31
+ # Create files
32
+ f1 = tmp_path / "original-file-name.py"
33
+ f1.write_text("file1 content")
34
+ f2 = tmp_path / "file2.txt"
35
+ f2.write_text("file2 content")
36
+
37
+ zip_path = tmp_path / "test.zip"
38
+ add_files_to_zip(str(zip_path), AbsoluteNotebookDirPath(str(tmp_path)), ["original-file-name.py", "file2.txt"], 'original-file-name.py')
39
+
40
+ with zipfile.ZipFile(zip_path, "r") as zf:
41
+ names = zf.namelist()
42
+ assert "app.py" in names
43
+ assert "file2.txt" in names
44
+ assert len(names) == 2
45
+
46
+ def test_directories_added_recursively(self, tmp_path):
47
+ """Ensure directories are added recursively with correct relative paths"""
48
+ nested = tmp_path / "folder"
49
+ nested.mkdir()
50
+ (nested / "nested1.txt").write_text("nested1 content")
51
+ subfolder = nested / "sub"
52
+ subfolder.mkdir()
53
+ (subfolder / "nested2.txt").write_text("nested2 content")
54
+
55
+ zip_path = tmp_path / "test.zip"
56
+ add_files_to_zip(str(zip_path), AbsoluteNotebookDirPath(str(tmp_path)), ["folder"], 'test-app.py')
57
+
58
+ with zipfile.ZipFile(zip_path, "r") as zf:
59
+ names = zf.namelist()
60
+ assert "folder/nested1.txt" in names
61
+ assert "folder/sub/nested2.txt" in names
62
+
63
+ def test_missing_files_skipped(self, tmp_path, caplog):
64
+ """Ensure missing files do not break the function and warning is logged"""
65
+ caplog.set_level(logging.WARNING)
66
+ zip_path = tmp_path / "test.zip"
67
+ add_files_to_zip(str(zip_path), AbsoluteNotebookDirPath(str(tmp_path)), ["does_not_exist.txt"], 'test-app.py', logger=logging.getLogger())
68
+
69
+ # Zip should exist but be empty
70
+ with zipfile.ZipFile(zip_path, "r") as zf:
71
+ assert zf.namelist() == []
72
+
73
+ # Check warning was logged
74
+ assert any("Skipping missing file" in record.message for record in caplog.records)
75
+
76
+ def test_arcname_paths_correct(self, tmp_path):
77
+ """Ensure arcname paths inside zip preserve relative paths to base_path"""
78
+ (tmp_path / "file.txt").write_text("content")
79
+ folder = tmp_path / "folder"
80
+ folder.mkdir()
81
+ (folder / "nested.txt").write_text("nested content")
82
+
83
+ zip_path = tmp_path / "test.zip"
84
+ add_files_to_zip(str(zip_path), AbsoluteNotebookDirPath(str(tmp_path)), ["file.txt", "folder"], 'test-app.py')
85
+
86
+ with zipfile.ZipFile(zip_path, "r") as zf:
87
+ names = zf.namelist()
88
+ assert "file.txt" in names
89
+ assert "folder/nested.txt" in names
@@ -0,0 +1,2 @@
1
+ # Copyright (c) Saga Inc.
2
+ # Distributed under the terms of the GNU Affero General Public License v3.0 License.
@@ -0,0 +1,282 @@
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]
268
+
269
+
270
+ def test_image_size_limit_exceeded(handler, temp_dir):
271
+ """Test that image uploads exceeding 3MB are rejected."""
272
+ filename = "large_image.jpg"
273
+ # Create 5MB of data (5 * 1024 * 1024 bytes)
274
+ file_data = b"x" * (5 * 1024 * 1024)
275
+ notebook_dir = temp_dir
276
+
277
+ # The _handle_regular_upload should raise a ValueError for oversized images
278
+ with pytest.raises(ValueError) as exc_info:
279
+ handler._handle_regular_upload(filename, file_data, notebook_dir)
280
+
281
+ # Verify the error message mentions the size limit
282
+ assert "exceeded 3MB limit" in str(exc_info.value)
@@ -6,10 +6,6 @@ from unittest.mock import AsyncMock, MagicMock, patch
6
6
  from traitlets.config import Config
7
7
  from mito_ai.completions.message_history import generate_short_chat_name
8
8
  from mito_ai.completions.providers import OpenAIProvider
9
- from mito_ai.completions.models import MessageType
10
- from mito_ai.openai_client import OPENAI_FAST_MODEL
11
- from mito_ai.anthropic_client import ANTHROPIC_FAST_MODEL
12
- from mito_ai.gemini_client import GEMINI_FAST_MODEL
13
9
 
14
10
 
15
11
  @pytest.fixture
@@ -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
- create_agent_smart_debug_prompt,
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"]
@@ -103,12 +106,15 @@ PROMPT_BUILDER_TEST_CASES = [
103
106
  AgentExecutionMetadata(
104
107
  variables=TEST_VARIABLES,
105
108
  files=TEST_FILES,
109
+ notebookPath='/test-notebook-path.ipynb',
110
+ notebookID='test-notebook-id',
106
111
  aiOptimizedCells=[
107
112
  AIOptimizedCell(cell_type="code", id="cell1", code=TEST_CODE)
108
113
  ],
109
114
  input=TEST_INPUT,
110
115
  promptType="agent:execution",
111
116
  threadId=ThreadID("test-thread-id"),
117
+ activeCellId="cell1",
112
118
  isChromeBrowser=True
113
119
  )
114
120
  ),
@@ -230,16 +236,17 @@ file1.csv
230
236
  file2.txt
231
237
  """
232
238
 
233
- # Create test messages with proper typing
239
+ # Create test messages with proper typing
234
240
  messages: List[ChatCompletionMessageParam] = [
235
241
  {"role": "system", "content": system_message_with_sections},
236
242
  {"role": "user", "content": user_message_with_sections},
237
243
  {"role": "assistant", "content": assistant_message_with_sections},
238
- {"role": "user", "content": "Recent user message"},
244
+ {"role": "user", "content": "Recent user message 1"},
245
+ {"role": "user", "content": "Recent user message 2"},
246
+ {"role": "user", "content": "Recent user message 3"},
239
247
  ]
240
248
 
241
- # Keep only the most recent message
242
- result = trim_old_messages(messages, keep_recent=1)
249
+ result = trim_old_messages(messages)
243
250
 
244
251
  # System message should remain unchanged even though it's old
245
252
  system_content = result[0].get("content")
@@ -262,14 +269,22 @@ file2.txt
262
269
  assert FILES_SECTION_HEADING in assistant_content
263
270
  assert "file1.csv" in assistant_content
264
271
 
265
- # Recent user message should remain unchanged
266
- recent_content = result[3].get("content")
267
- assert isinstance(recent_content, str)
268
- assert recent_content == "Recent user message"
272
+ # Recent user messages should remain unchanged
273
+ recent_content_1 = result[3].get("content")
274
+ assert isinstance(recent_content_1, str)
275
+ assert recent_content_1 == "Recent user message 1"
276
+
277
+ recent_content_2 = result[4].get("content")
278
+ assert isinstance(recent_content_2, str)
279
+ assert recent_content_2 == "Recent user message 2"
280
+
281
+ recent_content_3 = result[5].get("content")
282
+ assert isinstance(recent_content_3, str)
283
+ assert recent_content_3 == "Recent user message 3"
269
284
 
270
285
 
271
286
  def test_trim_old_messages_preserves_recent_messages() -> None:
272
- """Test that trim_old_messages preserves the most recent messages based on keep_recent parameter."""
287
+ """Test that trim_old_messages preserves the most recent messages based on MESSAGE_HISTORY_TRIM_THRESHOLD."""
273
288
  # Create test messages
274
289
  old_message_1 = f"""Old message 1.
275
290
  {FILES_SECTION_HEADING}
@@ -286,6 +301,10 @@ file3.csv
286
301
  recent_message_2 = f"""Recent message 2.
287
302
  {FILES_SECTION_HEADING}
288
303
  file4.csv
304
+ """
305
+ recent_message_3 = f"""Recent message 3.
306
+ {FILES_SECTION_HEADING}
307
+ file5.csv
289
308
  """
290
309
 
291
310
  # Create test messages with proper typing
@@ -294,10 +313,11 @@ file4.csv
294
313
  {"role": "user", "content": old_message_2},
295
314
  {"role": "user", "content": recent_message_1},
296
315
  {"role": "user", "content": recent_message_2},
316
+ {"role": "user", "content": recent_message_3},
297
317
  ]
298
318
 
299
- # Keep the 2 most recent messages
300
- result = trim_old_messages(messages, keep_recent=2)
319
+ # Test with MESSAGE_HISTORY_TRIM_THRESHOLD (3) - only the first 2 messages should be trimmed
320
+ result = trim_old_messages(messages)
301
321
 
302
322
  # Old messages should be trimmed
303
323
  old_content_1 = result[0].get("content")
@@ -322,24 +342,30 @@ file4.csv
322
342
  assert recent_content_2 == recent_message_2
323
343
  assert FILES_SECTION_HEADING in recent_content_2
324
344
  assert "file4.csv" in recent_content_2
345
+
346
+ recent_content_3 = result[4].get("content")
347
+ assert isinstance(recent_content_3, str)
348
+ assert recent_content_3 == recent_message_3
349
+ assert FILES_SECTION_HEADING in recent_content_3
350
+ assert "file5.csv" in recent_content_3
325
351
 
326
352
  def test_trim_old_messages_empty_list() -> None:
327
353
  """Test that trim_old_messages handles empty message lists correctly."""
328
354
  messages: List[ChatCompletionMessageParam] = []
329
- result = trim_old_messages(messages, keep_recent=2)
355
+ result = trim_old_messages(messages)
330
356
  assert result == []
331
357
 
332
358
 
333
- def test_trim_old_messages_fewer_than_keep_recent() -> None:
334
- """Test that trim_old_messages doesn't modify messages if there are fewer than keep_recent."""
359
+ def test_trim_old_messages_fewer_than_threshold() -> None:
360
+ """Test that trim_old_messages doesn't modify messages if there are fewer than MESSAGE_HISTORY_TRIM_THRESHOLD."""
335
361
  messages: List[ChatCompletionMessageParam] = [
336
362
  {"role": "user", "content": "User message 1"},
337
363
  {"role": "assistant", "content": "Assistant message 1"},
338
364
  ]
339
365
 
340
- result = trim_old_messages(messages, keep_recent=3)
366
+ result = trim_old_messages(messages)
341
367
 
342
- # Messages should remain unchanged
368
+ # Messages should remain unchanged since we have fewer than MESSAGE_HISTORY_TRIM_THRESHOLD (3) messages
343
369
  user_content = result[0].get("content")
344
370
  assert isinstance(user_content, str)
345
371
  assert user_content == "User message 1"
@@ -370,15 +396,17 @@ def test_trim_mixed_content_messages() -> None:
370
396
  })
371
397
 
372
398
  # Create sample message list with one old message (the mixed content)
373
- # and one recent message (to not be trimmed)
399
+ # and enough recent messages to exceed MESSAGE_HISTORY_TRIM_THRESHOLD (3)
374
400
  message_list: List[ChatCompletionMessageParam] = [
375
401
  mixed_content_message, # This should get trimmed
376
402
  {"role": "assistant", "content": "That's a chart showing data trends"},
377
- {"role": "user", "content": "Can you explain more?"} # Recent message, should not be trimmed
403
+ {"role": "user", "content": "Can you explain more?"}, # Recent message, should not be trimmed
404
+ {"role": "user", "content": "Another recent message"}, # Recent message, should not be trimmed
405
+ {"role": "user", "content": "Yet another recent message"} # Recent message, should not be trimmed
378
406
  ]
379
407
 
380
408
  # Apply the trimming function
381
- trimmed_messages = trim_old_messages(message_list, keep_recent=2)
409
+ trimmed_messages = trim_old_messages(message_list)
382
410
 
383
411
  # Verify that the first message has been trimmed properly
384
412
  assert trimmed_messages[0]["role"] == "user"
@@ -386,4 +414,56 @@ def test_trim_mixed_content_messages() -> None:
386
414
 
387
415
  # Verify that the recent messages are untouched
388
416
  assert trimmed_messages[1] == message_list[1]
389
- assert trimmed_messages[2] == message_list[2]
417
+ assert trimmed_messages[2] == message_list[2]
418
+ assert trimmed_messages[3] == message_list[3]
419
+ assert trimmed_messages[4] == message_list[4]
420
+
421
+
422
+ def test_get_display_history_calls_update_last_interaction() -> None:
423
+ """Test that get_display_history calls _update_last_interaction when retrieving a thread."""
424
+
425
+ # Create a mock thread
426
+ thread_id = ThreadID("test-thread-id")
427
+ mock_thread = Mock(spec=ChatThread)
428
+ mock_thread.display_history = [{"role": "user", "content": "test message"}]
429
+ mock_thread.last_interaction_ts = 1234567890.0
430
+
431
+ # Create message history instance and add the mock thread
432
+ message_history = GlobalMessageHistory()
433
+ message_history._chat_threads = {thread_id: mock_thread}
434
+
435
+ # Mock the _update_last_interaction method
436
+ with patch.object(message_history, '_update_last_interaction') as mock_update:
437
+ with patch.object(message_history, '_save_thread_to_disk') as mock_save:
438
+ # Call get_display_history
439
+ result = message_history.get_display_history(thread_id)
440
+
441
+ # Verify _update_last_interaction was called with the thread
442
+ mock_update.assert_called_once_with(mock_thread)
443
+
444
+ # Verify _save_thread_to_disk was also called
445
+ mock_save.assert_called_once_with(mock_thread)
446
+
447
+ # Verify the result is correct
448
+ assert result == [{"role": "user", "content": "test message"}]
449
+
450
+
451
+ def test_get_display_history_returns_empty_for_nonexistent_thread() -> None:
452
+ """Test that get_display_history returns empty list for non-existent thread."""
453
+ from mito_ai.completions.message_history import GlobalMessageHistory
454
+ from mito_ai.completions.models import ThreadID
455
+
456
+ message_history = GlobalMessageHistory()
457
+ thread_id = ThreadID("nonexistent-thread-id")
458
+
459
+ # Mock the methods to ensure they're not called
460
+ with patch.object(message_history, '_update_last_interaction') as mock_update:
461
+ with patch.object(message_history, '_save_thread_to_disk') as mock_save:
462
+ result = message_history.get_display_history(thread_id)
463
+
464
+ # Verify methods were not called since thread doesn't exist
465
+ mock_update.assert_not_called()
466
+ mock_save.assert_not_called()
467
+
468
+ # Verify empty result
469
+ assert result == []
@@ -80,28 +80,24 @@ def test_prepare_request_data_and_headers_basic() -> None:
80
80
  mock_get_user_field.side_effect = ["test@example.com", "user123"]
81
81
 
82
82
  # Mock the quota check
83
- with patch("mito_ai.utils.open_ai_utils.check_mito_server_quota") as mock_check_quota:
84
- data, headers = _prepare_request_data_and_headers(
85
- last_message_content="test message",
86
- ai_completion_data={"key": "value"},
87
- timeout=30,
88
- max_retries=3,
89
- message_type=MessageType.CHAT
90
- )
91
-
92
- # Verify quota check was called
93
- mock_check_quota.assert_called_once_with(MessageType.CHAT)
94
-
95
- # Verify data structure
96
- assert data["timeout"] == 30
97
- assert data["max_retries"] == 3
98
- assert data["email"] == "test@example.com"
99
- assert data["user_id"] == "user123"
100
- assert data["data"] == {"key": "value"}
101
- assert data["user_input"] == "test message"
102
-
103
- # Verify headers
104
- assert headers == {"Content-Type": "application/json"}
83
+ data, headers = _prepare_request_data_and_headers(
84
+ last_message_content="test message",
85
+ ai_completion_data={"key": "value"},
86
+ timeout=30,
87
+ max_retries=3,
88
+ message_type=MessageType.CHAT
89
+ )
90
+
91
+ # Verify data structure
92
+ assert data["timeout"] == 30
93
+ assert data["max_retries"] == 3
94
+ assert data["email"] == "test@example.com"
95
+ assert data["user_id"] == "user123"
96
+ assert data["data"] == {"key": "value"}
97
+ assert data["user_input"] == "test message"
98
+
99
+ # Verify headers
100
+ assert headers == {"Content-Type": "application/json"}
105
101
 
106
102
  def test_prepare_request_data_and_headers_null_message() -> None:
107
103
  """Test handling of null message content"""