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,84 @@
|
|
|
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, Optional, Dict
|
|
5
|
+
from mito_ai.rules.utils import get_rule
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_rules_str(additional_context: Optional[List[Dict[str, str]]]) -> str:
|
|
9
|
+
"""
|
|
10
|
+
Extract the rules from the additional context array, and retrieve the rule content.
|
|
11
|
+
"""
|
|
12
|
+
if not additional_context:
|
|
13
|
+
return ""
|
|
14
|
+
|
|
15
|
+
selected_rules = [context["value"] for context in additional_context if context.get("type") == "rule"]
|
|
16
|
+
if len(selected_rules) == 0:
|
|
17
|
+
return ""
|
|
18
|
+
|
|
19
|
+
rules_str = ""
|
|
20
|
+
for rule in selected_rules:
|
|
21
|
+
rule_content = get_rule(rule)
|
|
22
|
+
if rule_content is None or rule_content == "":
|
|
23
|
+
continue
|
|
24
|
+
|
|
25
|
+
rules_str += f"===========\n\nCustom Instructions Provided by User: {rule}\n\n{rule_content}\n\n==========="
|
|
26
|
+
|
|
27
|
+
return rules_str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_selected_context_str(additional_context: Optional[List[Dict[str, str]]]) -> str:
|
|
31
|
+
"""
|
|
32
|
+
Get the selected context from the additional context array.
|
|
33
|
+
"""
|
|
34
|
+
if not additional_context:
|
|
35
|
+
return ""
|
|
36
|
+
|
|
37
|
+
# STEP 1: Extract each context type into a separate list
|
|
38
|
+
selected_variables = [context["value"] for context in additional_context if context.get("type") == "variable"]
|
|
39
|
+
selected_files = [context["value"] for context in additional_context if context.get("type") == "file"]
|
|
40
|
+
selected_db_connections = [context["value"] for context in additional_context if context.get("type") == "db"]
|
|
41
|
+
selected_images = [context["value"] for context in additional_context if context.get("type", "").startswith("image/")]
|
|
42
|
+
|
|
43
|
+
# STEP 2: Create a list of strings (instructions) for each context type
|
|
44
|
+
context_parts = []
|
|
45
|
+
|
|
46
|
+
if len(selected_variables) > 0:
|
|
47
|
+
context_parts.append(
|
|
48
|
+
"The following variables have been selected by the user to be used in the task:\n"
|
|
49
|
+
+ "\n".join(selected_variables)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
if len(selected_files) > 0:
|
|
53
|
+
context_parts.append(
|
|
54
|
+
"The following files have been selected by the user to be used in the task:\n"
|
|
55
|
+
+ "\n".join(selected_files)
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if len(selected_db_connections) > 0:
|
|
59
|
+
context_parts.append(
|
|
60
|
+
"The following database connections have been selected by the user to be used in the task:\n"
|
|
61
|
+
+ "\n".join(selected_db_connections)
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if len(selected_images) > 0:
|
|
65
|
+
context_parts.append(
|
|
66
|
+
"The following images have been selected by the user to be used in the task:\n"
|
|
67
|
+
+ "\n".join(selected_images)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# STEP 3: Combine into a single string
|
|
71
|
+
return "\n\n".join(context_parts)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_streamlit_app_status_str(notebook_id: str, notebook_path: str) -> str:
|
|
75
|
+
"""
|
|
76
|
+
Get the streamlit app status string.
|
|
77
|
+
"""
|
|
78
|
+
from mito_ai.path_utils import does_notebook_id_have_corresponding_app
|
|
79
|
+
if does_notebook_id_have_corresponding_app(notebook_id, notebook_path):
|
|
80
|
+
return "The notebook has an existing Streamlit app that you can edit"
|
|
81
|
+
return "The notebook does not have an existing Streamlit app. If you want to show an app to the user, you must create a new one."
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
|
|
@@ -0,0 +1,284 @@
|
|
|
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
|
+
import asyncio
|
|
6
|
+
from typing import Any, Callable, Dict, List, Optional, Union, cast
|
|
7
|
+
from mito_ai import constants
|
|
8
|
+
from openai.types.chat import ChatCompletionMessageParam
|
|
9
|
+
from traitlets import Instance, Unicode, default, validate
|
|
10
|
+
from traitlets.config import LoggingConfigurable
|
|
11
|
+
from openai.types.chat import ChatCompletionMessageParam
|
|
12
|
+
|
|
13
|
+
from mito_ai import constants
|
|
14
|
+
from mito_ai.enterprise.utils import is_azure_openai_configured
|
|
15
|
+
from mito_ai.gemini_client import GeminiClient
|
|
16
|
+
from mito_ai.openai_client import OpenAIClient
|
|
17
|
+
from mito_ai.anthropic_client import AnthropicClient
|
|
18
|
+
from mito_ai.logger import get_logger
|
|
19
|
+
from mito_ai.completions.models import (
|
|
20
|
+
AICapabilities,
|
|
21
|
+
CompletionError,
|
|
22
|
+
CompletionItem,
|
|
23
|
+
CompletionItemError,
|
|
24
|
+
CompletionReply,
|
|
25
|
+
CompletionStreamChunk,
|
|
26
|
+
MessageType,
|
|
27
|
+
ResponseFormatInfo, CompletionItemError,
|
|
28
|
+
)
|
|
29
|
+
from mito_ai.utils.telemetry_utils import (
|
|
30
|
+
KEY_TYPE_PARAM,
|
|
31
|
+
MITO_AI_COMPLETION_ERROR,
|
|
32
|
+
MITO_AI_COMPLETION_RETRY,
|
|
33
|
+
MITO_SERVER_KEY,
|
|
34
|
+
USER_KEY,
|
|
35
|
+
log,
|
|
36
|
+
log_ai_completion_error,
|
|
37
|
+
log_ai_completion_retry,
|
|
38
|
+
log_ai_completion_success,
|
|
39
|
+
)
|
|
40
|
+
from mito_ai.utils.provider_utils import get_model_provider
|
|
41
|
+
from mito_ai.utils.mito_server_utils import ProviderCompletionException
|
|
42
|
+
|
|
43
|
+
__all__ = ["OpenAIProvider"]
|
|
44
|
+
|
|
45
|
+
class OpenAIProvider(LoggingConfigurable):
|
|
46
|
+
"""Provide AI feature through OpenAI services."""
|
|
47
|
+
|
|
48
|
+
api_key = Unicode(
|
|
49
|
+
config=True,
|
|
50
|
+
allow_none=True,
|
|
51
|
+
help="OpenAI API key. Default value is read from the OPENAI_API_KEY environment variable.",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
last_error = Instance(
|
|
55
|
+
CompletionError,
|
|
56
|
+
allow_none=True,
|
|
57
|
+
help="""Last error encountered when using the OpenAI provider.
|
|
58
|
+
|
|
59
|
+
This attribute is observed by the websocket provider to push the error to the client.""",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def __init__(self, **kwargs: Dict[str, Any]) -> None:
|
|
63
|
+
config = kwargs.get('config', {})
|
|
64
|
+
if 'api_key' in kwargs:
|
|
65
|
+
config['OpenAIClient'] = {'api_key': kwargs['api_key']}
|
|
66
|
+
kwargs['config'] = config
|
|
67
|
+
|
|
68
|
+
super().__init__(log=get_logger(), **kwargs)
|
|
69
|
+
self.last_error = None
|
|
70
|
+
self._openai_client: Optional[OpenAIClient] = OpenAIClient(**config)
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def capabilities(self) -> AICapabilities:
|
|
74
|
+
"""
|
|
75
|
+
Returns the capabilities of the AI provider.
|
|
76
|
+
"""
|
|
77
|
+
if constants.CLAUDE_API_KEY and not self.api_key:
|
|
78
|
+
return AICapabilities(
|
|
79
|
+
configuration={"model": "<dynamic>"},
|
|
80
|
+
provider="Claude",
|
|
81
|
+
)
|
|
82
|
+
if constants.GEMINI_API_KEY and not self.api_key:
|
|
83
|
+
return AICapabilities(
|
|
84
|
+
configuration={"model": "<dynamic>"},
|
|
85
|
+
provider="Gemini",
|
|
86
|
+
)
|
|
87
|
+
if self._openai_client:
|
|
88
|
+
return self._openai_client.capabilities
|
|
89
|
+
|
|
90
|
+
return AICapabilities(
|
|
91
|
+
configuration={"model": "<dynamic>"},
|
|
92
|
+
provider="Mito server",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def key_type(self) -> str:
|
|
97
|
+
if constants.CLAUDE_API_KEY and not self.api_key:
|
|
98
|
+
return "claude"
|
|
99
|
+
if constants.GEMINI_API_KEY and not self.api_key:
|
|
100
|
+
return "gemini"
|
|
101
|
+
if self._openai_client:
|
|
102
|
+
return self._openai_client.key_type
|
|
103
|
+
return MITO_SERVER_KEY
|
|
104
|
+
|
|
105
|
+
async def request_completions(
|
|
106
|
+
self,
|
|
107
|
+
message_type: MessageType,
|
|
108
|
+
messages: List[ChatCompletionMessageParam],
|
|
109
|
+
model: str,
|
|
110
|
+
response_format_info: Optional[ResponseFormatInfo] = None,
|
|
111
|
+
user_input: Optional[str] = None,
|
|
112
|
+
thread_id: Optional[str] = None,
|
|
113
|
+
max_retries: int = 3
|
|
114
|
+
) -> str:
|
|
115
|
+
"""
|
|
116
|
+
Request completions from the AI provider.
|
|
117
|
+
"""
|
|
118
|
+
self.last_error = None
|
|
119
|
+
completion = None
|
|
120
|
+
last_message_content = str(messages[-1].get('content', '')) if messages else ""
|
|
121
|
+
model_type = get_model_provider(model)
|
|
122
|
+
|
|
123
|
+
# Retry loop
|
|
124
|
+
for attempt in range(max_retries + 1):
|
|
125
|
+
try:
|
|
126
|
+
if model_type == "claude":
|
|
127
|
+
api_key = constants.CLAUDE_API_KEY
|
|
128
|
+
anthropic_client = AnthropicClient(api_key=api_key)
|
|
129
|
+
completion = await anthropic_client.request_completions(messages, model, response_format_info, message_type)
|
|
130
|
+
elif model_type == "gemini":
|
|
131
|
+
api_key = constants.GEMINI_API_KEY
|
|
132
|
+
gemini_client = GeminiClient(api_key=api_key)
|
|
133
|
+
messages_for_gemini = [dict(m) for m in messages]
|
|
134
|
+
completion = await gemini_client.request_completions(messages_for_gemini, model, response_format_info, message_type)
|
|
135
|
+
elif model_type == "openai":
|
|
136
|
+
if not self._openai_client:
|
|
137
|
+
raise RuntimeError("OpenAI client is not initialized.")
|
|
138
|
+
completion = await self._openai_client.request_completions(
|
|
139
|
+
message_type=message_type,
|
|
140
|
+
messages=messages,
|
|
141
|
+
model=model,
|
|
142
|
+
response_format_info=response_format_info
|
|
143
|
+
)
|
|
144
|
+
else:
|
|
145
|
+
raise ValueError(f"No AI provider configured for model: {model}")
|
|
146
|
+
|
|
147
|
+
# Success! Log and return
|
|
148
|
+
log_ai_completion_success(
|
|
149
|
+
key_type=USER_KEY if self.key_type == "user" else MITO_SERVER_KEY,
|
|
150
|
+
message_type=message_type,
|
|
151
|
+
last_message_content=last_message_content,
|
|
152
|
+
response={"completion": completion},
|
|
153
|
+
user_input=user_input or "",
|
|
154
|
+
thread_id=thread_id or "",
|
|
155
|
+
model=model
|
|
156
|
+
)
|
|
157
|
+
return completion # type: ignore
|
|
158
|
+
|
|
159
|
+
except PermissionError as e:
|
|
160
|
+
# If we hit a free tier limit, then raise an exception right away without retrying.
|
|
161
|
+
self.log.exception(f"Error during request_completions: {e}")
|
|
162
|
+
self.last_error = CompletionError.from_exception(e)
|
|
163
|
+
log_ai_completion_error('user_key' if self.key_type != MITO_SERVER_KEY else 'mito_server_key', thread_id or "", message_type, e)
|
|
164
|
+
raise
|
|
165
|
+
|
|
166
|
+
except BaseException as e:
|
|
167
|
+
# Check if we should retry (not on the last attempt)
|
|
168
|
+
if attempt < max_retries:
|
|
169
|
+
# Exponential backoff: wait 2^attempt seconds
|
|
170
|
+
wait_time = 2 ** attempt
|
|
171
|
+
self.log.info(f"Retrying request_completions after {wait_time}s (attempt {attempt + 1}/{max_retries + 1}): {str(e)}")
|
|
172
|
+
log_ai_completion_retry('user_key' if self.key_type != MITO_SERVER_KEY else 'mito_server_key', thread_id or "", message_type, e)
|
|
173
|
+
await asyncio.sleep(wait_time)
|
|
174
|
+
continue
|
|
175
|
+
else:
|
|
176
|
+
# Final failure after all retries - set error state and raise
|
|
177
|
+
self.log.exception(f"Error during request_completions after {attempt + 1} attempts: {e}")
|
|
178
|
+
self.last_error = CompletionError.from_exception(e)
|
|
179
|
+
log_ai_completion_error('user_key' if self.key_type != MITO_SERVER_KEY else 'mito_server_key', thread_id or "", message_type, e)
|
|
180
|
+
raise
|
|
181
|
+
|
|
182
|
+
# This should never be reached due to the raise in the except block,
|
|
183
|
+
# but added to satisfy the linter
|
|
184
|
+
raise RuntimeError("Unexpected code path in request_completions")
|
|
185
|
+
|
|
186
|
+
async def stream_completions(
|
|
187
|
+
self,
|
|
188
|
+
message_type: MessageType,
|
|
189
|
+
messages: List[ChatCompletionMessageParam],
|
|
190
|
+
model: str,
|
|
191
|
+
message_id: str,
|
|
192
|
+
thread_id: str,
|
|
193
|
+
reply_fn: Callable[[Union[CompletionReply, CompletionStreamChunk]], None],
|
|
194
|
+
user_input: Optional[str] = None,
|
|
195
|
+
response_format_info: Optional[ResponseFormatInfo] = None
|
|
196
|
+
) -> str:
|
|
197
|
+
"""
|
|
198
|
+
Stream completions from the AI provider and return the accumulated response.
|
|
199
|
+
Returns: The accumulated response string.
|
|
200
|
+
"""
|
|
201
|
+
self.last_error = None
|
|
202
|
+
accumulated_response = ""
|
|
203
|
+
last_message_content = str(messages[-1].get('content', '')) if messages else ""
|
|
204
|
+
model_type = get_model_provider(model)
|
|
205
|
+
reply_fn(CompletionReply(
|
|
206
|
+
items=[
|
|
207
|
+
CompletionItem(content="", isIncomplete=True, token=message_id)
|
|
208
|
+
],
|
|
209
|
+
parent_id=message_id,
|
|
210
|
+
))
|
|
211
|
+
|
|
212
|
+
try:
|
|
213
|
+
if model_type == "claude":
|
|
214
|
+
api_key = constants.CLAUDE_API_KEY
|
|
215
|
+
anthropic_client = AnthropicClient(api_key=api_key)
|
|
216
|
+
accumulated_response = await anthropic_client.stream_completions(
|
|
217
|
+
messages=messages,
|
|
218
|
+
model=model,
|
|
219
|
+
message_type=message_type,
|
|
220
|
+
message_id=message_id,
|
|
221
|
+
reply_fn=reply_fn
|
|
222
|
+
)
|
|
223
|
+
elif model_type == "gemini":
|
|
224
|
+
api_key = constants.GEMINI_API_KEY
|
|
225
|
+
gemini_client = GeminiClient(api_key=api_key)
|
|
226
|
+
# TODO: We shouldn't need to do this because the messages should already be dictionaries...
|
|
227
|
+
# but if we do have to do some pre-processing, we should do it in the gemini_client instead.
|
|
228
|
+
messages_for_gemini = [dict(m) for m in messages]
|
|
229
|
+
accumulated_response = await gemini_client.stream_completions(
|
|
230
|
+
messages=messages_for_gemini,
|
|
231
|
+
model=model,
|
|
232
|
+
message_id=message_id,
|
|
233
|
+
reply_fn=reply_fn,
|
|
234
|
+
message_type=message_type
|
|
235
|
+
)
|
|
236
|
+
elif model_type == "openai":
|
|
237
|
+
if not self._openai_client:
|
|
238
|
+
raise RuntimeError("OpenAI client is not initialized.")
|
|
239
|
+
accumulated_response = await self._openai_client.stream_completions(
|
|
240
|
+
message_type=message_type,
|
|
241
|
+
messages=messages,
|
|
242
|
+
model=model,
|
|
243
|
+
message_id=message_id,
|
|
244
|
+
thread_id=thread_id,
|
|
245
|
+
reply_fn=reply_fn,
|
|
246
|
+
user_input=user_input,
|
|
247
|
+
response_format_info=response_format_info
|
|
248
|
+
)
|
|
249
|
+
else:
|
|
250
|
+
raise ValueError(f"No AI provider configured for model: {model}")
|
|
251
|
+
|
|
252
|
+
# Log the successful completion
|
|
253
|
+
log_ai_completion_success(
|
|
254
|
+
key_type=USER_KEY if self.key_type == "user" else MITO_SERVER_KEY,
|
|
255
|
+
message_type=message_type,
|
|
256
|
+
last_message_content=last_message_content,
|
|
257
|
+
response={"completion": accumulated_response},
|
|
258
|
+
user_input=user_input or "",
|
|
259
|
+
thread_id=thread_id,
|
|
260
|
+
model=model
|
|
261
|
+
)
|
|
262
|
+
return accumulated_response
|
|
263
|
+
|
|
264
|
+
except BaseException as e:
|
|
265
|
+
self.log.exception(f"Error during stream_completions: {e}")
|
|
266
|
+
self.last_error = CompletionError.from_exception(e)
|
|
267
|
+
log_ai_completion_error('user_key' if self.key_type != MITO_SERVER_KEY else 'mito_server_key', thread_id, message_type, e)
|
|
268
|
+
|
|
269
|
+
# Send error message to client before raising
|
|
270
|
+
reply_fn(CompletionStreamChunk(
|
|
271
|
+
parent_id=message_id,
|
|
272
|
+
chunk=CompletionItem(
|
|
273
|
+
content="",
|
|
274
|
+
isIncomplete=True,
|
|
275
|
+
error=CompletionItemError(
|
|
276
|
+
message=f"Failed to process completion: {e!r}"
|
|
277
|
+
),
|
|
278
|
+
token=message_id,
|
|
279
|
+
),
|
|
280
|
+
done=True,
|
|
281
|
+
error=CompletionError.from_exception(e),
|
|
282
|
+
))
|
|
283
|
+
raise
|
|
284
|
+
|
mito_ai/constants.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
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
|
+
from typing import Union
|
|
6
|
+
|
|
7
|
+
# Claude
|
|
8
|
+
CLAUDE_API_KEY = os.environ.get("CLAUDE_API_KEY")
|
|
9
|
+
|
|
10
|
+
# Gemini
|
|
11
|
+
GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
|
|
12
|
+
|
|
13
|
+
# Ollama
|
|
14
|
+
OLLAMA_MODEL = os.environ.get("OLLAMA_MODEL")
|
|
15
|
+
OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "http://localhost:11434/v1")
|
|
16
|
+
|
|
17
|
+
# OpenAI
|
|
18
|
+
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
|
|
19
|
+
|
|
20
|
+
# Azure OpenAI Config
|
|
21
|
+
AZURE_OPENAI_API_KEY = os.environ.get("AZURE_OPENAI_API_KEY")
|
|
22
|
+
AZURE_OPENAI_API_VERSION = os.environ.get("AZURE_OPENAI_API_VERSION")
|
|
23
|
+
AZURE_OPENAI_ENDPOINT = os.environ.get("AZURE_OPENAI_ENDPOINT")
|
|
24
|
+
AZURE_OPENAI_MODEL = os.environ.get("AZURE_OPENAI_MODEL")
|
|
25
|
+
|
|
26
|
+
# Mito AI Base URLs and Endpoint Paths
|
|
27
|
+
MITO_PROD_BASE_URL = "https://7eax4i53f5odkshhlry4gw23by0yvnuv.lambda-url.us-east-1.on.aws/v2"
|
|
28
|
+
MITO_DEV_BASE_URL = "https://g5vwmogjg7gh7aktqezyrvcq6a0hyfnr.lambda-url.us-east-1.on.aws/v2"
|
|
29
|
+
MITO_LOCAL_BASE_URL = "http://127.0.0.1:8000/v2" # When you are running the mito completion server locally
|
|
30
|
+
|
|
31
|
+
# Set ACTIVE_BASE_URL manually
|
|
32
|
+
ACTIVE_BASE_URL = MITO_PROD_BASE_URL # Change to MITO_DEV_BASE_URL for dev
|
|
33
|
+
|
|
34
|
+
# Endpoint paths
|
|
35
|
+
ANTHROPIC_PATH = "anthropic/completions"
|
|
36
|
+
GEMINI_PATH = "gemini/completions"
|
|
37
|
+
OPENAI_PATH = "openai/completions"
|
|
38
|
+
|
|
39
|
+
# Full URLs (always use ACTIVE_BASE_URL)
|
|
40
|
+
MITO_ANTHROPIC_URL = f"{ACTIVE_BASE_URL}/{ANTHROPIC_PATH}"
|
|
41
|
+
MITO_GEMINI_URL = f"{ACTIVE_BASE_URL}/{GEMINI_PATH}"
|
|
42
|
+
MITO_OPENAI_URL = f"{ACTIVE_BASE_URL}/{OPENAI_PATH}"
|
|
43
|
+
|
|
44
|
+
# Streamlit conversion endpoints
|
|
45
|
+
MITO_STREAMLIT_DEV_BASE_URL = "https://fr12uvtfy5.execute-api.us-east-1.amazonaws.com"
|
|
46
|
+
MITO_STREAMLIT_TEST_BASE_URL = "https://iyual08t6d.execute-api.us-east-1.amazonaws.com"
|
|
47
|
+
|
|
48
|
+
# Set ACTIVE_BASE_URL manually
|
|
49
|
+
# TODO: Modify to PROD url before release
|
|
50
|
+
ACTIVE_STREAMLIT_BASE_URL = MITO_STREAMLIT_DEV_BASE_URL # Change to MITO_STREAMLIT_DEV_BASE_URL for dev
|
|
51
|
+
|
|
52
|
+
# AWS Cognito configuration
|
|
53
|
+
COGNITO_CONFIG_DEV = {
|
|
54
|
+
'TOKEN_ENDPOINT': 'https://mito-app-auth.auth.us-east-1.amazoncognito.com/oauth2/token',
|
|
55
|
+
'CLIENT_ID': '6ara3u3l8sss738hrhbq1qtiqf',
|
|
56
|
+
'CLIENT_SECRET': '',
|
|
57
|
+
'REDIRECT_URI': 'http://localhost:8888/lab'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
ACTIVE_COGNITO_CONFIG = COGNITO_CONFIG_DEV # Change to COGNITO_CONFIG_DEV for dev
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
MESSAGE_HISTORY_TRIM_THRESHOLD: int = 3
|
mito_ai/db/__init__.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
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, Dict, Any
|
|
5
|
+
from sqlalchemy import create_engine, text
|
|
6
|
+
from sqlalchemy.exc import SQLAlchemyError
|
|
7
|
+
from mito_ai.db.crawlers.constants import SUPPORTED_DATABASES
|
|
8
|
+
from mito_ai.db.models import ColumnInfo, TableSchema
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def crawl_db(conn_str: str, db_type: str) -> Dict[str, Any]:
|
|
12
|
+
try:
|
|
13
|
+
if db_type == "mssql":
|
|
14
|
+
# For Microsoft SQL Server, we need to trust the server certificate
|
|
15
|
+
engine = create_engine(
|
|
16
|
+
conn_str, connect_args={"TrustServerCertificate": "yes"}
|
|
17
|
+
)
|
|
18
|
+
else:
|
|
19
|
+
engine = create_engine(conn_str)
|
|
20
|
+
|
|
21
|
+
tables: List[str] = []
|
|
22
|
+
schema: TableSchema = {"tables": {}}
|
|
23
|
+
tables_query = SUPPORTED_DATABASES[db_type].get("tables_query", "")
|
|
24
|
+
columns_query = SUPPORTED_DATABASES[db_type].get("columns_query", "")
|
|
25
|
+
|
|
26
|
+
# Get a list of all tables in the database
|
|
27
|
+
with engine.connect() as connection:
|
|
28
|
+
# Use parameterized query for safety
|
|
29
|
+
result = connection.execute(text(tables_query), {"schema": "public"})
|
|
30
|
+
tables = [row[0] for row in result]
|
|
31
|
+
|
|
32
|
+
# For each table, get the column names and data types
|
|
33
|
+
for table in tables:
|
|
34
|
+
if db_type == "mysql":
|
|
35
|
+
# For MySQL we have to use string formatting
|
|
36
|
+
# since MySQL doesn't support parameter binding
|
|
37
|
+
query = columns_query.format(table=table)
|
|
38
|
+
columns = connection.execute(text(query))
|
|
39
|
+
else:
|
|
40
|
+
# For other databases, use parameter binding
|
|
41
|
+
columns = connection.execute(text(columns_query), {"table": table})
|
|
42
|
+
# Create a list of dictionaries with column name and type
|
|
43
|
+
column_info: List[ColumnInfo] = [
|
|
44
|
+
{"name": row[0], "type": row[1]} for row in columns
|
|
45
|
+
]
|
|
46
|
+
schema["tables"][table] = column_info
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
"schema": schema,
|
|
50
|
+
"error": None,
|
|
51
|
+
}
|
|
52
|
+
except SQLAlchemyError as e:
|
|
53
|
+
return {
|
|
54
|
+
"schema": None,
|
|
55
|
+
"error": f"Database error: {str(e)}",
|
|
56
|
+
}
|
|
57
|
+
except Exception as e:
|
|
58
|
+
return {
|
|
59
|
+
"schema": None,
|
|
60
|
+
"error": f"Unexpected error: {str(e)}",
|
|
61
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
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 Dict, TypedDict, List
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DatabaseConfig(TypedDict, total=False):
|
|
8
|
+
drivers: List[str]
|
|
9
|
+
tables_query: str
|
|
10
|
+
columns_query: str
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
SUPPORTED_DATABASES: Dict[str, DatabaseConfig] = {
|
|
14
|
+
"mssql": {
|
|
15
|
+
"drivers": ["pyodbc"],
|
|
16
|
+
"tables_query": "SELECT table_name FROM information_schema.tables WHERE table_schema = 'dbo'",
|
|
17
|
+
"columns_query": "SELECT column_name, data_type FROM information_schema.columns WHERE table_name = :table",
|
|
18
|
+
},
|
|
19
|
+
"mysql": {
|
|
20
|
+
"drivers": ["PyMySQL"],
|
|
21
|
+
"tables_query": "SHOW TABLES",
|
|
22
|
+
"columns_query": "SHOW COLUMNS FROM {table}",
|
|
23
|
+
},
|
|
24
|
+
"oracle": {
|
|
25
|
+
"drivers": ["oracledb"],
|
|
26
|
+
"tables_query": "SELECT table_name FROM user_tables",
|
|
27
|
+
"columns_query": "SELECT column_name, data_type FROM user_tab_columns WHERE table_name = :table",
|
|
28
|
+
},
|
|
29
|
+
"postgres": {
|
|
30
|
+
"drivers": ["psycopg2-binary"],
|
|
31
|
+
"tables_query": "SELECT table_name FROM information_schema.tables WHERE table_schema = :schema",
|
|
32
|
+
"columns_query": "SELECT column_name, data_type FROM information_schema.columns WHERE table_name = :table",
|
|
33
|
+
},
|
|
34
|
+
"snowflake": {
|
|
35
|
+
"drivers": ["snowflake-sqlalchemy"],
|
|
36
|
+
# Queries handled in the snowflake.py file.
|
|
37
|
+
},
|
|
38
|
+
"sqlite": {
|
|
39
|
+
"drivers": [],
|
|
40
|
+
"tables_query": "SELECT name FROM sqlite_master WHERE type='table'",
|
|
41
|
+
"columns_query": "SELECT name, type FROM pragma_table_info(:table)",
|
|
42
|
+
},
|
|
43
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Copyright (c) Saga Inc.
|
|
2
|
+
# Distributed under the terms of the GNU Affero General Public License v3.0 License.
|
|
3
|
+
|
|
4
|
+
from sqlalchemy import create_engine, text
|
|
5
|
+
from mito_ai.db.models import WarehouseDetails
|
|
6
|
+
|
|
7
|
+
SUPPORTED_DATABASE_KINDS = ["STANDARD", "IMPORTED DATABASE"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def crawl_snowflake(username: str, password: str, account: str, warehouse: str) -> dict:
|
|
11
|
+
try:
|
|
12
|
+
conn_str = (
|
|
13
|
+
f"snowflake://{username}:{password}@{account}/" f"?warehouse={warehouse}"
|
|
14
|
+
)
|
|
15
|
+
engine = create_engine(conn_str)
|
|
16
|
+
|
|
17
|
+
# Step 1: Get databases
|
|
18
|
+
db_query = text("SHOW DATABASES")
|
|
19
|
+
with engine.connect() as connection:
|
|
20
|
+
result = connection.execute(db_query)
|
|
21
|
+
databases = result.mappings().all()
|
|
22
|
+
|
|
23
|
+
# Step 2: Filter for 'STANDARD' and 'IMPORTED DATABASE' kinds
|
|
24
|
+
filtered_db_names = [
|
|
25
|
+
row["name"] for row in databases if row["kind"] in SUPPORTED_DATABASE_KINDS
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
# Step 3: Escape and format for SQL IN clause
|
|
29
|
+
db_list_sql = ", ".join(f"'{name}'" for name in filtered_db_names)
|
|
30
|
+
|
|
31
|
+
# Step 4: Use filtered databases in schema query
|
|
32
|
+
schema_query = text(
|
|
33
|
+
f"""
|
|
34
|
+
SELECT
|
|
35
|
+
c.table_catalog AS database_name,
|
|
36
|
+
c.table_schema AS schema_name,
|
|
37
|
+
c.table_name,
|
|
38
|
+
c.column_name,
|
|
39
|
+
c.data_type,
|
|
40
|
+
c.comment
|
|
41
|
+
FROM snowflake.account_usage.columns c
|
|
42
|
+
WHERE c.deleted IS NULL
|
|
43
|
+
AND c.table_catalog IN ({db_list_sql})
|
|
44
|
+
"""
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Step 5: Execute the schema query and process the results
|
|
48
|
+
warehouse_details: WarehouseDetails = {"databases": {}}
|
|
49
|
+
|
|
50
|
+
with engine.connect() as connection:
|
|
51
|
+
result = connection.execute(schema_query)
|
|
52
|
+
for row in result.fetchall():
|
|
53
|
+
db = row[0]
|
|
54
|
+
schema = row[1]
|
|
55
|
+
table = row[2]
|
|
56
|
+
column_name = row[3]
|
|
57
|
+
|
|
58
|
+
db_dict = warehouse_details["databases"].setdefault(db, {"schemas": {}})
|
|
59
|
+
schema_dict = db_dict["schemas"].setdefault(schema, {"tables": {}})
|
|
60
|
+
columns_list = schema_dict["tables"].setdefault(table, [])
|
|
61
|
+
columns_list.append({"name": column_name, "type": row[4]})
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
"schema": warehouse_details,
|
|
65
|
+
"error": None,
|
|
66
|
+
}
|
|
67
|
+
except Exception as e:
|
|
68
|
+
return {
|
|
69
|
+
"schema": None,
|
|
70
|
+
"error": str(e),
|
|
71
|
+
}
|