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
mito_ai/openai_client.py
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
# Copyright (c) Saga Inc.
|
|
2
|
+
# Distributed under the terms of the GNU Affero General Public License v3.0 License.
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
from typing import Any, AsyncGenerator, Callable, Dict, List, Optional, Union
|
|
6
|
+
|
|
7
|
+
from mito_ai.utils.mito_server_utils import ProviderCompletionException
|
|
8
|
+
import openai
|
|
9
|
+
from openai.types.chat import ChatCompletionMessageParam
|
|
10
|
+
from traitlets import Instance, Unicode, default, validate
|
|
11
|
+
from traitlets.config import LoggingConfigurable
|
|
12
|
+
|
|
13
|
+
from mito_ai import constants
|
|
14
|
+
from mito_ai.enterprise.utils import is_azure_openai_configured
|
|
15
|
+
from mito_ai.logger import get_logger
|
|
16
|
+
from mito_ai.completions.models import (
|
|
17
|
+
AICapabilities,
|
|
18
|
+
CompletionError,
|
|
19
|
+
CompletionItem,
|
|
20
|
+
CompletionItemError,
|
|
21
|
+
CompletionReply,
|
|
22
|
+
CompletionStreamChunk,
|
|
23
|
+
MessageType,
|
|
24
|
+
ResponseFormatInfo,
|
|
25
|
+
)
|
|
26
|
+
from mito_ai.utils.open_ai_utils import (
|
|
27
|
+
check_mito_server_quota,
|
|
28
|
+
get_ai_completion_from_mito_server,
|
|
29
|
+
get_open_ai_completion_function_params,
|
|
30
|
+
stream_ai_completion_from_mito_server,
|
|
31
|
+
)
|
|
32
|
+
from mito_ai.utils.server_limits import update_mito_server_quota
|
|
33
|
+
from mito_ai.utils.telemetry_utils import (
|
|
34
|
+
MITO_SERVER_KEY,
|
|
35
|
+
USER_KEY,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
OPENAI_MODEL_FALLBACK = "gpt-4.1"
|
|
39
|
+
|
|
40
|
+
class OpenAIClient(LoggingConfigurable):
|
|
41
|
+
"""Provide AI feature through OpenAI services."""
|
|
42
|
+
|
|
43
|
+
api_key = Unicode(
|
|
44
|
+
config=True,
|
|
45
|
+
allow_none=True,
|
|
46
|
+
help="OpenAI API key. Default value is read from the OPENAI_API_KEY environment variable.",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
last_error = Instance(
|
|
50
|
+
CompletionError,
|
|
51
|
+
allow_none=True,
|
|
52
|
+
help="""Last error encountered when using the OpenAI provider.
|
|
53
|
+
|
|
54
|
+
This attribute is observed by the websocket provider to push the error to the client.""",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Consider the request a failure if it takes longer than 45 seconds.
|
|
58
|
+
# We will try a total of 3 times. Once on the initial request,
|
|
59
|
+
# and then twice more if the first request fails.
|
|
60
|
+
# Note that max_retries cannot be set to None. If we want to disable it, set it to 0.
|
|
61
|
+
timeout = 30
|
|
62
|
+
max_retries = 1
|
|
63
|
+
|
|
64
|
+
def __init__(self, **kwargs: Dict[str, Any]) -> None:
|
|
65
|
+
super().__init__(log=get_logger(), **kwargs)
|
|
66
|
+
self.last_error = None
|
|
67
|
+
self._async_client: Optional[openai.AsyncOpenAI] = None
|
|
68
|
+
|
|
69
|
+
@default("api_key")
|
|
70
|
+
def _api_key_default(self) -> Optional[str]:
|
|
71
|
+
default_key = constants.OPENAI_API_KEY
|
|
72
|
+
return self._validate_api_key(default_key)
|
|
73
|
+
|
|
74
|
+
@validate("api_key")
|
|
75
|
+
def _validate_api_key(self, api_key: Optional[str]) -> Optional[str]:
|
|
76
|
+
if not api_key:
|
|
77
|
+
self.log.debug(
|
|
78
|
+
"No OpenAI API key provided; following back to Mito server API."
|
|
79
|
+
)
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
client = openai.OpenAI(api_key=api_key)
|
|
83
|
+
try:
|
|
84
|
+
# Make an http request to OpenAI to make sure it works
|
|
85
|
+
client.models.list()
|
|
86
|
+
except openai.AuthenticationError as e:
|
|
87
|
+
self.log.warning(
|
|
88
|
+
"Invalid OpenAI API key provided.",
|
|
89
|
+
exc_info=e,
|
|
90
|
+
)
|
|
91
|
+
self.last_error = CompletionError.from_exception(
|
|
92
|
+
e,
|
|
93
|
+
hint="You're missing the OPENAI_API_KEY environment variable. Run the following code in your terminal to set the environment variable and then relaunch the jupyter server `export OPENAI_API_KEY=<your-api-key>`",
|
|
94
|
+
)
|
|
95
|
+
return None
|
|
96
|
+
except openai.PermissionDeniedError as e:
|
|
97
|
+
self.log.warning(
|
|
98
|
+
"Invalid OpenAI API key provided.",
|
|
99
|
+
exc_info=e,
|
|
100
|
+
)
|
|
101
|
+
self.last_error = CompletionError.from_exception(e)
|
|
102
|
+
return None
|
|
103
|
+
except openai.InternalServerError as e:
|
|
104
|
+
self.log.debug(
|
|
105
|
+
"Unable to get OpenAI models due to OpenAI error.", exc_info=e
|
|
106
|
+
)
|
|
107
|
+
return api_key
|
|
108
|
+
except openai.RateLimitError as e:
|
|
109
|
+
self.log.debug(
|
|
110
|
+
"Unable to get OpenAI models due to rate limit error.", exc_info=e
|
|
111
|
+
)
|
|
112
|
+
return api_key
|
|
113
|
+
except openai.APIConnectionError as e:
|
|
114
|
+
self.log.warning(
|
|
115
|
+
"Unable to connect to OpenAI API.",
|
|
116
|
+
exec_info=e,
|
|
117
|
+
)
|
|
118
|
+
self.last_error = CompletionError.from_exception(e)
|
|
119
|
+
return None
|
|
120
|
+
else:
|
|
121
|
+
self.log.debug("User OpenAI API key validated.")
|
|
122
|
+
return api_key
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def capabilities(self) -> AICapabilities:
|
|
126
|
+
"""Get the provider capabilities."""
|
|
127
|
+
|
|
128
|
+
if is_azure_openai_configured():
|
|
129
|
+
return AICapabilities(
|
|
130
|
+
configuration={
|
|
131
|
+
"model": constants.AZURE_OPENAI_MODEL
|
|
132
|
+
},
|
|
133
|
+
provider="Azure OpenAI",
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
if constants.OLLAMA_MODEL and not self.api_key:
|
|
137
|
+
return AICapabilities(
|
|
138
|
+
configuration={
|
|
139
|
+
"model": constants.OLLAMA_MODEL
|
|
140
|
+
},
|
|
141
|
+
provider="Ollama",
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
if self.api_key:
|
|
145
|
+
self._validate_api_key(self.api_key)
|
|
146
|
+
|
|
147
|
+
return AICapabilities(
|
|
148
|
+
configuration={
|
|
149
|
+
"model": OPENAI_MODEL_FALLBACK,
|
|
150
|
+
},
|
|
151
|
+
provider="OpenAI (user key)",
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
check_mito_server_quota(MessageType.CHAT)
|
|
156
|
+
except Exception as e:
|
|
157
|
+
self.log.warning("Failed to set first usage date in user.json", exc_info=e)
|
|
158
|
+
self.last_error = CompletionError.from_exception(e)
|
|
159
|
+
|
|
160
|
+
return AICapabilities(
|
|
161
|
+
configuration={
|
|
162
|
+
"model": OPENAI_MODEL_FALLBACK,
|
|
163
|
+
},
|
|
164
|
+
provider="Mito server",
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def _active_async_client(self) -> Optional[openai.AsyncOpenAI]:
|
|
169
|
+
if not self._async_client or self._async_client.is_closed():
|
|
170
|
+
self._async_client = self._build_openai_client()
|
|
171
|
+
return self._async_client
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def key_type(self) -> str:
|
|
176
|
+
"""Returns the authentication key type being used."""
|
|
177
|
+
|
|
178
|
+
if self.api_key:
|
|
179
|
+
return USER_KEY
|
|
180
|
+
|
|
181
|
+
if constants.OLLAMA_MODEL:
|
|
182
|
+
return "ollama"
|
|
183
|
+
|
|
184
|
+
return MITO_SERVER_KEY
|
|
185
|
+
|
|
186
|
+
def _build_openai_client(self) -> Optional[Union[openai.AsyncOpenAI, openai.AsyncAzureOpenAI]]:
|
|
187
|
+
base_url = None
|
|
188
|
+
llm_api_key = None
|
|
189
|
+
|
|
190
|
+
if is_azure_openai_configured():
|
|
191
|
+
self.log.debug(f"Using Azure OpenAI with model: {constants.AZURE_OPENAI_MODEL}")
|
|
192
|
+
|
|
193
|
+
# The format for using Azure OpenAI is different than using
|
|
194
|
+
# other providers, so we have a special case for it here.
|
|
195
|
+
# Create Azure OpenAI client with explicit arguments
|
|
196
|
+
return openai.AsyncAzureOpenAI(
|
|
197
|
+
api_key=constants.AZURE_OPENAI_API_KEY,
|
|
198
|
+
api_version=constants.AZURE_OPENAI_API_VERSION,
|
|
199
|
+
azure_endpoint=constants.AZURE_OPENAI_ENDPOINT, # type: ignore
|
|
200
|
+
max_retries=self.max_retries,
|
|
201
|
+
timeout=self.timeout,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
elif constants.OLLAMA_MODEL and not self.api_key:
|
|
205
|
+
base_url = constants.OLLAMA_BASE_URL
|
|
206
|
+
llm_api_key = "ollama"
|
|
207
|
+
self.log.debug(f"Using Ollama with model: {constants.OLLAMA_MODEL}")
|
|
208
|
+
elif self.api_key:
|
|
209
|
+
llm_api_key = self.api_key
|
|
210
|
+
self.log.debug("Using OpenAI with user-provided API key")
|
|
211
|
+
else:
|
|
212
|
+
self.log.warning("No valid API key or model configuration provided")
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
# Create the client with explicit arguments to satisfy type checking
|
|
216
|
+
client = openai.AsyncOpenAI(
|
|
217
|
+
api_key=llm_api_key,
|
|
218
|
+
max_retries=self.max_retries,
|
|
219
|
+
timeout=self.timeout,
|
|
220
|
+
base_url=base_url if base_url else None,
|
|
221
|
+
)
|
|
222
|
+
return client
|
|
223
|
+
|
|
224
|
+
def _adjust_model_for_azure_or_ollama(self, model: str) -> str:
|
|
225
|
+
|
|
226
|
+
# If they have set an Azure OpenAI model, then we always use it
|
|
227
|
+
if is_azure_openai_configured() and constants.AZURE_OPENAI_MODEL is not None:
|
|
228
|
+
self.log.debug(f"Resolving to Azure OpenAI model: {constants.AZURE_OPENAI_MODEL}")
|
|
229
|
+
return constants.AZURE_OPENAI_MODEL
|
|
230
|
+
|
|
231
|
+
# If they have set an Ollama model, then we use it
|
|
232
|
+
if constants.OLLAMA_MODEL is not None:
|
|
233
|
+
return constants.OLLAMA_MODEL
|
|
234
|
+
|
|
235
|
+
# Otherwise, we use the model they provided
|
|
236
|
+
return model
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
async def request_completions(
|
|
240
|
+
self,
|
|
241
|
+
message_type: MessageType,
|
|
242
|
+
messages: List[ChatCompletionMessageParam],
|
|
243
|
+
model: str,
|
|
244
|
+
response_format_info: Optional[ResponseFormatInfo] = None,
|
|
245
|
+
) -> str:
|
|
246
|
+
"""
|
|
247
|
+
Request completions from the OpenAI API.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
message_type: The type of message to request completions for.
|
|
251
|
+
messages: The messages to request completions for.
|
|
252
|
+
model: The model to request completions for.
|
|
253
|
+
Returns:
|
|
254
|
+
The completion from the OpenAI API.
|
|
255
|
+
"""
|
|
256
|
+
# Reset the last error
|
|
257
|
+
self.last_error = None
|
|
258
|
+
completion = None
|
|
259
|
+
|
|
260
|
+
# Note: We don't catch exceptions here because we want them to bubble up
|
|
261
|
+
# to the providers file so we can handle all client exceptions in one place.
|
|
262
|
+
|
|
263
|
+
# Handle other providers as before
|
|
264
|
+
completion_function_params = get_open_ai_completion_function_params(
|
|
265
|
+
message_type, model, messages, False, response_format_info
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# If they have set an Azure OpenAI or Ollama model, then we use it
|
|
269
|
+
completion_function_params["model"] = self._adjust_model_for_azure_or_ollama(completion_function_params["model"])
|
|
270
|
+
|
|
271
|
+
if self._active_async_client is not None:
|
|
272
|
+
response = await self._active_async_client.chat.completions.create(**completion_function_params)
|
|
273
|
+
completion = response.choices[0].message.content or ""
|
|
274
|
+
else:
|
|
275
|
+
last_message_content = str(messages[-1].get("content", "")) if messages else None
|
|
276
|
+
completion = await get_ai_completion_from_mito_server(
|
|
277
|
+
last_message_content,
|
|
278
|
+
completion_function_params,
|
|
279
|
+
self.timeout,
|
|
280
|
+
self.max_retries,
|
|
281
|
+
message_type,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
return completion
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
async def stream_completions(
|
|
288
|
+
self,
|
|
289
|
+
message_type: MessageType,
|
|
290
|
+
messages: List[ChatCompletionMessageParam],
|
|
291
|
+
model: str,
|
|
292
|
+
message_id: str,
|
|
293
|
+
thread_id: str,
|
|
294
|
+
reply_fn: Callable[[Union[CompletionReply, CompletionStreamChunk]], None],
|
|
295
|
+
user_input: Optional[str] = None,
|
|
296
|
+
response_format_info: Optional[ResponseFormatInfo] = None
|
|
297
|
+
) -> str:
|
|
298
|
+
"""
|
|
299
|
+
Stream completions from the OpenAI API and return the accumulated response.
|
|
300
|
+
Returns: The accumulated response string.
|
|
301
|
+
"""
|
|
302
|
+
# Reset the last error
|
|
303
|
+
self.last_error = None
|
|
304
|
+
accumulated_response = ""
|
|
305
|
+
|
|
306
|
+
# Send initial acknowledgment
|
|
307
|
+
reply_fn(CompletionReply(
|
|
308
|
+
items=[
|
|
309
|
+
CompletionItem(content="", isIncomplete=True, token=message_id)
|
|
310
|
+
],
|
|
311
|
+
parent_id=message_id,
|
|
312
|
+
))
|
|
313
|
+
|
|
314
|
+
# Handle other providers as before
|
|
315
|
+
completion_function_params = get_open_ai_completion_function_params(
|
|
316
|
+
message_type, model, messages, True, response_format_info
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
completion_function_params["model"] = self._adjust_model_for_azure_or_ollama(completion_function_params["model"])
|
|
320
|
+
|
|
321
|
+
try:
|
|
322
|
+
if self._active_async_client is not None:
|
|
323
|
+
# Stream from OpenAI
|
|
324
|
+
client = self._active_async_client
|
|
325
|
+
if client is None:
|
|
326
|
+
raise ValueError("OpenAI client not initialized")
|
|
327
|
+
|
|
328
|
+
stream = await client.chat.completions.create(**completion_function_params)
|
|
329
|
+
|
|
330
|
+
async for chunk in stream:
|
|
331
|
+
if len(chunk.choices) == 0:
|
|
332
|
+
continue
|
|
333
|
+
|
|
334
|
+
is_finished = chunk.choices[0].finish_reason is not None
|
|
335
|
+
content = chunk.choices[0].delta.content or ""
|
|
336
|
+
accumulated_response += content
|
|
337
|
+
|
|
338
|
+
reply_fn(CompletionStreamChunk(
|
|
339
|
+
parent_id=message_id,
|
|
340
|
+
chunk=CompletionItem(
|
|
341
|
+
content=content,
|
|
342
|
+
isIncomplete=True,
|
|
343
|
+
token=message_id,
|
|
344
|
+
),
|
|
345
|
+
done=is_finished,
|
|
346
|
+
))
|
|
347
|
+
else:
|
|
348
|
+
# Stream from Mito server
|
|
349
|
+
last_message_content = str(messages[-1].get("content", "")) if messages else ""
|
|
350
|
+
async for chunk_from_mito_server in stream_ai_completion_from_mito_server(
|
|
351
|
+
last_message_content,
|
|
352
|
+
completion_function_params,
|
|
353
|
+
self.timeout,
|
|
354
|
+
self.max_retries,
|
|
355
|
+
message_type,
|
|
356
|
+
reply_fn=reply_fn,
|
|
357
|
+
message_id=message_id,
|
|
358
|
+
):
|
|
359
|
+
accumulated_response += str(chunk_from_mito_server)
|
|
360
|
+
|
|
361
|
+
# Update quota after streaming is complete
|
|
362
|
+
update_mito_server_quota(message_type)
|
|
363
|
+
|
|
364
|
+
return accumulated_response
|
|
365
|
+
|
|
366
|
+
except BaseException as e:
|
|
367
|
+
self.last_error = CompletionError.from_exception(e)
|
|
368
|
+
# Send error message to client before raising
|
|
369
|
+
reply_fn(CompletionStreamChunk(
|
|
370
|
+
parent_id=message_id,
|
|
371
|
+
chunk=CompletionItem(
|
|
372
|
+
content="",
|
|
373
|
+
isIncomplete=True,
|
|
374
|
+
error=CompletionItemError(
|
|
375
|
+
message=f"Failed to process completion: {e!r}"
|
|
376
|
+
),
|
|
377
|
+
token=message_id,
|
|
378
|
+
),
|
|
379
|
+
done=True,
|
|
380
|
+
error=CompletionError.from_exception(e),
|
|
381
|
+
))
|
|
382
|
+
raise
|
mito_ai/path_utils.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
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 NewType
|
|
5
|
+
import os
|
|
6
|
+
from mito_ai.utils.error_classes import StreamlitPreviewError
|
|
7
|
+
|
|
8
|
+
# Type definitions for better type safety
|
|
9
|
+
AbsoluteNotebookPath = NewType('AbsoluteNotebookPath', str)
|
|
10
|
+
AbsoluteNotebookDirPath = NewType('AbsoluteNotebookDirPath', str)
|
|
11
|
+
AbsoluteAppPath = NewType('AbsoluteAppPath', str)
|
|
12
|
+
AppFileName = NewType("AppFileName", str)
|
|
13
|
+
|
|
14
|
+
def get_absolute_notebook_path(notebook_path: str) -> AbsoluteNotebookPath:
|
|
15
|
+
"""
|
|
16
|
+
Convert any notebook path to an absolute path.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
notebook_path: Path to the notebook (can be relative or absolute)
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
AbsoluteNotebookPath: The absolute path to the notebook
|
|
23
|
+
|
|
24
|
+
Raises:
|
|
25
|
+
ValueError: If the path is invalid or empty
|
|
26
|
+
"""
|
|
27
|
+
if not notebook_path or not notebook_path.strip():
|
|
28
|
+
raise StreamlitPreviewError("Notebook path cannot be empty", 400)
|
|
29
|
+
|
|
30
|
+
absolute_path = os.path.abspath(notebook_path)
|
|
31
|
+
return AbsoluteNotebookPath(absolute_path)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_absolute_notebook_dir_path(notebook_path: AbsoluteNotebookPath) -> AbsoluteNotebookDirPath:
|
|
35
|
+
"""
|
|
36
|
+
Get the absolute directory containing the notebook.
|
|
37
|
+
"""
|
|
38
|
+
return AbsoluteNotebookDirPath(os.path.dirname(notebook_path))
|
|
39
|
+
|
|
40
|
+
def get_absolute_app_path(app_directory: AbsoluteNotebookDirPath, app_file_name: AppFileName) -> AbsoluteAppPath:
|
|
41
|
+
"""
|
|
42
|
+
Get the absolute path to the app
|
|
43
|
+
"""
|
|
44
|
+
return AbsoluteAppPath(os.path.join(app_directory, app_file_name))
|
|
45
|
+
|
|
46
|
+
def get_app_file_name(notebook_id: str) -> AppFileName:
|
|
47
|
+
"""
|
|
48
|
+
Converts the notebook id into the corresponding app id
|
|
49
|
+
"""
|
|
50
|
+
mito_app_name = notebook_id.replace('mito-notebook-', 'mito-app-')
|
|
51
|
+
return AppFileName(f'{mito_app_name}.py')
|
|
52
|
+
|
|
53
|
+
def does_app_path_exist(app_path: AbsoluteAppPath) -> bool:
|
|
54
|
+
"""
|
|
55
|
+
Check if the app file exists
|
|
56
|
+
"""
|
|
57
|
+
return os.path.exists(app_path)
|
|
58
|
+
|
|
59
|
+
def does_notebook_id_have_corresponding_app(notebook_id: str, notebook_path: str) -> bool:
|
|
60
|
+
"""
|
|
61
|
+
Given a notebook_id and raw notebook_path checks if the notebook has a corresponding
|
|
62
|
+
app by converting the notebook_path into an absolute path and converting the notebook_id
|
|
63
|
+
into an app name
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
app_file_name = get_app_file_name(notebook_id)
|
|
67
|
+
notebook_path = get_absolute_notebook_path(notebook_path)
|
|
68
|
+
app_directory = get_absolute_notebook_dir_path(notebook_path)
|
|
69
|
+
app_path = get_absolute_app_path(app_directory, app_file_name)
|
|
70
|
+
return does_app_path_exist(app_path)
|
|
@@ -0,0 +1,44 @@
|
|
|
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, Union
|
|
7
|
+
import tornado
|
|
8
|
+
import os
|
|
9
|
+
from jupyter_server.base.handlers import APIHandler
|
|
10
|
+
from mito_ai.rules.utils import RULES_DIR_PATH, get_all_rules, get_rule, set_rules_file
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RulesHandler(APIHandler):
|
|
14
|
+
"""Handler for operations on a specific setting"""
|
|
15
|
+
|
|
16
|
+
@tornado.web.authenticated
|
|
17
|
+
def get(self, key: Union[str, None] = None) -> None:
|
|
18
|
+
"""Get a specific rule by key or all rules if no key provided"""
|
|
19
|
+
if key is None or key == '':
|
|
20
|
+
# No key provided, return all rules
|
|
21
|
+
rules = get_all_rules()
|
|
22
|
+
self.finish(json.dumps(rules))
|
|
23
|
+
else:
|
|
24
|
+
# Key provided, return specific rule
|
|
25
|
+
rule_content = get_rule(key)
|
|
26
|
+
if rule_content is None:
|
|
27
|
+
self.set_status(404)
|
|
28
|
+
self.finish(json.dumps({"error": f"Rule with key '{key}' not found"}))
|
|
29
|
+
else:
|
|
30
|
+
self.finish(json.dumps({"key": key, "content": rule_content}))
|
|
31
|
+
|
|
32
|
+
@tornado.web.authenticated
|
|
33
|
+
def put(self, key: str) -> None:
|
|
34
|
+
"""Update or create a specific setting"""
|
|
35
|
+
data = json.loads(self.request.body)
|
|
36
|
+
if 'content' not in data:
|
|
37
|
+
self.set_status(400)
|
|
38
|
+
self.finish(json.dumps({"error": "Content is required"}))
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
set_rules_file(key, data['content'])
|
|
42
|
+
self.finish(json.dumps({"status": "updated", "rules file ": key}))
|
|
43
|
+
|
|
44
|
+
|
mito_ai/rules/urls.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
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.rules.handlers import RulesHandler
|
|
7
|
+
|
|
8
|
+
def get_rules_urls(base_url: str) -> List[Tuple[str, Any, dict]]:
|
|
9
|
+
"""Get all rules 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, "rules"), RulesHandler, {}),
|
|
21
|
+
(url_path_join(BASE_URL, "rules/(.+)"), RulesHandler, {}),
|
|
22
|
+
]
|
mito_ai/rules/utils.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
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, Final, List, Optional
|
|
5
|
+
import os
|
|
6
|
+
from mito_ai.utils.schema import MITO_FOLDER
|
|
7
|
+
|
|
8
|
+
RULES_DIR_PATH: Final[str] = os.path.join(MITO_FOLDER, 'rules')
|
|
9
|
+
|
|
10
|
+
def set_rules_file(rule_name: str, value: Any) -> None:
|
|
11
|
+
"""
|
|
12
|
+
Updates the value of a specific rule file in the rules directory
|
|
13
|
+
"""
|
|
14
|
+
# Ensure the directory exists
|
|
15
|
+
if not os.path.exists(RULES_DIR_PATH):
|
|
16
|
+
os.makedirs(RULES_DIR_PATH)
|
|
17
|
+
|
|
18
|
+
# Create the file path to the rule name as a .md file
|
|
19
|
+
file_path = os.path.join(RULES_DIR_PATH, f"{rule_name}.md")
|
|
20
|
+
|
|
21
|
+
with open(file_path, 'w+') as f:
|
|
22
|
+
f.write(value)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_rule(rule_name: str) -> Optional[str]:
|
|
26
|
+
"""
|
|
27
|
+
Retrieves the value of a specific rule file from the rules directory
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
if rule_name.endswith('.md'):
|
|
31
|
+
rule_name = rule_name[:-3]
|
|
32
|
+
|
|
33
|
+
file_path = os.path.join(RULES_DIR_PATH, f"{rule_name}.md")
|
|
34
|
+
|
|
35
|
+
if not os.path.exists(file_path):
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
with open(file_path, 'r') as f:
|
|
39
|
+
return f.read()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_all_rules() -> List[str]:
|
|
43
|
+
"""
|
|
44
|
+
Retrieves all rule files from the rules directory
|
|
45
|
+
"""
|
|
46
|
+
# Ensure the directory exists
|
|
47
|
+
if not os.path.exists(RULES_DIR_PATH):
|
|
48
|
+
os.makedirs(RULES_DIR_PATH)
|
|
49
|
+
return [] # Return empty list if directory didn't exist
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
return [f for f in os.listdir(RULES_DIR_PATH) if f.endswith('.md')]
|
|
53
|
+
except OSError as e:
|
|
54
|
+
# Log the error if needed and return empty list
|
|
55
|
+
print(f"Error reading rules directory: {e}")
|
|
56
|
+
return []
|
|
@@ -0,0 +1,41 @@
|
|
|
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 jupyter_server.base.handlers import APIHandler
|
|
7
|
+
from mito_ai.settings.utils import (
|
|
8
|
+
get_settings_field,
|
|
9
|
+
set_settings_field,
|
|
10
|
+
ensure_settings_file_exists,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SettingsHandler(APIHandler):
|
|
15
|
+
"""Handler for operations on a specific setting"""
|
|
16
|
+
|
|
17
|
+
@tornado.web.authenticated
|
|
18
|
+
@ensure_settings_file_exists
|
|
19
|
+
def get(self, key: str) -> None:
|
|
20
|
+
"""Get a specific setting by key"""
|
|
21
|
+
setting_value = get_settings_field(key)
|
|
22
|
+
if setting_value is None:
|
|
23
|
+
self.set_status(404)
|
|
24
|
+
self.finish(json.dumps({"error": f"Setting with key '{key}' not found"}))
|
|
25
|
+
else:
|
|
26
|
+
self.finish(json.dumps({"key": key, "value": setting_value}))
|
|
27
|
+
|
|
28
|
+
@tornado.web.authenticated
|
|
29
|
+
@ensure_settings_file_exists
|
|
30
|
+
def put(self, key: str) -> None:
|
|
31
|
+
"""Update or create a specific setting"""
|
|
32
|
+
data = json.loads(self.request.body)
|
|
33
|
+
if "value" not in data:
|
|
34
|
+
self.set_status(400)
|
|
35
|
+
self.finish(json.dumps({"error": "Value is required"}))
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
set_settings_field(key, data["value"])
|
|
39
|
+
self.finish(
|
|
40
|
+
json.dumps({"status": "updated", "key": key, "value": data["value"]})
|
|
41
|
+
)
|
mito_ai/settings/urls.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
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.settings.handlers import SettingsHandler
|
|
7
|
+
|
|
8
|
+
def get_settings_urls(base_url: str) -> List[Tuple[str, Any, dict]]:
|
|
9
|
+
"""Get all settings 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
|
+
return [
|
|
19
|
+
(url_path_join(BASE_URL, "settings/(.*)"), SettingsHandler, {}),
|
|
20
|
+
]
|
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
from typing import Any, Final, Callable
|
|
7
|
+
from functools import wraps
|
|
8
|
+
from mito_ai.utils.schema import MITO_FOLDER
|
|
9
|
+
|
|
10
|
+
SETTINGS_PATH: Final[str] = os.path.join(MITO_FOLDER, "settings.json")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def set_settings_field(field: str, value: Any) -> None:
|
|
14
|
+
"""
|
|
15
|
+
Updates the value of a specific field in settings.json
|
|
16
|
+
"""
|
|
17
|
+
with open(SETTINGS_PATH, "r") as user_file_old:
|
|
18
|
+
old_user_json = json.load(user_file_old)
|
|
19
|
+
old_user_json[field] = value
|
|
20
|
+
with open(SETTINGS_PATH, "w+") as f:
|
|
21
|
+
f.write(json.dumps(old_user_json))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_settings_field(field: str) -> Any:
|
|
25
|
+
"""
|
|
26
|
+
Retrieves the value of a specific field from settings.json
|
|
27
|
+
"""
|
|
28
|
+
with open(SETTINGS_PATH, "r") as user_file_old:
|
|
29
|
+
return json.load(user_file_old).get(field)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def ensure_settings_file_exists(method: Callable) -> Callable:
|
|
33
|
+
"""Decorator to ensure the settings.json file exists before executing the handler method."""
|
|
34
|
+
|
|
35
|
+
@wraps(method)
|
|
36
|
+
def wrapper(self: Any, *args: Any, **kwargs: Any) -> Any:
|
|
37
|
+
if not os.path.exists(SETTINGS_PATH):
|
|
38
|
+
with open(SETTINGS_PATH, "w") as f:
|
|
39
|
+
json.dump({}, f, indent=4)
|
|
40
|
+
return method(self, *args, **kwargs)
|
|
41
|
+
|
|
42
|
+
return wrapper
|