mito-ai 0.1.33__py3-none-any.whl → 0.1.49__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 (146) hide show
  1. mito_ai/__init__.py +49 -9
  2. mito_ai/_version.py +1 -1
  3. mito_ai/anthropic_client.py +142 -67
  4. mito_ai/{app_builder → app_deploy}/__init__.py +1 -1
  5. mito_ai/app_deploy/app_deploy_utils.py +44 -0
  6. mito_ai/app_deploy/handlers.py +345 -0
  7. mito_ai/{app_builder → app_deploy}/models.py +35 -22
  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/agent_execution_handler.py +1 -1
  19. mito_ai/completions/completion_handlers/chat_completion_handler.py +4 -4
  20. mito_ai/completions/completion_handlers/utils.py +99 -37
  21. mito_ai/completions/handlers.py +57 -20
  22. mito_ai/completions/message_history.py +9 -1
  23. mito_ai/completions/models.py +31 -7
  24. mito_ai/completions/prompt_builders/agent_execution_prompt.py +21 -2
  25. mito_ai/completions/prompt_builders/agent_smart_debug_prompt.py +8 -0
  26. mito_ai/completions/prompt_builders/agent_system_message.py +115 -42
  27. mito_ai/completions/prompt_builders/chat_name_prompt.py +6 -6
  28. mito_ai/completions/prompt_builders/chat_prompt.py +18 -11
  29. mito_ai/completions/prompt_builders/chat_system_message.py +4 -0
  30. mito_ai/completions/prompt_builders/prompt_constants.py +23 -4
  31. mito_ai/completions/prompt_builders/utils.py +72 -10
  32. mito_ai/completions/providers.py +81 -47
  33. mito_ai/constants.py +25 -24
  34. mito_ai/file_uploads/__init__.py +3 -0
  35. mito_ai/file_uploads/handlers.py +248 -0
  36. mito_ai/file_uploads/urls.py +21 -0
  37. mito_ai/gemini_client.py +44 -48
  38. mito_ai/log/handlers.py +10 -3
  39. mito_ai/log/urls.py +3 -3
  40. mito_ai/openai_client.py +30 -44
  41. mito_ai/path_utils.py +70 -0
  42. mito_ai/streamlit_conversion/agent_utils.py +37 -0
  43. mito_ai/streamlit_conversion/prompts/prompt_constants.py +172 -0
  44. mito_ai/streamlit_conversion/prompts/prompt_utils.py +10 -0
  45. mito_ai/streamlit_conversion/prompts/streamlit_app_creation_prompt.py +46 -0
  46. mito_ai/streamlit_conversion/prompts/streamlit_error_correction_prompt.py +28 -0
  47. mito_ai/streamlit_conversion/prompts/streamlit_finish_todo_prompt.py +45 -0
  48. mito_ai/streamlit_conversion/prompts/streamlit_system_prompt.py +56 -0
  49. mito_ai/streamlit_conversion/prompts/update_existing_app_prompt.py +50 -0
  50. mito_ai/streamlit_conversion/search_replace_utils.py +94 -0
  51. mito_ai/streamlit_conversion/streamlit_agent_handler.py +144 -0
  52. mito_ai/streamlit_conversion/streamlit_utils.py +85 -0
  53. mito_ai/streamlit_conversion/validate_streamlit_app.py +105 -0
  54. mito_ai/streamlit_preview/__init__.py +6 -0
  55. mito_ai/streamlit_preview/handlers.py +111 -0
  56. mito_ai/streamlit_preview/manager.py +152 -0
  57. mito_ai/streamlit_preview/urls.py +22 -0
  58. mito_ai/streamlit_preview/utils.py +29 -0
  59. mito_ai/tests/chat_history/test_chat_history.py +211 -0
  60. mito_ai/tests/completions/completion_handlers_utils_test.py +190 -0
  61. mito_ai/tests/deploy_app/test_app_deploy_utils.py +89 -0
  62. mito_ai/tests/file_uploads/__init__.py +2 -0
  63. mito_ai/tests/file_uploads/test_handlers.py +282 -0
  64. mito_ai/tests/message_history/test_generate_short_chat_name.py +0 -4
  65. mito_ai/tests/message_history/test_message_history_utils.py +103 -23
  66. mito_ai/tests/open_ai_utils_test.py +18 -22
  67. mito_ai/tests/providers/test_anthropic_client.py +447 -0
  68. mito_ai/tests/providers/test_azure.py +2 -6
  69. mito_ai/tests/providers/test_capabilities.py +120 -0
  70. mito_ai/tests/{test_gemini_client.py → providers/test_gemini_client.py} +40 -36
  71. mito_ai/tests/providers/test_mito_server_utils.py +448 -0
  72. mito_ai/tests/providers/test_model_resolution.py +130 -0
  73. mito_ai/tests/providers/test_openai_client.py +57 -0
  74. mito_ai/tests/providers/test_provider_completion_exception.py +66 -0
  75. mito_ai/tests/providers/test_provider_limits.py +42 -0
  76. mito_ai/tests/providers/test_providers.py +382 -0
  77. mito_ai/tests/providers/test_retry_logic.py +389 -0
  78. mito_ai/tests/providers/test_stream_mito_server_utils.py +140 -0
  79. mito_ai/tests/providers/utils.py +85 -0
  80. mito_ai/tests/streamlit_conversion/__init__.py +3 -0
  81. mito_ai/tests/streamlit_conversion/test_apply_search_replace.py +240 -0
  82. mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +246 -0
  83. mito_ai/tests/streamlit_conversion/test_streamlit_utils.py +193 -0
  84. mito_ai/tests/streamlit_conversion/test_validate_streamlit_app.py +112 -0
  85. mito_ai/tests/streamlit_preview/test_streamlit_preview_handler.py +118 -0
  86. mito_ai/tests/streamlit_preview/test_streamlit_preview_manager.py +292 -0
  87. mito_ai/tests/test_constants.py +31 -3
  88. mito_ai/tests/test_telemetry.py +12 -0
  89. mito_ai/tests/user/__init__.py +2 -0
  90. mito_ai/tests/user/test_user.py +120 -0
  91. mito_ai/tests/utils/test_anthropic_utils.py +6 -6
  92. mito_ai/user/handlers.py +45 -0
  93. mito_ai/user/urls.py +21 -0
  94. mito_ai/utils/anthropic_utils.py +55 -121
  95. mito_ai/utils/create.py +17 -1
  96. mito_ai/utils/error_classes.py +42 -0
  97. mito_ai/utils/gemini_utils.py +39 -94
  98. mito_ai/utils/message_history_utils.py +7 -4
  99. mito_ai/utils/mito_server_utils.py +242 -0
  100. mito_ai/utils/open_ai_utils.py +38 -155
  101. mito_ai/utils/provider_utils.py +49 -0
  102. mito_ai/utils/server_limits.py +1 -1
  103. mito_ai/utils/telemetry_utils.py +137 -5
  104. {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +102 -100
  105. {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/package.json +4 -2
  106. {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/package.json.orig +3 -1
  107. {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/toolbar-buttons.json +2 -2
  108. mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.281f4b9af60d620c6fb1.js → mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.8f1845da6bf2b128c049.js +15948 -8403
  109. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.8f1845da6bf2b128c049.js.map +1 -0
  110. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js +198 -0
  111. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js.map +1 -0
  112. mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.4f1d00fd0c58fcc05d8d.js → mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.8b24b5b3b93f95205b56.js +58 -33
  113. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.8b24b5b3b93f95205b56.js.map +1 -0
  114. mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.06083e515de4862df010.js → mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js +10 -2
  115. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js.map +1 -0
  116. mito_ai-0.1.49.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
  117. mito_ai-0.1.49.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
  118. mito_ai-0.1.49.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
  119. mito_ai-0.1.49.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
  120. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js +1021 -0
  121. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js.map +1 -0
  122. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js +59698 -0
  123. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js.map +1 -0
  124. mito_ai-0.1.49.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
  125. mito_ai-0.1.49.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
  126. mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.9795f79265ddb416864b.js → mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js +2 -240
  127. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js.map +1 -0
  128. {mito_ai-0.1.33.dist-info → mito_ai-0.1.49.dist-info}/METADATA +5 -2
  129. mito_ai-0.1.49.dist-info/RECORD +205 -0
  130. mito_ai/app_builder/handlers.py +0 -218
  131. mito_ai/tests/providers_test.py +0 -438
  132. mito_ai/tests/test_anthropic_client.py +0 -270
  133. mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.281f4b9af60d620c6fb1.js.map +0 -1
  134. mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.4f1d00fd0c58fcc05d8d.js.map +0 -1
  135. mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.06083e515de4862df010.js.map +0 -1
  136. mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_html2canvas_dist_html2canvas_js.ea47e8c8c906197f8d19.js +0 -7842
  137. mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_html2canvas_dist_html2canvas_js.ea47e8c8c906197f8d19.js.map +0 -1
  138. mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.9795f79265ddb416864b.js.map +0 -1
  139. mito_ai-0.1.33.dist-info/RECORD +0 -134
  140. {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
  141. {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
  142. {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js +0 -0
  143. {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js.map +0 -0
  144. {mito_ai-0.1.33.dist-info → mito_ai-0.1.49.dist-info}/WHEEL +0 -0
  145. {mito_ai-0.1.33.dist-info → mito_ai-0.1.49.dist-info}/entry_points.txt +0 -0
  146. {mito_ai-0.1.33.dist-info → mito_ai-0.1.49.dist-info}/licenses/LICENSE +0 -0
@@ -2,6 +2,7 @@
2
2
  # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
3
 
4
4
  from __future__ import annotations
5
+ import asyncio
5
6
  from typing import Any, Callable, Dict, List, Optional, Union, cast
6
7
  from mito_ai import constants
7
8
  from openai.types.chat import ChatCompletionMessageParam
@@ -28,12 +29,16 @@ from mito_ai.completions.models import (
28
29
  from mito_ai.utils.telemetry_utils import (
29
30
  KEY_TYPE_PARAM,
30
31
  MITO_AI_COMPLETION_ERROR,
32
+ MITO_AI_COMPLETION_RETRY,
31
33
  MITO_SERVER_KEY,
32
34
  USER_KEY,
33
35
  log,
36
+ log_ai_completion_error,
37
+ log_ai_completion_retry,
34
38
  log_ai_completion_success,
35
39
  )
36
- from mito_ai.constants import get_model_provider
40
+ from mito_ai.utils.provider_utils import get_model_provider
41
+ from mito_ai.utils.mito_server_utils import ProviderCompletionException
37
42
 
38
43
  __all__ = ["OpenAIProvider"]
39
44
 
@@ -66,6 +71,9 @@ This attribute is observed by the websocket provider to push the error to the cl
66
71
 
67
72
  @property
68
73
  def capabilities(self) -> AICapabilities:
74
+ """
75
+ Returns the capabilities of the AI provider.
76
+ """
69
77
  if constants.CLAUDE_API_KEY and not self.api_key:
70
78
  return AICapabilities(
71
79
  configuration={"model": "<dynamic>"},
@@ -78,6 +86,7 @@ This attribute is observed by the websocket provider to push the error to the cl
78
86
  )
79
87
  if self._openai_client:
80
88
  return self._openai_client.capabilities
89
+
81
90
  return AICapabilities(
82
91
  configuration={"model": "<dynamic>"},
83
92
  provider="Mito server",
@@ -100,7 +109,8 @@ This attribute is observed by the websocket provider to push the error to the cl
100
109
  model: str,
101
110
  response_format_info: Optional[ResponseFormatInfo] = None,
102
111
  user_input: Optional[str] = None,
103
- thread_id: Optional[str] = None
112
+ thread_id: Optional[str] = None,
113
+ max_retries: int = 3
104
114
  ) -> str:
105
115
  """
106
116
  Request completions from the AI provider.
@@ -109,43 +119,69 @@ This attribute is observed by the websocket provider to push the error to the cl
109
119
  completion = None
110
120
  last_message_content = str(messages[-1].get('content', '')) if messages else ""
111
121
  model_type = get_model_provider(model)
112
- try:
113
- if model_type == "claude":
114
- api_key = constants.CLAUDE_API_KEY
115
- anthropic_client = AnthropicClient(api_key=api_key, model=model)
116
- completion = await anthropic_client.request_completions(messages, response_format_info, message_type)
117
- elif model_type == "gemini":
118
- api_key = constants.GEMINI_API_KEY
119
- gemini_client = GeminiClient(api_key=api_key, model=model)
120
- messages_for_gemini = [dict(m) for m in messages]
121
- completion = await gemini_client.request_completions(messages_for_gemini, response_format_info, message_type)
122
- elif model_type == "openai":
123
- if not self._openai_client:
124
- raise RuntimeError("OpenAI client is not initialized.")
125
- completion = await self._openai_client.request_completions(
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,
126
150
  message_type=message_type,
127
- messages=messages,
128
- model=model,
129
- response_format_info=response_format_info
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
130
156
  )
131
- else:
132
- raise ValueError(f"No AI provider configured for model: {model}")
133
- log_ai_completion_success(
134
- key_type=USER_KEY if self.key_type == "user" else MITO_SERVER_KEY,
135
- message_type=message_type,
136
- last_message_content=last_message_content,
137
- response={"completion": completion},
138
- user_input=user_input or "",
139
- thread_id=thread_id or "",
140
- model=model
141
- )
142
- return completion
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
143
165
 
144
- except BaseException as e:
145
- self.log.exception(f"Error during request_completions: {e}")
146
- self.last_error = CompletionError.from_exception(e)
147
- log(MITO_AI_COMPLETION_ERROR, params={KEY_TYPE_PARAM: self.key_type}, error=e)
148
- raise
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")
149
185
 
150
186
  async def stream_completions(
151
187
  self,
@@ -176,19 +212,23 @@ This attribute is observed by the websocket provider to push the error to the cl
176
212
  try:
177
213
  if model_type == "claude":
178
214
  api_key = constants.CLAUDE_API_KEY
179
- anthropic_client = AnthropicClient(api_key=api_key, model=model)
180
- accumulated_response = await anthropic_client.stream_response(
215
+ anthropic_client = AnthropicClient(api_key=api_key)
216
+ accumulated_response = await anthropic_client.stream_completions(
181
217
  messages=messages,
218
+ model=model,
182
219
  message_type=message_type,
183
220
  message_id=message_id,
184
221
  reply_fn=reply_fn
185
222
  )
186
223
  elif model_type == "gemini":
187
224
  api_key = constants.GEMINI_API_KEY
188
- gemini_client = GeminiClient(api_key=api_key, model=model)
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.
189
228
  messages_for_gemini = [dict(m) for m in messages]
190
229
  accumulated_response = await gemini_client.stream_completions(
191
230
  messages=messages_for_gemini,
231
+ model=model,
192
232
  message_id=message_id,
193
233
  reply_fn=reply_fn,
194
234
  message_type=message_type
@@ -224,14 +264,8 @@ This attribute is observed by the websocket provider to push the error to the cl
224
264
  except BaseException as e:
225
265
  self.log.exception(f"Error during stream_completions: {e}")
226
266
  self.last_error = CompletionError.from_exception(e)
227
- log(
228
- MITO_AI_COMPLETION_ERROR,
229
- params={
230
- KEY_TYPE_PARAM: self.key_type,
231
- 'message_type': message_type.value,
232
- },
233
- error=e
234
- )
267
+ log_ai_completion_error('user_key' if self.key_type != MITO_SERVER_KEY else 'mito_server_key', thread_id, message_type, e)
268
+
235
269
  # Send error message to client before raising
236
270
  reply_fn(CompletionStreamChunk(
237
271
  parent_id=message_id,
mito_ai/constants.py CHANGED
@@ -23,30 +23,10 @@ AZURE_OPENAI_API_VERSION = os.environ.get("AZURE_OPENAI_API_VERSION")
23
23
  AZURE_OPENAI_ENDPOINT = os.environ.get("AZURE_OPENAI_ENDPOINT")
24
24
  AZURE_OPENAI_MODEL = os.environ.get("AZURE_OPENAI_MODEL")
25
25
 
26
- def get_model_provider(model: str) -> Union[str, None]:
27
- """
28
- Determine the model type based on the model name prefix
29
- """
30
- if not model:
31
- return None
32
-
33
- model_lower = model.lower()
34
-
35
- if model_lower.startswith('claude'):
36
- return 'claude'
37
- elif model_lower.startswith('gemini'):
38
- return 'gemini'
39
- elif model_lower.startswith('ollama'):
40
- return 'ollama'
41
- elif model_lower.startswith('gpt'):
42
- return 'openai'
43
-
44
- return None
45
-
46
-
47
26
  # Mito AI Base URLs and Endpoint Paths
48
- MITO_PROD_BASE_URL = "https://yxwyadgaznhavqvgnbfuo2k6ca0jboku.lambda-url.us-east-1.on.aws"
49
- MITO_DEV_BASE_URL = "https://x3rafympznv4abp7phos44gzgu0clbui.lambda-url.us-east-1.on.aws"
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
50
30
 
51
31
  # Set ACTIVE_BASE_URL manually
52
32
  ACTIVE_BASE_URL = MITO_PROD_BASE_URL # Change to MITO_DEV_BASE_URL for dev
@@ -59,4 +39,25 @@ OPENAI_PATH = "openai/completions"
59
39
  # Full URLs (always use ACTIVE_BASE_URL)
60
40
  MITO_ANTHROPIC_URL = f"{ACTIVE_BASE_URL}/{ANTHROPIC_PATH}"
61
41
  MITO_GEMINI_URL = f"{ACTIVE_BASE_URL}/{GEMINI_PATH}"
62
- MITO_OPENAI_URL = f"{ACTIVE_BASE_URL}/{OPENAI_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
@@ -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,248 @@
1
+ # Copyright (c) Saga Inc.
2
+ # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
+
4
+ import os
5
+ import tempfile
6
+ import tornado
7
+ from typing import Dict, Any
8
+ from jupyter_server.base.handlers import APIHandler
9
+ from mito_ai.utils.telemetry_utils import (
10
+ log_file_upload_attempt,
11
+ log_file_upload_failure,
12
+ )
13
+
14
+ MAX_IMAGE_SIZE_MB = 3
15
+
16
+
17
+ def _is_image_file(filename: str) -> bool:
18
+ image_extensions = {
19
+ ".jpg",
20
+ ".jpeg",
21
+ ".png",
22
+ ".gif",
23
+ ".bmp",
24
+ ".tiff",
25
+ ".tif",
26
+ ".webp",
27
+ ".svg",
28
+ }
29
+ file_extension = os.path.splitext(filename)[1].lower()
30
+ return file_extension in image_extensions
31
+
32
+
33
+ def _check_image_size_limit(file_data: bytes, filename: str) -> None:
34
+ if not _is_image_file(filename):
35
+ return
36
+
37
+ file_size_mb = len(file_data) / (1024 * 1024) # Convert bytes to MB
38
+
39
+ if file_size_mb > MAX_IMAGE_SIZE_MB:
40
+ raise ValueError(f"Image exceeded {MAX_IMAGE_SIZE_MB}MB limit.")
41
+
42
+
43
+ class FileUploadHandler(APIHandler):
44
+ # Class-level dictionary to store temporary directories for each file upload
45
+ # This persists across handler instances since Tornado recreates handlers per request
46
+ # Key: filename, Value: dict with temp_dir, total_chunks, received_chunks, logged_upload
47
+ _temp_dirs: Dict[str, Dict[str, Any]] = {}
48
+
49
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
50
+ super().__init__(*args, **kwargs)
51
+
52
+ @tornado.web.authenticated
53
+ def post(self) -> None:
54
+ """Handle file upload with multipart form data."""
55
+ try:
56
+ # Validate request has file
57
+ if not self._validate_file_upload():
58
+ return
59
+
60
+ uploaded_file = self.request.files["file"][0]
61
+ filename = uploaded_file["filename"]
62
+ file_data = uploaded_file["body"]
63
+
64
+ # Get notebook directory from request
65
+ notebook_dir = self.get_argument("notebook_dir", ".")
66
+
67
+ # Check if this is a chunked upload
68
+ chunk_number = self.get_argument("chunk_number", None)
69
+ total_chunks = self.get_argument("total_chunks", None)
70
+
71
+ if chunk_number and total_chunks:
72
+ self._handle_chunked_upload(
73
+ filename, file_data, chunk_number, total_chunks, notebook_dir
74
+ )
75
+ else:
76
+ # Log the file upload attempt for regular (non-chunked) uploads
77
+ file_extension = filename.split(".")[-1].lower()
78
+ log_file_upload_attempt(filename, file_extension, False, 0)
79
+ self._handle_regular_upload(filename, file_data, notebook_dir)
80
+
81
+ self.finish()
82
+
83
+ except Exception as e:
84
+ self._handle_error(str(e))
85
+
86
+ def _validate_file_upload(self) -> bool:
87
+ """Validate that a file was uploaded in the request."""
88
+ if "file" not in self.request.files:
89
+ self._handle_error("No file uploaded", status_code=400)
90
+ return False
91
+ return True
92
+
93
+ def _handle_chunked_upload(
94
+ self,
95
+ filename: str,
96
+ file_data: bytes,
97
+ chunk_number: str,
98
+ total_chunks: str,
99
+ notebook_dir: str,
100
+ ) -> None:
101
+ """Handle chunked file upload."""
102
+ chunk_num = int(chunk_number)
103
+ total_chunks_num = int(total_chunks)
104
+
105
+ # Log the file upload attempt only for the first chunk
106
+ if chunk_num == 1:
107
+ file_extension = filename.split(".")[-1].lower()
108
+ log_file_upload_attempt(filename, file_extension, True, total_chunks_num)
109
+
110
+ # Save chunk to temporary file
111
+ self._save_chunk(filename, file_data, chunk_num, total_chunks_num)
112
+
113
+ # Check if all chunks are received and reconstruct if complete
114
+ if self._are_all_chunks_received(filename, total_chunks_num):
115
+ self._reconstruct_file(filename, total_chunks_num, notebook_dir)
116
+ self._send_chunk_complete_response(filename, notebook_dir)
117
+ else:
118
+ self._send_chunk_received_response(chunk_num, total_chunks_num)
119
+
120
+ def _handle_regular_upload(
121
+ self, filename: str, file_data: bytes, notebook_dir: str
122
+ ) -> None:
123
+ """Handle regular (non-chunked) file upload."""
124
+ # Check image file size limit before saving
125
+ _check_image_size_limit(file_data, filename)
126
+
127
+ file_path = os.path.join(notebook_dir, filename)
128
+ with open(file_path, "wb") as f:
129
+ f.write(file_data)
130
+
131
+ self.write({"success": True, "filename": filename, "path": file_path})
132
+
133
+ def _save_chunk(
134
+ self, filename: str, file_data: bytes, chunk_number: int, total_chunks: int
135
+ ) -> None:
136
+ """Save a chunk to a temporary file."""
137
+ # Initialize temporary directory for this file if it doesn't exist
138
+ if filename not in self._temp_dirs:
139
+ temp_dir = tempfile.mkdtemp(prefix=f"mito_upload_{filename}_")
140
+ self._temp_dirs[filename] = {
141
+ "temp_dir": temp_dir,
142
+ "total_chunks": total_chunks,
143
+ "received_chunks": set(),
144
+ }
145
+
146
+ # Save the chunk to the temporary directory
147
+ chunk_filename = os.path.join(
148
+ self._temp_dirs[filename]["temp_dir"], f"chunk_{chunk_number}"
149
+ )
150
+ with open(chunk_filename, "wb") as f:
151
+ f.write(file_data)
152
+
153
+ # Mark this chunk as received
154
+ self._temp_dirs[filename]["received_chunks"].add(chunk_number)
155
+
156
+ def _are_all_chunks_received(self, filename: str, total_chunks: int) -> bool:
157
+ """Check if all chunks for a file have been received."""
158
+ if filename not in self._temp_dirs:
159
+ return False
160
+
161
+ received_chunks = self._temp_dirs[filename]["received_chunks"]
162
+ is_complete = len(received_chunks) == total_chunks
163
+ return is_complete
164
+
165
+ def _reconstruct_file(
166
+ self, filename: str, total_chunks: int, notebook_dir: str
167
+ ) -> None:
168
+ """Reconstruct the final file from all chunks and clean up temporary directory."""
169
+
170
+ if filename not in self._temp_dirs:
171
+ raise ValueError(f"No temporary directory found for file: {filename}")
172
+
173
+ temp_dir = self._temp_dirs[filename]["temp_dir"]
174
+ file_path = os.path.join(notebook_dir, filename)
175
+
176
+ try:
177
+ # First, read all chunks to check total file size for images
178
+ all_file_data = b""
179
+ for i in range(1, total_chunks + 1):
180
+ chunk_filename = os.path.join(temp_dir, f"chunk_{i}")
181
+ with open(chunk_filename, "rb") as chunk_file:
182
+ chunk_data = chunk_file.read()
183
+ all_file_data += chunk_data
184
+
185
+ # Check image file size limit before saving
186
+ _check_image_size_limit(all_file_data, filename)
187
+
188
+ # Write the complete file
189
+ with open(file_path, "wb") as final_file:
190
+ final_file.write(all_file_data)
191
+ finally:
192
+ # Clean up the temporary directory
193
+ self._cleanup_temp_dir(filename)
194
+
195
+ def _cleanup_temp_dir(self, filename: str) -> None:
196
+ """Clean up the temporary directory for a file."""
197
+ if filename in self._temp_dirs:
198
+ temp_dir = self._temp_dirs[filename]["temp_dir"]
199
+ try:
200
+ import shutil
201
+
202
+ shutil.rmtree(temp_dir)
203
+ except Exception as e:
204
+ # Log the error but don't fail the upload
205
+ print(
206
+ f"Warning: Failed to clean up temporary directory {temp_dir}: {e}"
207
+ )
208
+ finally:
209
+ # Remove from tracking dictionary
210
+ del self._temp_dirs[filename]
211
+
212
+ def _send_chunk_complete_response(self, filename: str, notebook_dir: str) -> None:
213
+ """Send response indicating all chunks have been processed and file is complete."""
214
+ file_path = os.path.join(notebook_dir, filename)
215
+ self.write(
216
+ {
217
+ "success": True,
218
+ "filename": filename,
219
+ "path": file_path,
220
+ "chunk_complete": True,
221
+ }
222
+ )
223
+
224
+ def _send_chunk_received_response(
225
+ self, chunk_number: int, total_chunks: int
226
+ ) -> None:
227
+ """Send response indicating a chunk was received but file is not yet complete."""
228
+ self.write(
229
+ {
230
+ "success": True,
231
+ "chunk_received": True,
232
+ "chunk_number": chunk_number,
233
+ "total_chunks": total_chunks,
234
+ }
235
+ )
236
+
237
+ def _handle_error(self, error_message: str, status_code: int = 500) -> None:
238
+ """Handle errors and send appropriate error response."""
239
+ log_file_upload_failure(error_message)
240
+ self.set_status(status_code)
241
+ self.write({"error": error_message})
242
+ self.finish()
243
+
244
+ def on_finish(self) -> None:
245
+ """Clean up any remaining temporary directories when the handler is finished."""
246
+ super().on_finish()
247
+ # Note: We don't clean up here anymore since we want to preserve state across requests
248
+ # The cleanup happens when the file is fully reconstructed
@@ -0,0 +1,21 @@
1
+ # Copyright (c) Saga Inc.
2
+ # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
+
4
+ from typing import List, Tuple, Any
5
+ from jupyter_server.utils import url_path_join
6
+ from mito_ai.file_uploads.handlers import FileUploadHandler
7
+
8
+
9
+ def get_file_uploads_urls(base_url: str) -> List[Tuple[str, Any, dict]]:
10
+ """Get all file uploads related URL patterns.
11
+
12
+ Args:
13
+ base_url: The base URL for the Jupyter server
14
+
15
+ Returns:
16
+ List of (url_pattern, handler_class, handler_kwargs) tuples
17
+ """
18
+ BASE_URL = base_url + "/mito-ai"
19
+ return [
20
+ (url_path_join(BASE_URL, "upload"), FileUploadHandler, {}),
21
+ ]
mito_ai/gemini_client.py CHANGED
@@ -5,10 +5,9 @@ from typing import Any, Callable, Dict, List, Optional, Union, Tuple
5
5
  from google import genai
6
6
  from google.genai import types
7
7
  from google.genai.types import GenerateContentConfig, Part, Content, GenerateContentResponse
8
- from mito_ai.completions.models import CompletionItem, CompletionReply, CompletionStreamChunk, MessageType, ResponseFormatInfo
8
+ from mito_ai.completions.models import CompletionError, CompletionItem, CompletionReply, CompletionStreamChunk, MessageType, ResponseFormatInfo
9
9
  from mito_ai.utils.gemini_utils import get_gemini_completion_from_mito_server, stream_gemini_completion_from_mito_server, get_gemini_completion_function_params
10
-
11
- GEMINI_FAST_MODEL = "gemini-2.0-flash-lite"
10
+ from mito_ai.utils.mito_server_utils import ProviderCompletionException
12
11
 
13
12
  def extract_and_parse_gemini_json_response(response: GenerateContentResponse) -> Optional[str]:
14
13
  """
@@ -100,65 +99,62 @@ def get_gemini_system_prompt_and_messages(messages: List[Dict[str, Any]]) -> Tup
100
99
 
101
100
 
102
101
  class GeminiClient:
103
- def __init__(self, api_key: Optional[str], model: str):
102
+ def __init__(self, api_key: Optional[str]):
104
103
  self.api_key = api_key
105
- self.model = model
106
104
  if api_key:
107
105
  self.client = genai.Client(api_key=api_key)
108
106
 
109
107
  async def request_completions(
110
108
  self,
111
109
  messages: List[Dict[str, Any]],
110
+ model: str,
112
111
  response_format_info: Optional[ResponseFormatInfo] = None,
113
112
  message_type: MessageType = MessageType.CHAT
114
113
  ) -> str:
115
- try:
116
- # Extract system instructions and contents
117
- system_instructions, contents = get_gemini_system_prompt_and_messages(messages)
118
-
119
- # Get provider data for Gemini completion
120
- provider_data = get_gemini_completion_function_params(
121
- model=self.model if response_format_info else GEMINI_FAST_MODEL,
122
- contents=contents,
114
+ # Extract system instructions and contents
115
+ system_instructions, contents = get_gemini_system_prompt_and_messages(messages)
116
+
117
+ # Get provider data for Gemini completion
118
+ provider_data = get_gemini_completion_function_params(
119
+ model=model,
120
+ contents=contents,
121
+ message_type=message_type,
122
+ response_format_info=response_format_info
123
+ )
124
+
125
+ if self.api_key:
126
+ # Generate content using the Gemini client
127
+ response_config = GenerateContentConfig(
128
+ system_instruction=system_instructions,
129
+ response_mime_type=provider_data.get("config", {}).get("response_mime_type"),
130
+ response_schema=provider_data.get("config", {}).get("response_schema")
131
+ )
132
+ response = self.client.models.generate_content(
133
+ model=provider_data["model"],
134
+ contents=contents, # type: ignore
135
+ config=response_config
136
+ )
137
+
138
+ result = extract_and_parse_gemini_json_response(response)
139
+
140
+ if not result:
141
+ return "No response received from Gemini API"
142
+
143
+ return result
144
+ else:
145
+ # Fallback to Mito server for completion
146
+ return await get_gemini_completion_from_mito_server(
147
+ model=provider_data["model"],
148
+ contents=messages, # Use the extracted contents instead of converted messages to avoid serialization issues
123
149
  message_type=message_type,
124
- response_format_info=response_format_info
150
+ config=provider_data.get("config", None),
151
+ response_format_info=response_format_info,
125
152
  )
126
153
 
127
- if self.api_key:
128
- # Generate content using the Gemini client
129
- response_config = GenerateContentConfig(
130
- system_instruction=system_instructions,
131
- response_mime_type=provider_data.get("config", {}).get("response_mime_type"),
132
- response_schema=provider_data.get("config", {}).get("response_schema")
133
- )
134
- response = self.client.models.generate_content(
135
- model=provider_data["model"],
136
- contents=contents, # type: ignore
137
- config=response_config
138
- )
139
-
140
- result = extract_and_parse_gemini_json_response(response)
141
-
142
- if not result:
143
- return "No response received from Gemini API"
144
-
145
- return result
146
- else:
147
- # Fallback to Mito server for completion
148
- return await get_gemini_completion_from_mito_server(
149
- model=provider_data["model"],
150
- contents=messages, # Use the extracted contents instead of converted messages to avoid serialization issues
151
- message_type=message_type,
152
- config=provider_data.get("config", None),
153
- response_format_info=response_format_info,
154
- )
155
-
156
- except Exception as e:
157
- return f"Error generating content: {str(e)}"
158
-
159
154
  async def stream_completions(
160
155
  self,
161
156
  messages: List[Dict[str, Any]],
157
+ model: str,
162
158
  message_id: str,
163
159
  reply_fn: Callable[[Union[CompletionReply, CompletionStreamChunk]], None],
164
160
  message_type: MessageType = MessageType.CHAT
@@ -169,7 +165,7 @@ class GeminiClient:
169
165
  system_instructions, contents = get_gemini_system_prompt_and_messages(messages)
170
166
  if self.api_key:
171
167
  for chunk in self.client.models.generate_content_stream(
172
- model=self.model,
168
+ model=model,
173
169
  contents=contents, # type: ignore
174
170
  config=GenerateContentConfig(
175
171
  system_instruction=system_instructions
@@ -208,7 +204,7 @@ class GeminiClient:
208
204
  return accumulated_response
209
205
  else:
210
206
  async for chunk_text in stream_gemini_completion_from_mito_server(
211
- model=self.model,
207
+ model=model,
212
208
  contents=messages, # Use the extracted contents instead of converted messages to avoid serialization issues
213
209
  message_type=message_type,
214
210
  message_id=message_id,