mito-ai 0.1.50__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 (205) hide show
  1. mito_ai/__init__.py +114 -0
  2. mito_ai/_version.py +4 -0
  3. mito_ai/anthropic_client.py +334 -0
  4. mito_ai/app_deploy/__init__.py +6 -0
  5. mito_ai/app_deploy/app_deploy_utils.py +44 -0
  6. mito_ai/app_deploy/handlers.py +345 -0
  7. mito_ai/app_deploy/models.py +98 -0
  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/__init__.py +3 -0
  19. mito_ai/completions/completion_handlers/agent_auto_error_fixup_handler.py +59 -0
  20. mito_ai/completions/completion_handlers/agent_execution_handler.py +66 -0
  21. mito_ai/completions/completion_handlers/chat_completion_handler.py +141 -0
  22. mito_ai/completions/completion_handlers/code_explain_handler.py +113 -0
  23. mito_ai/completions/completion_handlers/completion_handler.py +42 -0
  24. mito_ai/completions/completion_handlers/inline_completer_handler.py +48 -0
  25. mito_ai/completions/completion_handlers/smart_debug_handler.py +160 -0
  26. mito_ai/completions/completion_handlers/utils.py +147 -0
  27. mito_ai/completions/handlers.py +415 -0
  28. mito_ai/completions/message_history.py +401 -0
  29. mito_ai/completions/models.py +404 -0
  30. mito_ai/completions/prompt_builders/__init__.py +3 -0
  31. mito_ai/completions/prompt_builders/agent_execution_prompt.py +57 -0
  32. mito_ai/completions/prompt_builders/agent_smart_debug_prompt.py +160 -0
  33. mito_ai/completions/prompt_builders/agent_system_message.py +472 -0
  34. mito_ai/completions/prompt_builders/chat_name_prompt.py +15 -0
  35. mito_ai/completions/prompt_builders/chat_prompt.py +116 -0
  36. mito_ai/completions/prompt_builders/chat_system_message.py +92 -0
  37. mito_ai/completions/prompt_builders/explain_code_prompt.py +32 -0
  38. mito_ai/completions/prompt_builders/inline_completer_prompt.py +197 -0
  39. mito_ai/completions/prompt_builders/prompt_constants.py +170 -0
  40. mito_ai/completions/prompt_builders/smart_debug_prompt.py +199 -0
  41. mito_ai/completions/prompt_builders/utils.py +84 -0
  42. mito_ai/completions/providers.py +284 -0
  43. mito_ai/constants.py +63 -0
  44. mito_ai/db/__init__.py +3 -0
  45. mito_ai/db/crawlers/__init__.py +6 -0
  46. mito_ai/db/crawlers/base_crawler.py +61 -0
  47. mito_ai/db/crawlers/constants.py +43 -0
  48. mito_ai/db/crawlers/snowflake.py +71 -0
  49. mito_ai/db/handlers.py +168 -0
  50. mito_ai/db/models.py +31 -0
  51. mito_ai/db/urls.py +34 -0
  52. mito_ai/db/utils.py +185 -0
  53. mito_ai/docker/mssql/compose.yml +37 -0
  54. mito_ai/docker/mssql/init/setup.sql +21 -0
  55. mito_ai/docker/mysql/compose.yml +18 -0
  56. mito_ai/docker/mysql/init/setup.sql +13 -0
  57. mito_ai/docker/oracle/compose.yml +17 -0
  58. mito_ai/docker/oracle/init/setup.sql +20 -0
  59. mito_ai/docker/postgres/compose.yml +17 -0
  60. mito_ai/docker/postgres/init/setup.sql +13 -0
  61. mito_ai/enterprise/__init__.py +3 -0
  62. mito_ai/enterprise/utils.py +15 -0
  63. mito_ai/file_uploads/__init__.py +3 -0
  64. mito_ai/file_uploads/handlers.py +248 -0
  65. mito_ai/file_uploads/urls.py +21 -0
  66. mito_ai/gemini_client.py +232 -0
  67. mito_ai/log/handlers.py +38 -0
  68. mito_ai/log/urls.py +21 -0
  69. mito_ai/logger.py +37 -0
  70. mito_ai/openai_client.py +382 -0
  71. mito_ai/path_utils.py +70 -0
  72. mito_ai/rules/handlers.py +44 -0
  73. mito_ai/rules/urls.py +22 -0
  74. mito_ai/rules/utils.py +56 -0
  75. mito_ai/settings/handlers.py +41 -0
  76. mito_ai/settings/urls.py +20 -0
  77. mito_ai/settings/utils.py +42 -0
  78. mito_ai/streamlit_conversion/agent_utils.py +37 -0
  79. mito_ai/streamlit_conversion/prompts/prompt_constants.py +172 -0
  80. mito_ai/streamlit_conversion/prompts/prompt_utils.py +10 -0
  81. mito_ai/streamlit_conversion/prompts/streamlit_app_creation_prompt.py +46 -0
  82. mito_ai/streamlit_conversion/prompts/streamlit_error_correction_prompt.py +28 -0
  83. mito_ai/streamlit_conversion/prompts/streamlit_finish_todo_prompt.py +45 -0
  84. mito_ai/streamlit_conversion/prompts/streamlit_system_prompt.py +56 -0
  85. mito_ai/streamlit_conversion/prompts/update_existing_app_prompt.py +50 -0
  86. mito_ai/streamlit_conversion/search_replace_utils.py +94 -0
  87. mito_ai/streamlit_conversion/streamlit_agent_handler.py +144 -0
  88. mito_ai/streamlit_conversion/streamlit_utils.py +85 -0
  89. mito_ai/streamlit_conversion/validate_streamlit_app.py +105 -0
  90. mito_ai/streamlit_preview/__init__.py +6 -0
  91. mito_ai/streamlit_preview/handlers.py +111 -0
  92. mito_ai/streamlit_preview/manager.py +152 -0
  93. mito_ai/streamlit_preview/urls.py +22 -0
  94. mito_ai/streamlit_preview/utils.py +29 -0
  95. mito_ai/tests/__init__.py +3 -0
  96. mito_ai/tests/chat_history/test_chat_history.py +211 -0
  97. mito_ai/tests/completions/completion_handlers_utils_test.py +190 -0
  98. mito_ai/tests/conftest.py +53 -0
  99. mito_ai/tests/create_agent_system_message_prompt_test.py +22 -0
  100. mito_ai/tests/data/prompt_lg.py +69 -0
  101. mito_ai/tests/data/prompt_sm.py +6 -0
  102. mito_ai/tests/data/prompt_xl.py +13 -0
  103. mito_ai/tests/data/stock_data.sqlite3 +0 -0
  104. mito_ai/tests/db/conftest.py +39 -0
  105. mito_ai/tests/db/connections_test.py +102 -0
  106. mito_ai/tests/db/mssql_test.py +29 -0
  107. mito_ai/tests/db/mysql_test.py +29 -0
  108. mito_ai/tests/db/oracle_test.py +29 -0
  109. mito_ai/tests/db/postgres_test.py +29 -0
  110. mito_ai/tests/db/schema_test.py +93 -0
  111. mito_ai/tests/db/sqlite_test.py +31 -0
  112. mito_ai/tests/db/test_db_constants.py +61 -0
  113. mito_ai/tests/deploy_app/test_app_deploy_utils.py +89 -0
  114. mito_ai/tests/file_uploads/__init__.py +2 -0
  115. mito_ai/tests/file_uploads/test_handlers.py +282 -0
  116. mito_ai/tests/message_history/test_generate_short_chat_name.py +120 -0
  117. mito_ai/tests/message_history/test_message_history_utils.py +469 -0
  118. mito_ai/tests/open_ai_utils_test.py +152 -0
  119. mito_ai/tests/performance_test.py +329 -0
  120. mito_ai/tests/providers/test_anthropic_client.py +447 -0
  121. mito_ai/tests/providers/test_azure.py +631 -0
  122. mito_ai/tests/providers/test_capabilities.py +120 -0
  123. mito_ai/tests/providers/test_gemini_client.py +195 -0
  124. mito_ai/tests/providers/test_mito_server_utils.py +448 -0
  125. mito_ai/tests/providers/test_model_resolution.py +130 -0
  126. mito_ai/tests/providers/test_openai_client.py +57 -0
  127. mito_ai/tests/providers/test_provider_completion_exception.py +66 -0
  128. mito_ai/tests/providers/test_provider_limits.py +42 -0
  129. mito_ai/tests/providers/test_providers.py +382 -0
  130. mito_ai/tests/providers/test_retry_logic.py +389 -0
  131. mito_ai/tests/providers/test_stream_mito_server_utils.py +140 -0
  132. mito_ai/tests/providers/utils.py +85 -0
  133. mito_ai/tests/rules/conftest.py +26 -0
  134. mito_ai/tests/rules/rules_test.py +117 -0
  135. mito_ai/tests/server_limits_test.py +406 -0
  136. mito_ai/tests/settings/conftest.py +26 -0
  137. mito_ai/tests/settings/settings_test.py +70 -0
  138. mito_ai/tests/settings/test_settings_constants.py +9 -0
  139. mito_ai/tests/streamlit_conversion/__init__.py +3 -0
  140. mito_ai/tests/streamlit_conversion/test_apply_search_replace.py +240 -0
  141. mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +246 -0
  142. mito_ai/tests/streamlit_conversion/test_streamlit_utils.py +193 -0
  143. mito_ai/tests/streamlit_conversion/test_validate_streamlit_app.py +112 -0
  144. mito_ai/tests/streamlit_preview/test_streamlit_preview_handler.py +118 -0
  145. mito_ai/tests/streamlit_preview/test_streamlit_preview_manager.py +292 -0
  146. mito_ai/tests/test_constants.py +47 -0
  147. mito_ai/tests/test_telemetry.py +12 -0
  148. mito_ai/tests/user/__init__.py +2 -0
  149. mito_ai/tests/user/test_user.py +120 -0
  150. mito_ai/tests/utils/__init__.py +3 -0
  151. mito_ai/tests/utils/test_anthropic_utils.py +162 -0
  152. mito_ai/tests/utils/test_gemini_utils.py +98 -0
  153. mito_ai/tests/version_check_test.py +169 -0
  154. mito_ai/user/handlers.py +45 -0
  155. mito_ai/user/urls.py +21 -0
  156. mito_ai/utils/__init__.py +3 -0
  157. mito_ai/utils/anthropic_utils.py +168 -0
  158. mito_ai/utils/create.py +94 -0
  159. mito_ai/utils/db.py +74 -0
  160. mito_ai/utils/error_classes.py +42 -0
  161. mito_ai/utils/gemini_utils.py +133 -0
  162. mito_ai/utils/message_history_utils.py +87 -0
  163. mito_ai/utils/mito_server_utils.py +242 -0
  164. mito_ai/utils/open_ai_utils.py +200 -0
  165. mito_ai/utils/provider_utils.py +49 -0
  166. mito_ai/utils/schema.py +86 -0
  167. mito_ai/utils/server_limits.py +152 -0
  168. mito_ai/utils/telemetry_utils.py +480 -0
  169. mito_ai/utils/utils.py +89 -0
  170. mito_ai/utils/version_utils.py +94 -0
  171. mito_ai/utils/websocket_base.py +88 -0
  172. mito_ai/version_check.py +60 -0
  173. mito_ai-0.1.50.data/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +7 -0
  174. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/build_log.json +728 -0
  175. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/package.json +243 -0
  176. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/package.json.orig +238 -0
  177. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/toolbar-buttons.json +37 -0
  178. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.8f1845da6bf2b128c049.js +21602 -0
  179. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.8f1845da6bf2b128c049.js.map +1 -0
  180. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js +198 -0
  181. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js.map +1 -0
  182. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.78d3ccb73e7ca1da3aae.js +619 -0
  183. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.78d3ccb73e7ca1da3aae.js.map +1 -0
  184. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/style.js +4 -0
  185. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js +712 -0
  186. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js.map +1 -0
  187. mito_ai-0.1.50.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
  188. mito_ai-0.1.50.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
  189. mito_ai-0.1.50.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
  190. mito_ai-0.1.50.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
  191. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js +1021 -0
  192. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js.map +1 -0
  193. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js +59698 -0
  194. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js.map +1 -0
  195. mito_ai-0.1.50.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
  196. mito_ai-0.1.50.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
  197. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js +2792 -0
  198. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js.map +1 -0
  199. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js +4859 -0
  200. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js.map +1 -0
  201. mito_ai-0.1.50.dist-info/METADATA +221 -0
  202. mito_ai-0.1.50.dist-info/RECORD +205 -0
  203. mito_ai-0.1.50.dist-info/WHEEL +4 -0
  204. mito_ai-0.1.50.dist-info/entry_points.txt +2 -0
  205. mito_ai-0.1.50.dist-info/licenses/LICENSE +3 -0
@@ -0,0 +1,120 @@
1
+ # Copyright (c) Saga Inc.
2
+ # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
+
4
+ import json
5
+ import os
6
+ import requests
7
+ import tempfile
8
+ from unittest.mock import patch
9
+ import pytest
10
+ from mito_ai.tests.conftest import TOKEN
11
+
12
+
13
+ @pytest.fixture
14
+ def mock_user_json():
15
+ """Fixture that creates a temporary user.json file with test data"""
16
+ with tempfile.TemporaryDirectory() as temp_dir:
17
+ # Create the .mito directory
18
+ mito_dir = os.path.join(temp_dir, ".mito")
19
+ os.makedirs(mito_dir, exist_ok=True)
20
+
21
+ # Create a user.json file with test data
22
+ user_json_path = os.path.join(mito_dir, "user.json")
23
+ user_data = {
24
+ "user_email": "test@mail.com",
25
+ "static_user_id": "test_user_123",
26
+ }
27
+
28
+ with open(user_json_path, "w") as f:
29
+ json.dump(user_data, f)
30
+
31
+ yield user_json_path
32
+
33
+
34
+ # --- GET USER KEY ---
35
+
36
+
37
+ def test_get_user_with_mocked_data_success(
38
+ jp_base_url: str, mock_user_json: str
39
+ ) -> None:
40
+ """Test successful GET user endpoint with mocked data"""
41
+ with patch("mito_ai.utils.db.USER_JSON_PATH", mock_user_json):
42
+ response = requests.get(
43
+ jp_base_url + f"/mito-ai/user/user_email",
44
+ headers={"Authorization": f"token {TOKEN}"},
45
+ )
46
+ assert response.status_code == 200
47
+
48
+ response_json = response.json()
49
+ assert response_json["key"] == "user_email"
50
+ assert response_json["value"] == "test@mail.com"
51
+
52
+
53
+ def test_get_user_with_mocked_data_not_found(
54
+ jp_base_url: str, mock_user_json: str
55
+ ) -> None:
56
+ """Test GET user endpoint with mocked data for non-existent key"""
57
+ with patch("mito_ai.utils.db.USER_JSON_PATH", mock_user_json):
58
+ response = requests.get(
59
+ jp_base_url + "/mito-ai/user/non_existent_key",
60
+ headers={"Authorization": f"token {TOKEN}"},
61
+ )
62
+ assert response.status_code == 404
63
+
64
+ response_json = response.json()
65
+ assert (
66
+ response_json["error"] == "User field with key 'non_existent_key' not found"
67
+ )
68
+
69
+
70
+ def test_get_user_with_no_auth(jp_base_url: str) -> None:
71
+ response = requests.get(
72
+ jp_base_url + f"/mito-ai/user/user_email",
73
+ )
74
+ assert response.status_code == 403 # Forbidden
75
+
76
+
77
+ def test_get_user_with_incorrect_auth(jp_base_url: str) -> None:
78
+ response = requests.get(
79
+ jp_base_url + f"/mito-ai/user/user_email",
80
+ headers={"Authorization": f"token incorrect-token"},
81
+ )
82
+ assert response.status_code == 403 # Forbidden
83
+
84
+
85
+ # --- PUT USER KEY ---
86
+
87
+
88
+ def test_put_user_with_mocked_data_success(
89
+ jp_base_url: str, mock_user_json: str
90
+ ) -> None:
91
+ """Test successful PUT user endpoint with mocked data"""
92
+ with patch("mito_ai.utils.db.USER_JSON_PATH", mock_user_json):
93
+ response = requests.put(
94
+ jp_base_url + f"/mito-ai/user/user_email",
95
+ headers={"Authorization": f"token {TOKEN}"},
96
+ json={"value": "jdoe@mail.com"},
97
+ )
98
+ assert response.status_code == 200
99
+
100
+ response_json = response.json()
101
+ assert response_json["status"] == "success"
102
+ assert response_json["key"] == "user_email"
103
+ assert response_json["value"] == "jdoe@mail.com"
104
+
105
+
106
+ def test_put_user_with_no_auth(jp_base_url: str) -> None:
107
+ response = requests.put(
108
+ jp_base_url + f"/mito-ai/user/user_email",
109
+ json={"value": "jdoe@mail.com"},
110
+ )
111
+ assert response.status_code == 403 # Forbidden
112
+
113
+
114
+ def test_put_user_with_incorrect_auth(jp_base_url: str) -> None:
115
+ response = requests.put(
116
+ jp_base_url + f"/mito-ai/user/user_email",
117
+ headers={"Authorization": f"token incorrect-token"},
118
+ json={"value": "jdoe@mail.com"},
119
+ )
120
+ assert response.status_code == 403 # Forbidden
@@ -0,0 +1,3 @@
1
+ # Copyright (c) Saga Inc.
2
+ # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
+
@@ -0,0 +1,162 @@
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 anthropic
6
+ from typing import List, Dict, Any, Tuple, Union, cast
7
+ from anthropic.types import MessageParam, ToolUnionParam, ToolParam
8
+ from mito_ai.utils.anthropic_utils import ANTHROPIC_TIMEOUT, _prepare_anthropic_request_data_and_headers
9
+ from mito_ai.completions.models import MessageType
10
+ from mito_ai.utils.schema import UJ_STATIC_USER_ID, UJ_USER_EMAIL
11
+ from mito_ai.utils.db import get_user_field
12
+
13
+
14
+ # Mock the get_user_field and set_user_field functions
15
+ @pytest.fixture(autouse=True)
16
+ def mock_user_functions(monkeypatch):
17
+ def mock_get_field(field):
18
+ if field == UJ_USER_EMAIL:
19
+ return "test@example.com"
20
+ elif field == UJ_STATIC_USER_ID:
21
+ return "test_user_id"
22
+ return None
23
+
24
+ def mock_set_field(field, value):
25
+ # Do nothing in tests
26
+ pass
27
+
28
+ monkeypatch.setattr("mito_ai.utils.anthropic_utils.get_user_field", mock_get_field)
29
+ monkeypatch.setattr("mito_ai.utils.server_limits.set_user_field", mock_set_field)
30
+
31
+
32
+ def test_basic_request_preparation():
33
+ """Test basic request preparation with minimal parameters"""
34
+ model = "claude-3-sonnet"
35
+ max_tokens = 100
36
+ temperature = 0.7
37
+ # Use NotGiven to ensure system is not included in inner_data
38
+ system = anthropic.Omit()
39
+ messages: List[MessageParam] = [{"role": "user", "content": "Hello"}]
40
+ message_type = MessageType.CHAT
41
+
42
+ data, headers = _prepare_anthropic_request_data_and_headers(
43
+ model=model,
44
+ max_tokens=max_tokens,
45
+ temperature=temperature,
46
+ system=system,
47
+ messages=messages,
48
+ message_type=message_type,
49
+ tools=None,
50
+ tool_choice=None,
51
+ stream=None
52
+ )
53
+
54
+ assert headers == {"Content-Type": "application/json"}
55
+ assert data["timeout"] == ANTHROPIC_TIMEOUT
56
+ assert data["max_retries"] == 1
57
+ assert data["email"] == "test@example.com"
58
+ assert data["user_id"] == "test_user_id"
59
+
60
+ inner_data = data["data"]
61
+ assert inner_data["model"] == model
62
+ assert inner_data["max_tokens"] == max_tokens
63
+ assert inner_data["temperature"] == temperature
64
+ assert inner_data["messages"] == messages
65
+ # When system is NotGiven, it should not be included in inner_data
66
+ assert "system" not in inner_data
67
+
68
+
69
+ def test_system_message_handling():
70
+ """Test handling of system message when provided"""
71
+ system = "You are a helpful assistant"
72
+ messages: List[MessageParam] = [{"role": "user", "content": "Hello"}]
73
+
74
+ data, _ = _prepare_anthropic_request_data_and_headers(
75
+ model="claude-3-sonnet",
76
+ max_tokens=100,
77
+ temperature=0.7,
78
+ system=system,
79
+ messages=messages,
80
+ message_type=MessageType.CHAT,
81
+ tools=None,
82
+ tool_choice=None,
83
+ stream=None
84
+ )
85
+
86
+ assert data["data"]["system"] == system
87
+
88
+
89
+ def test_tools_and_tool_choice():
90
+ """Test handling of tools and tool_choice parameters"""
91
+ tools = cast(List[ToolUnionParam], [{
92
+ "type": "function",
93
+ "function": {
94
+ "name": "test_function",
95
+ "description": "A test function",
96
+ "parameters": {
97
+ "type": "object",
98
+ "properties": {},
99
+ "required": []
100
+ }
101
+ }
102
+ }])
103
+ tool_choice: Dict[str, Any] = {"type": "function", "function": {"name": "test_function"}}
104
+
105
+ data, _ = _prepare_anthropic_request_data_and_headers(
106
+ model="claude-3-sonnet",
107
+ max_tokens=100,
108
+ temperature=0.7,
109
+ system=anthropic.Omit(),
110
+ messages=[{"role": "user", "content": "Hello"}],
111
+ message_type=MessageType.CHAT,
112
+ tools=tools,
113
+ tool_choice=tool_choice,
114
+ stream=None
115
+ )
116
+
117
+ assert data["data"]["tools"] == tools
118
+ assert data["data"]["tool_choice"] == tool_choice
119
+
120
+
121
+ def test_stream_parameter():
122
+ """Test handling of stream parameter"""
123
+ data, _ = _prepare_anthropic_request_data_and_headers(
124
+ model="claude-3-sonnet",
125
+ max_tokens=100,
126
+ temperature=0.7,
127
+ system=anthropic.Omit(),
128
+ messages=[{"role": "user", "content": "Hello"}],
129
+ message_type=MessageType.CHAT,
130
+ tools=None,
131
+ tool_choice=None,
132
+ stream=True
133
+ )
134
+
135
+ assert data["data"]["stream"] is True
136
+
137
+
138
+ def test_missing_user_info(monkeypatch):
139
+ """Test behavior when user email and ID are not available"""
140
+
141
+ def mock_get_field(field):
142
+ return None
143
+
144
+ # Override the autouse fixture for this specific test
145
+ monkeypatch.setattr("mito_ai.utils.anthropic_utils.get_user_field", mock_get_field)
146
+ monkeypatch.setattr("mito_ai.utils.anthropic_utils.__user_email", None)
147
+ monkeypatch.setattr("mito_ai.utils.anthropic_utils.__user_id", None)
148
+
149
+ data, _ = _prepare_anthropic_request_data_and_headers(
150
+ model="claude-3-sonnet",
151
+ max_tokens=100,
152
+ temperature=0.7,
153
+ system=anthropic.Omit(),
154
+ messages=[{"role": "user", "content": "Hello"}],
155
+ message_type=MessageType.CHAT,
156
+ tools=None,
157
+ tool_choice=None,
158
+ stream=None
159
+ )
160
+
161
+ assert data["email"] is None
162
+ assert data["user_id"] is None
@@ -0,0 +1,98 @@
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
+ from mito_ai.utils.gemini_utils import _prepare_gemini_request_data_and_headers
6
+ from mito_ai.completions.models import MessageType
7
+
8
+ TEST_CONTENTS = [
9
+ {'role': 'system', 'content': 'You are Mito Data Copilot, an AI assistant for Jupyter.'},
10
+ {'role': 'user', 'content': 'Help me complete the following task. print 10'},
11
+ {'role': 'assistant', 'content': 'python\nprint(10)\n\nPrinted the number 10 '},
12
+ {'role': 'user', 'content': 'Update to print 11'}
13
+ ]
14
+
15
+
16
+ def test_basic_request_preparation():
17
+ """Test basic request preparation with minimal parameters."""
18
+ model = "gemini-pro"
19
+ message_type = MessageType.CHAT
20
+
21
+ data, headers = _prepare_gemini_request_data_and_headers(
22
+ model=model,
23
+ contents=TEST_CONTENTS,
24
+ message_type=message_type
25
+ )
26
+
27
+ assert headers == {"Content-Type": "application/json"}
28
+ assert data["timeout"] == 30
29
+ assert data["max_retries"] == 1
30
+ assert data["data"]["model"] == model
31
+ assert data["data"]["contents"] == TEST_CONTENTS
32
+ assert data["data"]["message_type"] == message_type.value
33
+
34
+
35
+ def test_request_with_config():
36
+ """Test request preparation with additional config parameters."""
37
+ model = "gemini-pro"
38
+ message_type = MessageType.CHAT
39
+ config = {
40
+ "temperature": 0.7,
41
+ "max_tokens": 100
42
+ }
43
+
44
+ data, headers = _prepare_gemini_request_data_and_headers(
45
+ model=model,
46
+ contents=TEST_CONTENTS,
47
+ message_type=message_type,
48
+ config=config
49
+ )
50
+
51
+ assert data["data"]["config"] == config
52
+
53
+
54
+ def test_request_with_response_format():
55
+ """Test request preparation with response format info."""
56
+
57
+ class TestFormat:
58
+ name = "test_format"
59
+ format = "json"
60
+
61
+ model = "gemini-pro"
62
+ message_type = MessageType.CHAT
63
+ response_format_info = TestFormat()
64
+
65
+ data, headers = _prepare_gemini_request_data_and_headers(
66
+ model=model,
67
+ contents=TEST_CONTENTS,
68
+ message_type=message_type,
69
+ response_format_info=response_format_info
70
+ )
71
+
72
+ expected_format = {
73
+ "name": "test_format",
74
+ "format": "json"
75
+ }
76
+ assert data["data"]["response_format_info"] == '{"name": "test_format", "format": "json"}'
77
+
78
+
79
+ def test_request_with_complex_config():
80
+ """Test request preparation with complex nested config."""
81
+ model = "gemini-pro"
82
+ message_type = MessageType.CHAT
83
+ config = {
84
+ "temperature": 0.7,
85
+ "nested": {
86
+ "key": "value",
87
+ "array": [1, 2, 3]
88
+ }
89
+ }
90
+
91
+ data, headers = _prepare_gemini_request_data_and_headers(
92
+ model=model,
93
+ contents=TEST_CONTENTS,
94
+ message_type=message_type,
95
+ config=config
96
+ )
97
+
98
+ assert data["data"]["config"] == config
@@ -0,0 +1,169 @@
1
+ # Copyright (c) Saga Inc.
2
+ # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
+
4
+ import json
5
+ import time
6
+ import unittest
7
+ from unittest.mock import patch, MagicMock, PropertyMock
8
+ from typing import Any, Optional, Tuple, List, Dict, Callable, Union, cast
9
+
10
+ import sys
11
+ import os
12
+
13
+ # Add the project root to path
14
+ project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
15
+ if project_root not in sys.path:
16
+ sys.path.insert(0, project_root)
17
+
18
+ from mito_ai.version_check import VersionCheckHandler
19
+
20
+ class TestVersionCheckHandler(unittest.TestCase):
21
+
22
+ def setUp(self) -> None:
23
+ # Clear cache before each test
24
+ if hasattr(VersionCheckHandler, '_get_latest_version'):
25
+ VersionCheckHandler._get_latest_version.cache_clear()
26
+
27
+ @patch("mito_ai.version_check.requests.get")
28
+ def test_get_latest_version_method(self, mock_requests_get: MagicMock) -> None:
29
+ # Mock successful response
30
+ mock_response = MagicMock()
31
+ mock_response.json.return_value = {
32
+ "info": {"version": "1.2.3"}
33
+ }
34
+ mock_requests_get.return_value = mock_response
35
+
36
+ # Call the method
37
+ version, cache_time = VersionCheckHandler._get_latest_version()
38
+
39
+ # Verify results
40
+ self.assertEqual(version, "1.2.3")
41
+ self.assertIsInstance(cache_time, float)
42
+
43
+ # Call it again to use cache
44
+ version2, cache_time2 = VersionCheckHandler._get_latest_version()
45
+
46
+ # Should return same results without calling requests.get again
47
+ self.assertEqual(version2, "1.2.3")
48
+ self.assertEqual(cache_time, cache_time2)
49
+ mock_requests_get.assert_called_once()
50
+
51
+ @patch("mito_ai.version_check.requests.get")
52
+ def test_get_latest_version_error(self, mock_requests_get: MagicMock) -> None:
53
+ # Mock error response
54
+ mock_requests_get.side_effect = Exception("Connection error")
55
+
56
+ try:
57
+ # The actual implementation lets the exception propagate when called directly
58
+ # So we need to catch it in our test
59
+ version, cache_time = VersionCheckHandler._get_latest_version()
60
+ self.fail("Expected an exception but none was raised")
61
+ except Exception as e:
62
+ # Just verify the exception was raised
63
+ self.assertEqual(str(e), "Connection error")
64
+
65
+ @patch("mito_ai.version_check.__version__", "1.0.0")
66
+ @patch("mito_ai.version_check.requests.get")
67
+ def test_successful_version_fetch(self, mock_requests_get: MagicMock) -> None:
68
+ # Mock the responses
69
+ mock_response = MagicMock()
70
+ mock_response.json.return_value = {
71
+ "info": {"version": "1.1.0"}
72
+ }
73
+ mock_requests_get.return_value = mock_response
74
+
75
+ # Create handler instance
76
+ handler = self._create_mocked_handler()
77
+
78
+ # Call the get method directly
79
+ handler.get()
80
+
81
+ # Get the response body and verify
82
+ response_body = json.loads(handler._write_buffer[0])
83
+ self.assertEqual(response_body["current_version"], "1.0.0")
84
+ self.assertEqual(response_body["latest_version"], "1.1.0")
85
+ self.assertIn("cache_age_seconds", response_body)
86
+
87
+ # Verify the requests were made correctly
88
+ mock_requests_get.assert_called_once_with(
89
+ "https://pypi.org/pypi/mito-ai/json", timeout=3
90
+ )
91
+
92
+ @patch("mito_ai.version_check.__version__", "1.0.0")
93
+ @patch("mito_ai.version_check.requests.get")
94
+ def test_cache_behavior(self, mock_requests_get: MagicMock) -> None:
95
+ # Mock the responses
96
+ mock_response = MagicMock()
97
+ mock_response.json.return_value = {
98
+ "info": {"version": "1.1.0"}
99
+ }
100
+ mock_requests_get.return_value = mock_response
101
+
102
+ # First request should make an external API call
103
+ handler1 = self._create_mocked_handler()
104
+ handler1.get()
105
+
106
+ # Second request should use the cache
107
+ handler2 = self._create_mocked_handler()
108
+ handler2.get()
109
+
110
+ # Verify requests.get was called only once
111
+ mock_requests_get.assert_called_once()
112
+
113
+ @patch("mito_ai.version_check.__version__", "1.0.0")
114
+ @patch("mito_ai.version_check.requests.get")
115
+ def test_pypi_request_failure(self, mock_requests_get: MagicMock) -> None:
116
+ # Mock the response
117
+ mock_requests_get.side_effect = Exception("Connection error")
118
+
119
+ # Create handler instance
120
+ handler = self._create_mocked_handler()
121
+
122
+ # Call the get method directly
123
+ handler.get()
124
+
125
+ # Get the response body and verify
126
+ response_body = json.loads(handler._write_buffer[0])
127
+ # When the request fails, the handler returns an error response
128
+ self.assertIn("error", response_body)
129
+ self.assertEqual(handler._status_code, 500)
130
+
131
+ @patch("mito_ai.version_check.__version__", "1.0.0")
132
+ def test_general_exception_handling(self) -> None:
133
+ # Create handler instance with a mock that will cause an exception
134
+ handler = self._create_mocked_handler()
135
+
136
+ with patch.object(VersionCheckHandler, '_get_latest_version', side_effect=Exception("Test error")):
137
+ # Call the get method directly
138
+ handler.get()
139
+
140
+ # Get the response body and verify
141
+ response_body = json.loads(handler._write_buffer[0])
142
+ self.assertIn("error", response_body)
143
+ self.assertEqual(handler._status_code, 500)
144
+
145
+ def _create_mocked_handler(self) -> VersionCheckHandler:
146
+ """Create a mocked RequestHandler instance for testing."""
147
+ handler = VersionCheckHandler(MagicMock(), MagicMock())
148
+ handler._status_code = 200
149
+ handler._write_buffer = []
150
+
151
+ # We need to assign the mock methods to new variables first to avoid
152
+ # the mypy "Cannot assign to a method" error
153
+ def mock_write(chunk: str) -> None:
154
+ # Convert string to bytes to match expected type
155
+ encoded_chunk = chunk.encode('utf-8') if isinstance(chunk, str) else chunk
156
+ handler._write_buffer.append(encoded_chunk)
157
+
158
+ def mock_set_status(status_code: int) -> None:
159
+ handler._status_code = status_code
160
+
161
+ def mock_set_header(name: str, value: str) -> None:
162
+ pass
163
+
164
+ # Use setattr instead of direct assignment to avoid mypy errors
165
+ setattr(handler, 'write', mock_write)
166
+ setattr(handler, 'set_status', mock_set_status)
167
+ setattr(handler, 'set_header', mock_set_header)
168
+
169
+ return handler
@@ -0,0 +1,45 @@
1
+ # Copyright (c) Saga Inc.
2
+ # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
+
4
+ import json
5
+ import tornado
6
+ from typing import Any, Optional
7
+ from jupyter_server.base.handlers import APIHandler
8
+ from mito_ai.utils.db import get_user_field, set_user_field
9
+ from mito_ai.utils.telemetry_utils import identify
10
+ from mito_ai.utils.version_utils import is_pro
11
+
12
+
13
+ class UserHandler(APIHandler):
14
+ """Handler for operations on a specific user"""
15
+
16
+ @tornado.web.authenticated
17
+ def get(self, key: str) -> None:
18
+ value: Optional[Any] = None
19
+
20
+ if key == "is_pro":
21
+ # Special case, since we don't store this key
22
+ # in the user.json file.
23
+ value = str(is_pro())
24
+ else:
25
+ value = get_user_field(key)
26
+
27
+ if value is None:
28
+ self.set_status(404)
29
+ self.finish(json.dumps({"error": f"User field with key '{key}' not found"}))
30
+ else:
31
+ self.finish(json.dumps({"key": key, "value": value}))
32
+
33
+ @tornado.web.authenticated
34
+ def put(self, key: str) -> None:
35
+ data = json.loads(self.request.body)
36
+ if "value" not in data:
37
+ self.set_status(400)
38
+ self.finish(json.dumps({"error": "Value is required"}))
39
+ return
40
+
41
+ set_user_field(key, data["value"])
42
+ identify() # Log the new user
43
+ self.finish(
44
+ json.dumps({"status": "success", "key": key, "value": data["value"]})
45
+ )
mito_ai/user/urls.py ADDED
@@ -0,0 +1,21 @@
1
+ # Copyright (c) Saga Inc.
2
+ # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
+
4
+ from typing import Any, List, Tuple
5
+ from jupyter_server.utils import url_path_join
6
+ from mito_ai.user.handlers import UserHandler
7
+
8
+
9
+ def get_user_urls(base_url: str) -> List[Tuple[str, Any, dict]]:
10
+ """Get all user related URL patterns.
11
+
12
+ Args:
13
+ base_url: The base URL for the Jupyter server
14
+
15
+ Returns:
16
+ List of (url_pattern, handler_class, handler_kwargs) tuples
17
+ """
18
+ BASE_URL = base_url + "/mito-ai"
19
+ return [
20
+ (url_path_join(BASE_URL, "user/(.*)"), UserHandler, {}),
21
+ ]
@@ -0,0 +1,3 @@
1
+ # Copyright (c) Saga Inc.
2
+ # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
+