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,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
+ )
@@ -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