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,248 @@
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 tornado
7
+ from typing import Dict, Any
8
+ from jupyter_server.base.handlers import APIHandler
9
+ from mito_ai.utils.telemetry_utils import (
10
+ log_file_upload_attempt,
11
+ log_file_upload_failure,
12
+ )
13
+
14
+ MAX_IMAGE_SIZE_MB = 3
15
+
16
+
17
+ def _is_image_file(filename: str) -> bool:
18
+ image_extensions = {
19
+ ".jpg",
20
+ ".jpeg",
21
+ ".png",
22
+ ".gif",
23
+ ".bmp",
24
+ ".tiff",
25
+ ".tif",
26
+ ".webp",
27
+ ".svg",
28
+ }
29
+ file_extension = os.path.splitext(filename)[1].lower()
30
+ return file_extension in image_extensions
31
+
32
+
33
+ def _check_image_size_limit(file_data: bytes, filename: str) -> None:
34
+ if not _is_image_file(filename):
35
+ return
36
+
37
+ file_size_mb = len(file_data) / (1024 * 1024) # Convert bytes to MB
38
+
39
+ if file_size_mb > MAX_IMAGE_SIZE_MB:
40
+ raise ValueError(f"Image exceeded {MAX_IMAGE_SIZE_MB}MB limit.")
41
+
42
+
43
+ class FileUploadHandler(APIHandler):
44
+ # Class-level dictionary to store temporary directories for each file upload
45
+ # This persists across handler instances since Tornado recreates handlers per request
46
+ # Key: filename, Value: dict with temp_dir, total_chunks, received_chunks, logged_upload
47
+ _temp_dirs: Dict[str, Dict[str, Any]] = {}
48
+
49
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
50
+ super().__init__(*args, **kwargs)
51
+
52
+ @tornado.web.authenticated
53
+ def post(self) -> None:
54
+ """Handle file upload with multipart form data."""
55
+ try:
56
+ # Validate request has file
57
+ if not self._validate_file_upload():
58
+ return
59
+
60
+ uploaded_file = self.request.files["file"][0]
61
+ filename = uploaded_file["filename"]
62
+ file_data = uploaded_file["body"]
63
+
64
+ # Get notebook directory from request
65
+ notebook_dir = self.get_argument("notebook_dir", ".")
66
+
67
+ # Check if this is a chunked upload
68
+ chunk_number = self.get_argument("chunk_number", None)
69
+ total_chunks = self.get_argument("total_chunks", None)
70
+
71
+ if chunk_number and total_chunks:
72
+ self._handle_chunked_upload(
73
+ filename, file_data, chunk_number, total_chunks, notebook_dir
74
+ )
75
+ else:
76
+ # Log the file upload attempt for regular (non-chunked) uploads
77
+ file_extension = filename.split(".")[-1].lower()
78
+ log_file_upload_attempt(filename, file_extension, False, 0)
79
+ self._handle_regular_upload(filename, file_data, notebook_dir)
80
+
81
+ self.finish()
82
+
83
+ except Exception as e:
84
+ self._handle_error(str(e))
85
+
86
+ def _validate_file_upload(self) -> bool:
87
+ """Validate that a file was uploaded in the request."""
88
+ if "file" not in self.request.files:
89
+ self._handle_error("No file uploaded", status_code=400)
90
+ return False
91
+ return True
92
+
93
+ def _handle_chunked_upload(
94
+ self,
95
+ filename: str,
96
+ file_data: bytes,
97
+ chunk_number: str,
98
+ total_chunks: str,
99
+ notebook_dir: str,
100
+ ) -> None:
101
+ """Handle chunked file upload."""
102
+ chunk_num = int(chunk_number)
103
+ total_chunks_num = int(total_chunks)
104
+
105
+ # Log the file upload attempt only for the first chunk
106
+ if chunk_num == 1:
107
+ file_extension = filename.split(".")[-1].lower()
108
+ log_file_upload_attempt(filename, file_extension, True, total_chunks_num)
109
+
110
+ # Save chunk to temporary file
111
+ self._save_chunk(filename, file_data, chunk_num, total_chunks_num)
112
+
113
+ # Check if all chunks are received and reconstruct if complete
114
+ if self._are_all_chunks_received(filename, total_chunks_num):
115
+ self._reconstruct_file(filename, total_chunks_num, notebook_dir)
116
+ self._send_chunk_complete_response(filename, notebook_dir)
117
+ else:
118
+ self._send_chunk_received_response(chunk_num, total_chunks_num)
119
+
120
+ def _handle_regular_upload(
121
+ self, filename: str, file_data: bytes, notebook_dir: str
122
+ ) -> None:
123
+ """Handle regular (non-chunked) file upload."""
124
+ # Check image file size limit before saving
125
+ _check_image_size_limit(file_data, filename)
126
+
127
+ file_path = os.path.join(notebook_dir, filename)
128
+ with open(file_path, "wb") as f:
129
+ f.write(file_data)
130
+
131
+ self.write({"success": True, "filename": filename, "path": file_path})
132
+
133
+ def _save_chunk(
134
+ self, filename: str, file_data: bytes, chunk_number: int, total_chunks: int
135
+ ) -> None:
136
+ """Save a chunk to a temporary file."""
137
+ # Initialize temporary directory for this file if it doesn't exist
138
+ if filename not in self._temp_dirs:
139
+ temp_dir = tempfile.mkdtemp(prefix=f"mito_upload_{filename}_")
140
+ self._temp_dirs[filename] = {
141
+ "temp_dir": temp_dir,
142
+ "total_chunks": total_chunks,
143
+ "received_chunks": set(),
144
+ }
145
+
146
+ # Save the chunk to the temporary directory
147
+ chunk_filename = os.path.join(
148
+ self._temp_dirs[filename]["temp_dir"], f"chunk_{chunk_number}"
149
+ )
150
+ with open(chunk_filename, "wb") as f:
151
+ f.write(file_data)
152
+
153
+ # Mark this chunk as received
154
+ self._temp_dirs[filename]["received_chunks"].add(chunk_number)
155
+
156
+ def _are_all_chunks_received(self, filename: str, total_chunks: int) -> bool:
157
+ """Check if all chunks for a file have been received."""
158
+ if filename not in self._temp_dirs:
159
+ return False
160
+
161
+ received_chunks = self._temp_dirs[filename]["received_chunks"]
162
+ is_complete = len(received_chunks) == total_chunks
163
+ return is_complete
164
+
165
+ def _reconstruct_file(
166
+ self, filename: str, total_chunks: int, notebook_dir: str
167
+ ) -> None:
168
+ """Reconstruct the final file from all chunks and clean up temporary directory."""
169
+
170
+ if filename not in self._temp_dirs:
171
+ raise ValueError(f"No temporary directory found for file: {filename}")
172
+
173
+ temp_dir = self._temp_dirs[filename]["temp_dir"]
174
+ file_path = os.path.join(notebook_dir, filename)
175
+
176
+ try:
177
+ # First, read all chunks to check total file size for images
178
+ all_file_data = b""
179
+ for i in range(1, total_chunks + 1):
180
+ chunk_filename = os.path.join(temp_dir, f"chunk_{i}")
181
+ with open(chunk_filename, "rb") as chunk_file:
182
+ chunk_data = chunk_file.read()
183
+ all_file_data += chunk_data
184
+
185
+ # Check image file size limit before saving
186
+ _check_image_size_limit(all_file_data, filename)
187
+
188
+ # Write the complete file
189
+ with open(file_path, "wb") as final_file:
190
+ final_file.write(all_file_data)
191
+ finally:
192
+ # Clean up the temporary directory
193
+ self._cleanup_temp_dir(filename)
194
+
195
+ def _cleanup_temp_dir(self, filename: str) -> None:
196
+ """Clean up the temporary directory for a file."""
197
+ if filename in self._temp_dirs:
198
+ temp_dir = self._temp_dirs[filename]["temp_dir"]
199
+ try:
200
+ import shutil
201
+
202
+ shutil.rmtree(temp_dir)
203
+ except Exception as e:
204
+ # Log the error but don't fail the upload
205
+ print(
206
+ f"Warning: Failed to clean up temporary directory {temp_dir}: {e}"
207
+ )
208
+ finally:
209
+ # Remove from tracking dictionary
210
+ del self._temp_dirs[filename]
211
+
212
+ def _send_chunk_complete_response(self, filename: str, notebook_dir: str) -> None:
213
+ """Send response indicating all chunks have been processed and file is complete."""
214
+ file_path = os.path.join(notebook_dir, filename)
215
+ self.write(
216
+ {
217
+ "success": True,
218
+ "filename": filename,
219
+ "path": file_path,
220
+ "chunk_complete": True,
221
+ }
222
+ )
223
+
224
+ def _send_chunk_received_response(
225
+ self, chunk_number: int, total_chunks: int
226
+ ) -> None:
227
+ """Send response indicating a chunk was received but file is not yet complete."""
228
+ self.write(
229
+ {
230
+ "success": True,
231
+ "chunk_received": True,
232
+ "chunk_number": chunk_number,
233
+ "total_chunks": total_chunks,
234
+ }
235
+ )
236
+
237
+ def _handle_error(self, error_message: str, status_code: int = 500) -> None:
238
+ """Handle errors and send appropriate error response."""
239
+ log_file_upload_failure(error_message)
240
+ self.set_status(status_code)
241
+ self.write({"error": error_message})
242
+ self.finish()
243
+
244
+ def on_finish(self) -> None:
245
+ """Clean up any remaining temporary directories when the handler is finished."""
246
+ super().on_finish()
247
+ # Note: We don't clean up here anymore since we want to preserve state across requests
248
+ # The cleanup happens when the file is fully reconstructed
@@ -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 List, Tuple, Any
5
+ from jupyter_server.utils import url_path_join
6
+ from mito_ai.file_uploads.handlers import FileUploadHandler
7
+
8
+
9
+ def get_file_uploads_urls(base_url: str) -> List[Tuple[str, Any, dict]]:
10
+ """Get all file uploads 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, "upload"), FileUploadHandler, {}),
21
+ ]
@@ -0,0 +1,232 @@
1
+ # Copyright (c) Saga Inc.
2
+ # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
+ import base64
4
+ from typing import Any, Callable, Dict, List, Optional, Union, Tuple
5
+ from google import genai
6
+ from google.genai import types
7
+ from google.genai.types import GenerateContentConfig, Part, Content, GenerateContentResponse
8
+ from mito_ai.completions.models import CompletionError, CompletionItem, CompletionReply, CompletionStreamChunk, MessageType, ResponseFormatInfo
9
+ from mito_ai.utils.gemini_utils import get_gemini_completion_from_mito_server, stream_gemini_completion_from_mito_server, get_gemini_completion_function_params
10
+ from mito_ai.utils.mito_server_utils import ProviderCompletionException
11
+
12
+ def extract_and_parse_gemini_json_response(response: GenerateContentResponse) -> Optional[str]:
13
+ """
14
+ Extracts and parses the JSON response from the Gemini API.
15
+ """
16
+ if hasattr(response, 'text') and response.text:
17
+ return response.text
18
+
19
+ if hasattr(response, 'candidates') and response.candidates:
20
+ candidate = response.candidates[0]
21
+ if hasattr(candidate, 'content') and candidate.content:
22
+ content = candidate.content
23
+ if hasattr(content, 'parts') and content.parts:
24
+ return " ".join(str(part) for part in content.parts)
25
+ return str(content)
26
+ return str(candidate)
27
+
28
+ return None
29
+
30
+ def get_gemini_system_prompt_and_messages(messages: List[Dict[str, Any]]) -> Tuple[str, List[Dict[str, Any]]]:
31
+ """
32
+ Converts a list of OpenAI messages to a list of Gemini messages.
33
+
34
+ IMPORTANT: THIS FUNCTION IS ALSO USED IN THE LAMDBA FUNCTION. IF YOU UPDATE IT HERE,
35
+ YOU PROBABLY NEED TO UPDATE THE LAMBDA FUNCTION AS WELL.
36
+ """
37
+
38
+ system_prompt = ""
39
+ gemini_messages: List[Dict[str, Any]] = []
40
+
41
+ for msg in messages:
42
+ role = msg.get("role")
43
+ content = msg.get("content", "")
44
+
45
+ if role == "system":
46
+ if content:
47
+ # We assume that that there is only one system message
48
+ system_prompt = content
49
+ elif role in ("user", "assistant"):
50
+ parts: List[Union[Dict[str, str], Part]] = []
51
+
52
+ # Handle different content types
53
+ if isinstance(content, str):
54
+ # Simple text content
55
+ if content:
56
+ parts.append({"text": content})
57
+ elif isinstance(content, list):
58
+ # Mixed content (text + images)
59
+ for item in content:
60
+ if item.get("type") == "text":
61
+ text_content = item.get("text", "")
62
+ if text_content:
63
+ parts.append({"text": text_content})
64
+ elif item.get("type") == "image_url":
65
+ image_url_data = item.get("image_url", {})
66
+ image_url = image_url_data.get("url", "")
67
+
68
+ if image_url and image_url.startswith("data:"):
69
+ # Handle base64 data URLs
70
+ try:
71
+ # Extract the base64 data and mime type
72
+ header, base64_data = image_url.split(",", 1)
73
+ mime_type = header.split(";")[0].split(":")[1]
74
+
75
+ # Decode base64 to bytes
76
+ image_bytes = base64.b64decode(base64_data)
77
+
78
+ # Create Gemini image part
79
+ image_part = types.Part.from_bytes(
80
+ data=image_bytes,
81
+ mime_type=mime_type
82
+ )
83
+ parts.append(image_part)
84
+ except Exception as e:
85
+ print(f"Error processing image: {e}")
86
+ # Skip this image if there's an error
87
+ continue
88
+
89
+ # Only add to contents if we have parts
90
+ if parts:
91
+ # Map assistant role to model role for Gemini API
92
+ gemini_role = "model" if role == "assistant" else "user"
93
+ gemini_messages.append({
94
+ "role": gemini_role,
95
+ "parts": parts
96
+ })
97
+
98
+ return system_prompt, gemini_messages
99
+
100
+
101
+ class GeminiClient:
102
+ def __init__(self, api_key: Optional[str]):
103
+ self.api_key = api_key
104
+ if api_key:
105
+ self.client = genai.Client(api_key=api_key)
106
+
107
+ async def request_completions(
108
+ self,
109
+ messages: List[Dict[str, Any]],
110
+ model: str,
111
+ response_format_info: Optional[ResponseFormatInfo] = None,
112
+ message_type: MessageType = MessageType.CHAT
113
+ ) -> str:
114
+ # Extract system instructions and contents
115
+ system_instructions, contents = get_gemini_system_prompt_and_messages(messages)
116
+
117
+ # Get provider data for Gemini completion
118
+ provider_data = get_gemini_completion_function_params(
119
+ model=model,
120
+ contents=contents,
121
+ message_type=message_type,
122
+ response_format_info=response_format_info
123
+ )
124
+
125
+ if self.api_key:
126
+ # Generate content using the Gemini client
127
+ response_config = GenerateContentConfig(
128
+ system_instruction=system_instructions,
129
+ response_mime_type=provider_data.get("config", {}).get("response_mime_type"),
130
+ response_schema=provider_data.get("config", {}).get("response_schema")
131
+ )
132
+ response = self.client.models.generate_content(
133
+ model=provider_data["model"],
134
+ contents=contents, # type: ignore
135
+ config=response_config
136
+ )
137
+
138
+ result = extract_and_parse_gemini_json_response(response)
139
+
140
+ if not result:
141
+ return "No response received from Gemini API"
142
+
143
+ return result
144
+ else:
145
+ # Fallback to Mito server for completion
146
+ return await get_gemini_completion_from_mito_server(
147
+ model=provider_data["model"],
148
+ contents=messages, # Use the extracted contents instead of converted messages to avoid serialization issues
149
+ message_type=message_type,
150
+ config=provider_data.get("config", None),
151
+ response_format_info=response_format_info,
152
+ )
153
+
154
+ async def stream_completions(
155
+ self,
156
+ messages: List[Dict[str, Any]],
157
+ model: str,
158
+ message_id: str,
159
+ reply_fn: Callable[[Union[CompletionReply, CompletionStreamChunk]], None],
160
+ message_type: MessageType = MessageType.CHAT
161
+ ) -> str:
162
+ accumulated_response = ""
163
+ try:
164
+ # Extract system instructions and Gemini-compatible contents
165
+ system_instructions, contents = get_gemini_system_prompt_and_messages(messages)
166
+ if self.api_key:
167
+ for chunk in self.client.models.generate_content_stream(
168
+ model=model,
169
+ contents=contents, # type: ignore
170
+ config=GenerateContentConfig(
171
+ system_instruction=system_instructions
172
+ )
173
+ ):
174
+
175
+ next_chunk = ""
176
+ if hasattr(chunk, 'text'):
177
+ next_chunk = chunk.text or ''
178
+ else:
179
+ next_chunk = str(chunk)
180
+
181
+ accumulated_response += next_chunk
182
+
183
+ # Return the chunk to the frontend
184
+ reply_fn(CompletionStreamChunk(
185
+ parent_id=message_id,
186
+ chunk=CompletionItem(
187
+ content=next_chunk or '',
188
+ isIncomplete=True,
189
+ token=message_id,
190
+ ),
191
+ done=False,
192
+ ))
193
+
194
+ # Send final chunk
195
+ reply_fn(CompletionStreamChunk(
196
+ parent_id=message_id,
197
+ chunk=CompletionItem(
198
+ content="",
199
+ isIncomplete=False,
200
+ token=message_id,
201
+ ),
202
+ done=True,
203
+ ))
204
+ return accumulated_response
205
+ else:
206
+ async for chunk_text in stream_gemini_completion_from_mito_server(
207
+ model=model,
208
+ contents=messages, # Use the extracted contents instead of converted messages to avoid serialization issues
209
+ message_type=message_type,
210
+ message_id=message_id,
211
+ reply_fn=reply_fn
212
+ ):
213
+ # Clean and decode the chunk text
214
+ clean_chunk = chunk_text.strip('"')
215
+ decoded_chunk = clean_chunk.encode().decode('unicode_escape')
216
+ accumulated_response += decoded_chunk or ''
217
+
218
+ # Send final chunk with the complete response
219
+ reply_fn(CompletionStreamChunk(
220
+ parent_id=message_id,
221
+ chunk=CompletionItem(
222
+ content=accumulated_response,
223
+ isIncomplete=False,
224
+ token=message_id,
225
+ ),
226
+ done=True,
227
+ ))
228
+
229
+ return accumulated_response
230
+
231
+ except Exception as e:
232
+ return f"Error streaming content: {str(e)}"
@@ -0,0 +1,38 @@
1
+ # Copyright (c) Saga Inc.
2
+ # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
+
4
+ from dataclasses import dataclass
5
+ import json
6
+ from typing import Any, Final, Literal
7
+ import tornado
8
+ import os
9
+ from jupyter_server.base.handlers import APIHandler
10
+ from mito_ai.utils.telemetry_utils import MITO_SERVER_KEY, USER_KEY, log
11
+
12
+
13
+ class LogHandler(APIHandler):
14
+ """Handler for logging"""
15
+
16
+ def initialize(self, key_type: Literal['mito_server_key', 'user_key']) -> None:
17
+ """Initialize the log handler"""
18
+
19
+ # The key_type is required so that we know if we can log pro users
20
+ self.key_type = key_type
21
+
22
+ @tornado.web.authenticated
23
+ def put(self) -> None:
24
+ """Log an event"""
25
+ data = json.loads(self.request.body)
26
+
27
+ if 'log_event' not in data:
28
+ self.set_status(400)
29
+ self.finish(json.dumps({"error": "Log event is required"}))
30
+ return
31
+
32
+ log_event = data['log_event']
33
+ params = data.get('params', {})
34
+
35
+ key_type = MITO_SERVER_KEY if self.key_type == "mito_server_key" else USER_KEY
36
+ log(log_event, params, key_type=key_type)
37
+
38
+
mito_ai/log/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.log.handlers import LogHandler
7
+
8
+ def get_log_urls(base_url: str, key_type: str) -> List[Tuple[str, Any, dict]]:
9
+ """Get all log related URL patterns.
10
+
11
+ Args:
12
+ base_url: The base URL for the Jupyter server
13
+
14
+ Returns:
15
+ List of (url_pattern, handler_class, handler_kwargs) tuples
16
+ """
17
+ BASE_URL = base_url + "/mito-ai"
18
+
19
+ return [
20
+ (url_path_join(BASE_URL, "log"), LogHandler, {"key_type": key_type}),
21
+ ]
mito_ai/logger.py ADDED
@@ -0,0 +1,37 @@
1
+ # Copyright (c) Saga Inc.
2
+ # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
+
4
+ import logging
5
+
6
+ from traitlets.config import Application
7
+
8
+
9
+ _LOGGER = None # type: logging.Logger | None
10
+
11
+
12
+ def get_logger() -> logging.Logger:
13
+ """Create a logger for the Mito AI module.
14
+
15
+ The logger will be attached as a child of the Jupyter server logger.
16
+ This allows for easier filtering/flagging of log messages in the server logs.
17
+
18
+ Example:
19
+
20
+ The following snippet shows a log message produced by JupyterLab followed by
21
+ two log messages produced by the Mito AI module.
22
+
23
+ [D 2024-12-16 15:49:07.333 LabApp] 204 PUT /lab/api/workspaces/default?1734360547329 (ea7486428da24ff3921aebd4422611d9@::1) 1.85ms
24
+ [D 2024-12-16 15:49:08.293 ServerApp.mito_ai] Message received: {...}
25
+ [D 2024-12-16 15:49:08.293 ServerApp.mito_ai] Requesting completion from Mito server.
26
+
27
+ You can filter the server logs to only show messages produced by the Mito AI module:
28
+
29
+ jupyter lab --debug 2>&1 | egrep mito_ai
30
+ """
31
+ global _LOGGER
32
+ if _LOGGER is None:
33
+ app = Application.instance()
34
+ _LOGGER = logging.getLogger("{!s}.mito_ai".format(app.log.name))
35
+ Application.clear_instance()
36
+
37
+ return _LOGGER