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.
- mito_ai/__init__.py +114 -0
- mito_ai/_version.py +4 -0
- mito_ai/anthropic_client.py +334 -0
- mito_ai/app_deploy/__init__.py +6 -0
- mito_ai/app_deploy/app_deploy_utils.py +44 -0
- mito_ai/app_deploy/handlers.py +345 -0
- mito_ai/app_deploy/models.py +98 -0
- mito_ai/app_manager/__init__.py +4 -0
- mito_ai/app_manager/handlers.py +167 -0
- mito_ai/app_manager/models.py +71 -0
- mito_ai/app_manager/utils.py +24 -0
- mito_ai/auth/README.md +18 -0
- mito_ai/auth/__init__.py +6 -0
- mito_ai/auth/handlers.py +96 -0
- mito_ai/auth/urls.py +13 -0
- mito_ai/chat_history/handlers.py +63 -0
- mito_ai/chat_history/urls.py +32 -0
- mito_ai/completions/completion_handlers/__init__.py +3 -0
- mito_ai/completions/completion_handlers/agent_auto_error_fixup_handler.py +59 -0
- mito_ai/completions/completion_handlers/agent_execution_handler.py +66 -0
- mito_ai/completions/completion_handlers/chat_completion_handler.py +141 -0
- mito_ai/completions/completion_handlers/code_explain_handler.py +113 -0
- mito_ai/completions/completion_handlers/completion_handler.py +42 -0
- mito_ai/completions/completion_handlers/inline_completer_handler.py +48 -0
- mito_ai/completions/completion_handlers/smart_debug_handler.py +160 -0
- mito_ai/completions/completion_handlers/utils.py +147 -0
- mito_ai/completions/handlers.py +415 -0
- mito_ai/completions/message_history.py +401 -0
- mito_ai/completions/models.py +404 -0
- mito_ai/completions/prompt_builders/__init__.py +3 -0
- mito_ai/completions/prompt_builders/agent_execution_prompt.py +57 -0
- mito_ai/completions/prompt_builders/agent_smart_debug_prompt.py +160 -0
- mito_ai/completions/prompt_builders/agent_system_message.py +472 -0
- mito_ai/completions/prompt_builders/chat_name_prompt.py +15 -0
- mito_ai/completions/prompt_builders/chat_prompt.py +116 -0
- mito_ai/completions/prompt_builders/chat_system_message.py +92 -0
- mito_ai/completions/prompt_builders/explain_code_prompt.py +32 -0
- mito_ai/completions/prompt_builders/inline_completer_prompt.py +197 -0
- mito_ai/completions/prompt_builders/prompt_constants.py +170 -0
- mito_ai/completions/prompt_builders/smart_debug_prompt.py +199 -0
- mito_ai/completions/prompt_builders/utils.py +84 -0
- mito_ai/completions/providers.py +284 -0
- mito_ai/constants.py +63 -0
- mito_ai/db/__init__.py +3 -0
- mito_ai/db/crawlers/__init__.py +6 -0
- mito_ai/db/crawlers/base_crawler.py +61 -0
- mito_ai/db/crawlers/constants.py +43 -0
- mito_ai/db/crawlers/snowflake.py +71 -0
- mito_ai/db/handlers.py +168 -0
- mito_ai/db/models.py +31 -0
- mito_ai/db/urls.py +34 -0
- mito_ai/db/utils.py +185 -0
- mito_ai/docker/mssql/compose.yml +37 -0
- mito_ai/docker/mssql/init/setup.sql +21 -0
- mito_ai/docker/mysql/compose.yml +18 -0
- mito_ai/docker/mysql/init/setup.sql +13 -0
- mito_ai/docker/oracle/compose.yml +17 -0
- mito_ai/docker/oracle/init/setup.sql +20 -0
- mito_ai/docker/postgres/compose.yml +17 -0
- mito_ai/docker/postgres/init/setup.sql +13 -0
- mito_ai/enterprise/__init__.py +3 -0
- mito_ai/enterprise/utils.py +15 -0
- mito_ai/file_uploads/__init__.py +3 -0
- mito_ai/file_uploads/handlers.py +248 -0
- mito_ai/file_uploads/urls.py +21 -0
- mito_ai/gemini_client.py +232 -0
- mito_ai/log/handlers.py +38 -0
- mito_ai/log/urls.py +21 -0
- mito_ai/logger.py +37 -0
- mito_ai/openai_client.py +382 -0
- mito_ai/path_utils.py +70 -0
- mito_ai/rules/handlers.py +44 -0
- mito_ai/rules/urls.py +22 -0
- mito_ai/rules/utils.py +56 -0
- mito_ai/settings/handlers.py +41 -0
- mito_ai/settings/urls.py +20 -0
- mito_ai/settings/utils.py +42 -0
- mito_ai/streamlit_conversion/agent_utils.py +37 -0
- mito_ai/streamlit_conversion/prompts/prompt_constants.py +172 -0
- mito_ai/streamlit_conversion/prompts/prompt_utils.py +10 -0
- mito_ai/streamlit_conversion/prompts/streamlit_app_creation_prompt.py +46 -0
- mito_ai/streamlit_conversion/prompts/streamlit_error_correction_prompt.py +28 -0
- mito_ai/streamlit_conversion/prompts/streamlit_finish_todo_prompt.py +45 -0
- mito_ai/streamlit_conversion/prompts/streamlit_system_prompt.py +56 -0
- mito_ai/streamlit_conversion/prompts/update_existing_app_prompt.py +50 -0
- mito_ai/streamlit_conversion/search_replace_utils.py +94 -0
- mito_ai/streamlit_conversion/streamlit_agent_handler.py +144 -0
- mito_ai/streamlit_conversion/streamlit_utils.py +85 -0
- mito_ai/streamlit_conversion/validate_streamlit_app.py +105 -0
- mito_ai/streamlit_preview/__init__.py +6 -0
- mito_ai/streamlit_preview/handlers.py +111 -0
- mito_ai/streamlit_preview/manager.py +152 -0
- mito_ai/streamlit_preview/urls.py +22 -0
- mito_ai/streamlit_preview/utils.py +29 -0
- mito_ai/tests/__init__.py +3 -0
- mito_ai/tests/chat_history/test_chat_history.py +211 -0
- mito_ai/tests/completions/completion_handlers_utils_test.py +190 -0
- mito_ai/tests/conftest.py +53 -0
- mito_ai/tests/create_agent_system_message_prompt_test.py +22 -0
- mito_ai/tests/data/prompt_lg.py +69 -0
- mito_ai/tests/data/prompt_sm.py +6 -0
- mito_ai/tests/data/prompt_xl.py +13 -0
- mito_ai/tests/data/stock_data.sqlite3 +0 -0
- mito_ai/tests/db/conftest.py +39 -0
- mito_ai/tests/db/connections_test.py +102 -0
- mito_ai/tests/db/mssql_test.py +29 -0
- mito_ai/tests/db/mysql_test.py +29 -0
- mito_ai/tests/db/oracle_test.py +29 -0
- mito_ai/tests/db/postgres_test.py +29 -0
- mito_ai/tests/db/schema_test.py +93 -0
- mito_ai/tests/db/sqlite_test.py +31 -0
- mito_ai/tests/db/test_db_constants.py +61 -0
- mito_ai/tests/deploy_app/test_app_deploy_utils.py +89 -0
- mito_ai/tests/file_uploads/__init__.py +2 -0
- mito_ai/tests/file_uploads/test_handlers.py +282 -0
- mito_ai/tests/message_history/test_generate_short_chat_name.py +120 -0
- mito_ai/tests/message_history/test_message_history_utils.py +469 -0
- mito_ai/tests/open_ai_utils_test.py +152 -0
- mito_ai/tests/performance_test.py +329 -0
- mito_ai/tests/providers/test_anthropic_client.py +447 -0
- mito_ai/tests/providers/test_azure.py +631 -0
- mito_ai/tests/providers/test_capabilities.py +120 -0
- mito_ai/tests/providers/test_gemini_client.py +195 -0
- mito_ai/tests/providers/test_mito_server_utils.py +448 -0
- mito_ai/tests/providers/test_model_resolution.py +130 -0
- mito_ai/tests/providers/test_openai_client.py +57 -0
- mito_ai/tests/providers/test_provider_completion_exception.py +66 -0
- mito_ai/tests/providers/test_provider_limits.py +42 -0
- mito_ai/tests/providers/test_providers.py +382 -0
- mito_ai/tests/providers/test_retry_logic.py +389 -0
- mito_ai/tests/providers/test_stream_mito_server_utils.py +140 -0
- mito_ai/tests/providers/utils.py +85 -0
- mito_ai/tests/rules/conftest.py +26 -0
- mito_ai/tests/rules/rules_test.py +117 -0
- mito_ai/tests/server_limits_test.py +406 -0
- mito_ai/tests/settings/conftest.py +26 -0
- mito_ai/tests/settings/settings_test.py +70 -0
- mito_ai/tests/settings/test_settings_constants.py +9 -0
- mito_ai/tests/streamlit_conversion/__init__.py +3 -0
- mito_ai/tests/streamlit_conversion/test_apply_search_replace.py +240 -0
- mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +246 -0
- mito_ai/tests/streamlit_conversion/test_streamlit_utils.py +193 -0
- mito_ai/tests/streamlit_conversion/test_validate_streamlit_app.py +112 -0
- mito_ai/tests/streamlit_preview/test_streamlit_preview_handler.py +118 -0
- mito_ai/tests/streamlit_preview/test_streamlit_preview_manager.py +292 -0
- mito_ai/tests/test_constants.py +47 -0
- mito_ai/tests/test_telemetry.py +12 -0
- mito_ai/tests/user/__init__.py +2 -0
- mito_ai/tests/user/test_user.py +120 -0
- mito_ai/tests/utils/__init__.py +3 -0
- mito_ai/tests/utils/test_anthropic_utils.py +162 -0
- mito_ai/tests/utils/test_gemini_utils.py +98 -0
- mito_ai/tests/version_check_test.py +169 -0
- mito_ai/user/handlers.py +45 -0
- mito_ai/user/urls.py +21 -0
- mito_ai/utils/__init__.py +3 -0
- mito_ai/utils/anthropic_utils.py +168 -0
- mito_ai/utils/create.py +94 -0
- mito_ai/utils/db.py +74 -0
- mito_ai/utils/error_classes.py +42 -0
- mito_ai/utils/gemini_utils.py +133 -0
- mito_ai/utils/message_history_utils.py +87 -0
- mito_ai/utils/mito_server_utils.py +242 -0
- mito_ai/utils/open_ai_utils.py +200 -0
- mito_ai/utils/provider_utils.py +49 -0
- mito_ai/utils/schema.py +86 -0
- mito_ai/utils/server_limits.py +152 -0
- mito_ai/utils/telemetry_utils.py +480 -0
- mito_ai/utils/utils.py +89 -0
- mito_ai/utils/version_utils.py +94 -0
- mito_ai/utils/websocket_base.py +88 -0
- mito_ai/version_check.py +60 -0
- mito_ai-0.1.50.data/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +7 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/build_log.json +728 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/package.json +243 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/package.json.orig +238 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/toolbar-buttons.json +37 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.8f1845da6bf2b128c049.js +21602 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.8f1845da6bf2b128c049.js.map +1 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js +198 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js.map +1 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.78d3ccb73e7ca1da3aae.js +619 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.78d3ccb73e7ca1da3aae.js.map +1 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/style.js +4 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js +712 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js.map +1 -0
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js +2792 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js.map +1 -0
- 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
- 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
- mito_ai-0.1.50.dist-info/METADATA +221 -0
- mito_ai-0.1.50.dist-info/RECORD +205 -0
- mito_ai-0.1.50.dist-info/WHEEL +4 -0
- mito_ai-0.1.50.dist-info/entry_points.txt +2 -0
- 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
|
+
]
|
mito_ai/gemini_client.py
ADDED
|
@@ -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)}"
|
mito_ai/log/handlers.py
ADDED
|
@@ -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
|