mito-ai 0.1.50__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. mito_ai/__init__.py +114 -0
  2. mito_ai/_version.py +4 -0
  3. mito_ai/anthropic_client.py +334 -0
  4. mito_ai/app_deploy/__init__.py +6 -0
  5. mito_ai/app_deploy/app_deploy_utils.py +44 -0
  6. mito_ai/app_deploy/handlers.py +345 -0
  7. mito_ai/app_deploy/models.py +98 -0
  8. mito_ai/app_manager/__init__.py +4 -0
  9. mito_ai/app_manager/handlers.py +167 -0
  10. mito_ai/app_manager/models.py +71 -0
  11. mito_ai/app_manager/utils.py +24 -0
  12. mito_ai/auth/README.md +18 -0
  13. mito_ai/auth/__init__.py +6 -0
  14. mito_ai/auth/handlers.py +96 -0
  15. mito_ai/auth/urls.py +13 -0
  16. mito_ai/chat_history/handlers.py +63 -0
  17. mito_ai/chat_history/urls.py +32 -0
  18. mito_ai/completions/completion_handlers/__init__.py +3 -0
  19. mito_ai/completions/completion_handlers/agent_auto_error_fixup_handler.py +59 -0
  20. mito_ai/completions/completion_handlers/agent_execution_handler.py +66 -0
  21. mito_ai/completions/completion_handlers/chat_completion_handler.py +141 -0
  22. mito_ai/completions/completion_handlers/code_explain_handler.py +113 -0
  23. mito_ai/completions/completion_handlers/completion_handler.py +42 -0
  24. mito_ai/completions/completion_handlers/inline_completer_handler.py +48 -0
  25. mito_ai/completions/completion_handlers/smart_debug_handler.py +160 -0
  26. mito_ai/completions/completion_handlers/utils.py +147 -0
  27. mito_ai/completions/handlers.py +415 -0
  28. mito_ai/completions/message_history.py +401 -0
  29. mito_ai/completions/models.py +404 -0
  30. mito_ai/completions/prompt_builders/__init__.py +3 -0
  31. mito_ai/completions/prompt_builders/agent_execution_prompt.py +57 -0
  32. mito_ai/completions/prompt_builders/agent_smart_debug_prompt.py +160 -0
  33. mito_ai/completions/prompt_builders/agent_system_message.py +472 -0
  34. mito_ai/completions/prompt_builders/chat_name_prompt.py +15 -0
  35. mito_ai/completions/prompt_builders/chat_prompt.py +116 -0
  36. mito_ai/completions/prompt_builders/chat_system_message.py +92 -0
  37. mito_ai/completions/prompt_builders/explain_code_prompt.py +32 -0
  38. mito_ai/completions/prompt_builders/inline_completer_prompt.py +197 -0
  39. mito_ai/completions/prompt_builders/prompt_constants.py +170 -0
  40. mito_ai/completions/prompt_builders/smart_debug_prompt.py +199 -0
  41. mito_ai/completions/prompt_builders/utils.py +84 -0
  42. mito_ai/completions/providers.py +284 -0
  43. mito_ai/constants.py +63 -0
  44. mito_ai/db/__init__.py +3 -0
  45. mito_ai/db/crawlers/__init__.py +6 -0
  46. mito_ai/db/crawlers/base_crawler.py +61 -0
  47. mito_ai/db/crawlers/constants.py +43 -0
  48. mito_ai/db/crawlers/snowflake.py +71 -0
  49. mito_ai/db/handlers.py +168 -0
  50. mito_ai/db/models.py +31 -0
  51. mito_ai/db/urls.py +34 -0
  52. mito_ai/db/utils.py +185 -0
  53. mito_ai/docker/mssql/compose.yml +37 -0
  54. mito_ai/docker/mssql/init/setup.sql +21 -0
  55. mito_ai/docker/mysql/compose.yml +18 -0
  56. mito_ai/docker/mysql/init/setup.sql +13 -0
  57. mito_ai/docker/oracle/compose.yml +17 -0
  58. mito_ai/docker/oracle/init/setup.sql +20 -0
  59. mito_ai/docker/postgres/compose.yml +17 -0
  60. mito_ai/docker/postgres/init/setup.sql +13 -0
  61. mito_ai/enterprise/__init__.py +3 -0
  62. mito_ai/enterprise/utils.py +15 -0
  63. mito_ai/file_uploads/__init__.py +3 -0
  64. mito_ai/file_uploads/handlers.py +248 -0
  65. mito_ai/file_uploads/urls.py +21 -0
  66. mito_ai/gemini_client.py +232 -0
  67. mito_ai/log/handlers.py +38 -0
  68. mito_ai/log/urls.py +21 -0
  69. mito_ai/logger.py +37 -0
  70. mito_ai/openai_client.py +382 -0
  71. mito_ai/path_utils.py +70 -0
  72. mito_ai/rules/handlers.py +44 -0
  73. mito_ai/rules/urls.py +22 -0
  74. mito_ai/rules/utils.py +56 -0
  75. mito_ai/settings/handlers.py +41 -0
  76. mito_ai/settings/urls.py +20 -0
  77. mito_ai/settings/utils.py +42 -0
  78. mito_ai/streamlit_conversion/agent_utils.py +37 -0
  79. mito_ai/streamlit_conversion/prompts/prompt_constants.py +172 -0
  80. mito_ai/streamlit_conversion/prompts/prompt_utils.py +10 -0
  81. mito_ai/streamlit_conversion/prompts/streamlit_app_creation_prompt.py +46 -0
  82. mito_ai/streamlit_conversion/prompts/streamlit_error_correction_prompt.py +28 -0
  83. mito_ai/streamlit_conversion/prompts/streamlit_finish_todo_prompt.py +45 -0
  84. mito_ai/streamlit_conversion/prompts/streamlit_system_prompt.py +56 -0
  85. mito_ai/streamlit_conversion/prompts/update_existing_app_prompt.py +50 -0
  86. mito_ai/streamlit_conversion/search_replace_utils.py +94 -0
  87. mito_ai/streamlit_conversion/streamlit_agent_handler.py +144 -0
  88. mito_ai/streamlit_conversion/streamlit_utils.py +85 -0
  89. mito_ai/streamlit_conversion/validate_streamlit_app.py +105 -0
  90. mito_ai/streamlit_preview/__init__.py +6 -0
  91. mito_ai/streamlit_preview/handlers.py +111 -0
  92. mito_ai/streamlit_preview/manager.py +152 -0
  93. mito_ai/streamlit_preview/urls.py +22 -0
  94. mito_ai/streamlit_preview/utils.py +29 -0
  95. mito_ai/tests/__init__.py +3 -0
  96. mito_ai/tests/chat_history/test_chat_history.py +211 -0
  97. mito_ai/tests/completions/completion_handlers_utils_test.py +190 -0
  98. mito_ai/tests/conftest.py +53 -0
  99. mito_ai/tests/create_agent_system_message_prompt_test.py +22 -0
  100. mito_ai/tests/data/prompt_lg.py +69 -0
  101. mito_ai/tests/data/prompt_sm.py +6 -0
  102. mito_ai/tests/data/prompt_xl.py +13 -0
  103. mito_ai/tests/data/stock_data.sqlite3 +0 -0
  104. mito_ai/tests/db/conftest.py +39 -0
  105. mito_ai/tests/db/connections_test.py +102 -0
  106. mito_ai/tests/db/mssql_test.py +29 -0
  107. mito_ai/tests/db/mysql_test.py +29 -0
  108. mito_ai/tests/db/oracle_test.py +29 -0
  109. mito_ai/tests/db/postgres_test.py +29 -0
  110. mito_ai/tests/db/schema_test.py +93 -0
  111. mito_ai/tests/db/sqlite_test.py +31 -0
  112. mito_ai/tests/db/test_db_constants.py +61 -0
  113. mito_ai/tests/deploy_app/test_app_deploy_utils.py +89 -0
  114. mito_ai/tests/file_uploads/__init__.py +2 -0
  115. mito_ai/tests/file_uploads/test_handlers.py +282 -0
  116. mito_ai/tests/message_history/test_generate_short_chat_name.py +120 -0
  117. mito_ai/tests/message_history/test_message_history_utils.py +469 -0
  118. mito_ai/tests/open_ai_utils_test.py +152 -0
  119. mito_ai/tests/performance_test.py +329 -0
  120. mito_ai/tests/providers/test_anthropic_client.py +447 -0
  121. mito_ai/tests/providers/test_azure.py +631 -0
  122. mito_ai/tests/providers/test_capabilities.py +120 -0
  123. mito_ai/tests/providers/test_gemini_client.py +195 -0
  124. mito_ai/tests/providers/test_mito_server_utils.py +448 -0
  125. mito_ai/tests/providers/test_model_resolution.py +130 -0
  126. mito_ai/tests/providers/test_openai_client.py +57 -0
  127. mito_ai/tests/providers/test_provider_completion_exception.py +66 -0
  128. mito_ai/tests/providers/test_provider_limits.py +42 -0
  129. mito_ai/tests/providers/test_providers.py +382 -0
  130. mito_ai/tests/providers/test_retry_logic.py +389 -0
  131. mito_ai/tests/providers/test_stream_mito_server_utils.py +140 -0
  132. mito_ai/tests/providers/utils.py +85 -0
  133. mito_ai/tests/rules/conftest.py +26 -0
  134. mito_ai/tests/rules/rules_test.py +117 -0
  135. mito_ai/tests/server_limits_test.py +406 -0
  136. mito_ai/tests/settings/conftest.py +26 -0
  137. mito_ai/tests/settings/settings_test.py +70 -0
  138. mito_ai/tests/settings/test_settings_constants.py +9 -0
  139. mito_ai/tests/streamlit_conversion/__init__.py +3 -0
  140. mito_ai/tests/streamlit_conversion/test_apply_search_replace.py +240 -0
  141. mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +246 -0
  142. mito_ai/tests/streamlit_conversion/test_streamlit_utils.py +193 -0
  143. mito_ai/tests/streamlit_conversion/test_validate_streamlit_app.py +112 -0
  144. mito_ai/tests/streamlit_preview/test_streamlit_preview_handler.py +118 -0
  145. mito_ai/tests/streamlit_preview/test_streamlit_preview_manager.py +292 -0
  146. mito_ai/tests/test_constants.py +47 -0
  147. mito_ai/tests/test_telemetry.py +12 -0
  148. mito_ai/tests/user/__init__.py +2 -0
  149. mito_ai/tests/user/test_user.py +120 -0
  150. mito_ai/tests/utils/__init__.py +3 -0
  151. mito_ai/tests/utils/test_anthropic_utils.py +162 -0
  152. mito_ai/tests/utils/test_gemini_utils.py +98 -0
  153. mito_ai/tests/version_check_test.py +169 -0
  154. mito_ai/user/handlers.py +45 -0
  155. mito_ai/user/urls.py +21 -0
  156. mito_ai/utils/__init__.py +3 -0
  157. mito_ai/utils/anthropic_utils.py +168 -0
  158. mito_ai/utils/create.py +94 -0
  159. mito_ai/utils/db.py +74 -0
  160. mito_ai/utils/error_classes.py +42 -0
  161. mito_ai/utils/gemini_utils.py +133 -0
  162. mito_ai/utils/message_history_utils.py +87 -0
  163. mito_ai/utils/mito_server_utils.py +242 -0
  164. mito_ai/utils/open_ai_utils.py +200 -0
  165. mito_ai/utils/provider_utils.py +49 -0
  166. mito_ai/utils/schema.py +86 -0
  167. mito_ai/utils/server_limits.py +152 -0
  168. mito_ai/utils/telemetry_utils.py +480 -0
  169. mito_ai/utils/utils.py +89 -0
  170. mito_ai/utils/version_utils.py +94 -0
  171. mito_ai/utils/websocket_base.py +88 -0
  172. mito_ai/version_check.py +60 -0
  173. mito_ai-0.1.50.data/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +7 -0
  174. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/build_log.json +728 -0
  175. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/package.json +243 -0
  176. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/package.json.orig +238 -0
  177. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/toolbar-buttons.json +37 -0
  178. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.8f1845da6bf2b128c049.js +21602 -0
  179. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.8f1845da6bf2b128c049.js.map +1 -0
  180. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js +198 -0
  181. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js.map +1 -0
  182. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.78d3ccb73e7ca1da3aae.js +619 -0
  183. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.78d3ccb73e7ca1da3aae.js.map +1 -0
  184. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/style.js +4 -0
  185. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js +712 -0
  186. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js.map +1 -0
  187. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_apis_signOut_mjs-node_module-75790d.688c25857e7b81b1740f.js +533 -0
  188. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_apis_signOut_mjs-node_module-75790d.688c25857e7b81b1740f.js.map +1 -0
  189. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_tokenProvider_tokenProvider_-72f1c8.a917210f057fcfe224ad.js +6941 -0
  190. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_tokenProvider_tokenProvider_-72f1c8.a917210f057fcfe224ad.js.map +1 -0
  191. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js +1021 -0
  192. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js.map +1 -0
  193. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js +59698 -0
  194. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js.map +1 -0
  195. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_react-dom_client_js-node_modules_aws-amplify_ui-react_dist_styles_css.b43d4249e4d3dac9ad7b.js +7440 -0
  196. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_react-dom_client_js-node_modules_aws-amplify_ui-react_dist_styles_css.b43d4249e4d3dac9ad7b.js.map +1 -0
  197. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js +2792 -0
  198. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js.map +1 -0
  199. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js +4859 -0
  200. mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js.map +1 -0
  201. mito_ai-0.1.50.dist-info/METADATA +221 -0
  202. mito_ai-0.1.50.dist-info/RECORD +205 -0
  203. mito_ai-0.1.50.dist-info/WHEEL +4 -0
  204. mito_ai-0.1.50.dist-info/entry_points.txt +2 -0
  205. mito_ai-0.1.50.dist-info/licenses/LICENSE +3 -0
@@ -0,0 +1,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,3 @@
1
+ # Copyright (c) Saga Inc.
2
+ # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
+
@@ -0,0 +1,6 @@
1
+ # Copyright (c) Saga Inc.
2
+ # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
+
4
+ from mito_ai.db.crawlers.snowflake import crawl_snowflake
5
+
6
+ __all__ = ["crawl_snowflake"]
@@ -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
+ }