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
@@ -5,14 +5,17 @@ import asyncio
5
5
  import json
6
6
  import time
7
7
  from typing import Any, Dict, List, Optional, Callable, Union, AsyncGenerator, Tuple
8
- from tornado.httpclient import AsyncHTTPClient
8
+ from mito_ai.utils.mito_server_utils import get_response_from_mito_server, stream_response_from_mito_server
9
9
  from mito_ai.completions.models import AgentResponse, CompletionReply, CompletionStreamChunk, CompletionItem, MessageType
10
- from .utils import _create_http_client
11
10
  from mito_ai.constants import MITO_GEMINI_URL
11
+ from mito_ai.utils.provider_utils import does_message_require_fast_model
12
+ from mito_ai.utils.utils import _create_http_client
12
13
 
13
14
  timeout = 30
14
15
  max_retries = 1
15
16
 
17
+ FAST_GEMINI_MODEL = "gemini-2.0-flash-lite"
18
+
16
19
  def _prepare_gemini_request_data_and_headers(
17
20
  model: str,
18
21
  contents: List[Dict[str, Any]],
@@ -62,116 +65,58 @@ async def get_gemini_completion_from_mito_server(
62
65
  response_format_info: Optional[Any] = None
63
66
  ) -> str:
64
67
  data, headers = _prepare_gemini_request_data_and_headers(model, contents, message_type, config, response_format_info, stream=False)
65
- http_client, http_client_timeout = _create_http_client(timeout, max_retries)
66
- start_time = time.time()
67
- try:
68
- res = await http_client.fetch(
69
- MITO_GEMINI_URL,
70
- method="POST",
71
- headers=headers,
72
- body=json.dumps(data),
73
- request_timeout=http_client_timeout
74
- )
75
- print(f"Gemini request completed in {time.time() - start_time:.2f} seconds")
76
- except Exception as e:
77
- print(f"Gemini request failed after {time.time() - start_time:.2f} seconds with error: {str(e)}")
78
- raise
79
- finally:
80
- http_client.close()
81
-
82
- # The response is a string
83
- return res.body.decode("utf-8")
68
+ return await get_response_from_mito_server(
69
+ MITO_GEMINI_URL,
70
+ headers,
71
+ data,
72
+ timeout,
73
+ max_retries,
74
+ message_type,
75
+ provider_name="Gemini"
76
+ )
84
77
 
85
78
  async def stream_gemini_completion_from_mito_server(
86
79
  model: str,
87
80
  contents: List[Dict[str, Any]],
88
81
  message_type: MessageType,
89
82
  message_id: str,
90
- reply_fn: Optional[Callable[[Union[CompletionReply, CompletionStreamChunk]], None]]
83
+ reply_fn: Callable[[Union[CompletionReply, CompletionStreamChunk]], None]
91
84
  ) -> AsyncGenerator[str, None]:
92
85
  data, headers = _prepare_gemini_request_data_and_headers(model, contents, message_type, stream=True)
93
- http_client, http_client_timeout = _create_http_client(timeout, max_retries)
94
- start_time = time.time()
95
- chunk_queue: asyncio.Queue[str] = asyncio.Queue()
96
- fetch_complete = False
97
- def chunk_callback(chunk: bytes) -> None:
98
- try:
99
- chunk_str = chunk.decode('utf-8')
100
- asyncio.create_task(chunk_queue.put(chunk_str))
101
- except Exception as e:
102
- print(f"Error processing Gemini streaming chunk: {str(e)}")
103
- fetch_future = None
104
- try:
105
- fetch_future = http_client.fetch(
106
- MITO_GEMINI_URL,
107
- method="POST",
108
- headers=headers,
109
- body=json.dumps(data),
110
- request_timeout=http_client_timeout,
111
- streaming_callback=chunk_callback
112
- )
113
- async def wait_for_fetch() -> None:
114
- try:
115
- await fetch_future
116
- nonlocal fetch_complete
117
- fetch_complete = True
118
- print("Gemini fetch completed")
119
- except Exception as e:
120
- print(f"Error in Gemini fetch: {str(e)}")
121
- raise
122
- fetch_task = asyncio.create_task(wait_for_fetch())
123
- while not (fetch_complete and chunk_queue.empty()):
124
- try:
125
- chunk = await asyncio.wait_for(chunk_queue.get(), timeout=0.1)
126
- clean_chunk = chunk.strip('"')
127
- decoded_chunk = clean_chunk.encode().decode('unicode_escape')
128
- if reply_fn and message_id:
129
- reply_fn(CompletionStreamChunk(
130
- parent_id=message_id,
131
- chunk=CompletionItem(
132
- content=decoded_chunk,
133
- isIncomplete=True,
134
- token=message_id,
135
- ),
136
- done=False,
137
- ))
138
- yield chunk
139
- except asyncio.TimeoutError:
140
- if fetch_complete and chunk_queue.empty():
141
- break
142
- continue
143
- print(f"\nGemini stream completed in {time.time() - start_time:.2f} seconds")
144
- if reply_fn and message_id:
145
- reply_fn(CompletionStreamChunk(
146
- parent_id=message_id,
147
- chunk=CompletionItem(
148
- content="",
149
- isIncomplete=False,
150
- token=message_id,
151
- ),
152
- done=True,
153
- ))
154
- except Exception as e:
155
- print(f"\nGemini stream failed after {time.time() - start_time:.2f} seconds with error: {str(e)}")
156
- if fetch_future:
157
- try:
158
- await fetch_future
159
- except Exception:
160
- pass
161
- raise
162
- finally:
163
- http_client.close()
86
+
87
+ # Define chunk processor for Gemini's special processing
88
+ def gemini_chunk_processor(chunk: str) -> str:
89
+ clean_chunk = chunk.strip('"')
90
+ return clean_chunk.encode().decode('unicode_escape')
91
+
92
+ # Use the unified streaming function with Gemini's chunk processor
93
+ async for chunk in stream_response_from_mito_server(
94
+ url=MITO_GEMINI_URL,
95
+ headers=headers,
96
+ data=data,
97
+ timeout=timeout,
98
+ max_retries=max_retries,
99
+ message_type=message_type,
100
+ reply_fn=reply_fn,
101
+ message_id=message_id,
102
+ chunk_processor=gemini_chunk_processor,
103
+ provider_name="Gemini",
104
+ ):
105
+ yield chunk
164
106
 
165
107
  def get_gemini_completion_function_params(
108
+ message_type: MessageType,
166
109
  model: str,
167
110
  contents: list[dict[str, Any]],
168
- message_type: MessageType,
169
111
  response_format_info: Optional[Any] = None,
170
112
  ) -> Dict[str, Any]:
171
113
  """
172
114
  Build the provider_data dict for Gemini completions, mirroring the OpenAI/Anthropic approach.
173
115
  Only includes fields needed for the Gemini API.
174
116
  """
117
+ message_requires_fast_model = does_message_require_fast_model(message_type)
118
+ model = FAST_GEMINI_MODEL if message_requires_fast_model else model
119
+
175
120
  provider_data: Dict[str, Any] = {
176
121
  "model": model,
177
122
  "contents": contents,
@@ -3,12 +3,14 @@
3
3
 
4
4
  import re
5
5
  from typing import List
6
+ from mito_ai.constants import MESSAGE_HISTORY_TRIM_THRESHOLD
6
7
  from openai.types.chat import ChatCompletionMessageParam
7
8
  from mito_ai.completions.prompt_builders.prompt_constants import (
8
9
  ACTIVE_CELL_ID_SECTION_HEADING,
9
10
  ACTIVE_CELL_OUTPUT_SECTION_HEADING,
10
11
  GET_CELL_OUTPUT_TOOL_RESPONSE_SECTION_HEADING,
11
12
  FILES_SECTION_HEADING,
13
+ STREAMLIT_APP_STATUS_SECTION_HEADING,
12
14
  VARIABLES_SECTION_HEADING,
13
15
  JUPYTER_NOTEBOOK_SECTION_HEADING,
14
16
  CONTENT_REMOVED_PLACEHOLDER
@@ -30,7 +32,8 @@ def trim_sections_from_message_content(content: str) -> str:
30
32
  JUPYTER_NOTEBOOK_SECTION_HEADING,
31
33
  GET_CELL_OUTPUT_TOOL_RESPONSE_SECTION_HEADING,
32
34
  ACTIVE_CELL_OUTPUT_SECTION_HEADING,
33
- ACTIVE_CELL_ID_SECTION_HEADING
35
+ ACTIVE_CELL_ID_SECTION_HEADING,
36
+ STREAMLIT_APP_STATUS_SECTION_HEADING
34
37
  ]
35
38
 
36
39
  for heading in section_headings:
@@ -43,18 +46,18 @@ def trim_sections_from_message_content(content: str) -> str:
43
46
  return content
44
47
 
45
48
 
46
- def trim_old_messages(messages: List[ChatCompletionMessageParam], keep_recent: int = 3) -> List[ChatCompletionMessageParam]:
49
+ def trim_old_messages(messages: List[ChatCompletionMessageParam]) -> List[ChatCompletionMessageParam]:
47
50
  """
48
51
  Trims metadata sections from messages that are older than the specified number of recent messages.
49
52
  We do this in order to reduce the token count of the messages, which helps us stay under the token limit for the LLM.
50
53
  """
51
- if len(messages) <= keep_recent:
54
+ if len(messages) <= MESSAGE_HISTORY_TRIM_THRESHOLD:
52
55
  return messages
53
56
 
54
57
  # Process all messages except the keep_recent most recent ones.
55
58
  # Only trim user messages, which is where this metadata lives.
56
59
  # We want to not edit the system messages, as they contain important information / examples.
57
- for i in range(len(messages) - keep_recent):
60
+ for i in range(len(messages) - MESSAGE_HISTORY_TRIM_THRESHOLD):
58
61
  content = messages[i].get("content")
59
62
 
60
63
  is_user_message = messages[i].get("role") == "user"
@@ -0,0 +1,242 @@
1
+ # Copyright (c) Saga Inc.
2
+ # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
+
4
+ import asyncio
5
+ import json
6
+ import time
7
+ from typing import Any, Dict, Optional, Callable, Union, AsyncGenerator
8
+ from mito_ai.completions.models import MessageType, CompletionReply, CompletionStreamChunk, CompletionItem
9
+ from mito_ai.utils.server_limits import check_mito_server_quota, update_mito_server_quota
10
+ from tornado.httpclient import HTTPResponse
11
+ from mito_ai.constants import MITO_GEMINI_URL
12
+ from mito_ai.utils.utils import _create_http_client
13
+
14
+ MITO_ERROR_MARKER = "MITO_ERROR_MARKER:"
15
+
16
+ class ProviderCompletionException(Exception):
17
+ """Custom exception for Mito server errors that converts well to CompletionError."""
18
+
19
+ def __init__(self, error_message: str, provider_name: str = "LLM Provider", error_type: str = "LLMProviderError"):
20
+ self.error_message = error_message
21
+ self.provider_name = provider_name
22
+ self.error_type = error_type
23
+
24
+ # Create user-friendly title and hint
25
+ self.user_friendly_title = f"{provider_name} Error: {error_message}"
26
+ self.user_friendly_hint = f"There was a problem with {provider_name}. Try switching to a different model and trying again."
27
+
28
+ # Set args[0] for fallback compatibility
29
+ super().__init__(self.user_friendly_title)
30
+
31
+ def __str__(self) -> str:
32
+ return f"{self.provider_name} Error: {self.error_message}"
33
+
34
+
35
+ async def get_response_from_mito_server(
36
+ url: str,
37
+ headers: dict,
38
+ data: Dict[str, Any],
39
+ timeout: int,
40
+ max_retries: int,
41
+ message_type: MessageType,
42
+ provider_name: str = "Mito Server"
43
+ ) -> str:
44
+ """
45
+ Get a response from the Mito server.
46
+
47
+ Raises:
48
+ ProviderCompletionException: When the server returns an error or invalid response
49
+ Exception: For network/HTTP errors (let these bubble up to be handled by retry logic)
50
+ """
51
+ # First check the mito server quota. If the user has reached the limit, we raise an exception.
52
+ check_mito_server_quota(message_type)
53
+
54
+ http_client, http_client_timeout = _create_http_client(timeout, max_retries)
55
+ start_time = time.time()
56
+
57
+ try:
58
+ res = await http_client.fetch(
59
+ url,
60
+ method="POST",
61
+ headers=headers,
62
+ body=json.dumps(data),
63
+ request_timeout=http_client_timeout
64
+ )
65
+ print(f"Mito server request completed in {time.time() - start_time:.2f} seconds")
66
+
67
+ # Parse and validate response
68
+ try:
69
+ content = json.loads(res.body.decode("utf-8"))
70
+
71
+ if "completion" in content:
72
+ return content["completion"] # type: ignore
73
+ elif "error" in content:
74
+ # Server returned an error
75
+ raise ProviderCompletionException(content['error'], provider_name=provider_name)
76
+ else:
77
+ # Invalid response format
78
+ raise ProviderCompletionException(f"No completion found in response: {content}", provider_name=provider_name)
79
+ except ProviderCompletionException:
80
+ # Re-raise ProviderCompletionException as-is
81
+ raise
82
+ except Exception as e:
83
+ raise ProviderCompletionException(f"Error parsing response: {str(e)}", provider_name=provider_name)
84
+
85
+ finally:
86
+ try:
87
+ # We always update the quota, even if there is an error
88
+ update_mito_server_quota(message_type)
89
+ except Exception as e:
90
+ pass
91
+
92
+ http_client.close()
93
+
94
+
95
+ async def stream_response_from_mito_server(
96
+ url: str,
97
+ headers: Dict[str, str],
98
+ data: Dict[str, Any],
99
+ timeout: int,
100
+ max_retries: int,
101
+ message_type: MessageType,
102
+ reply_fn: Callable[[Union[CompletionReply, CompletionStreamChunk]], None],
103
+ message_id: str,
104
+ chunk_processor: Optional[Callable[[str], str]] = None,
105
+ provider_name: str = "Mito Server",
106
+ ) -> AsyncGenerator[str, None]:
107
+ """
108
+ Stream responses from the Mito server.
109
+
110
+ This is a unified streaming function that can be used by all providers (OpenAI, Anthropic, Gemini).
111
+
112
+ Args:
113
+ url: The Mito server URL to stream from
114
+ headers: Request headers
115
+ data: Request data
116
+ timeout: Request timeout in seconds
117
+ max_retries: Maximum number of retries
118
+ message_type: The message type for quota tracking
119
+ provider_name: Name of the provider for error messages
120
+ reply_fn: Optional function to call with each chunk for streaming replies
121
+ message_id: The message ID to track the request
122
+ chunk_processor: Optional function to process chunks before yielding (e.g., for Gemini's special processing)
123
+
124
+ Yields:
125
+ Chunks of text from the streaming response
126
+ """
127
+ # Check the mito server quota
128
+ check_mito_server_quota(message_type)
129
+
130
+ # Create HTTP client with appropriate timeout settings
131
+ http_client, http_client_timeout = _create_http_client(timeout, max_retries)
132
+
133
+ # Set up streaming infrastructure
134
+ start_time = time.time()
135
+ chunk_queue: asyncio.Queue[str] = asyncio.Queue()
136
+ fetch_complete = False
137
+
138
+ # Define a callback to process chunks and add them to the queue
139
+ def chunk_callback(chunk: bytes) -> None:
140
+ try:
141
+ chunk_str = chunk.decode('utf-8')
142
+ asyncio.create_task(chunk_queue.put(chunk_str))
143
+ except Exception as e:
144
+ print(f"Error processing {provider_name} streaming chunk: {str(e)}")
145
+
146
+ # Execute the streaming request
147
+ fetch_future = None
148
+ try:
149
+ fetch_future = http_client.fetch(
150
+ url,
151
+ method="POST",
152
+ headers=headers,
153
+ body=json.dumps(data),
154
+ request_timeout=http_client_timeout,
155
+ streaming_callback=chunk_callback
156
+ )
157
+
158
+ # Create a task to wait for the fetch to complete
159
+ async def wait_for_fetch() -> None:
160
+ try:
161
+ await fetch_future
162
+ nonlocal fetch_complete
163
+ fetch_complete = True
164
+ print(f"{provider_name} fetch completed")
165
+ except Exception as e:
166
+ print(f"Error in {provider_name} fetch: {str(e)}")
167
+ raise
168
+
169
+ # Start the task to wait for fetch completion
170
+ fetch_task = asyncio.create_task(wait_for_fetch())
171
+
172
+ # Yield chunks as they arrive
173
+ while not (fetch_complete and chunk_queue.empty()):
174
+ try:
175
+ # Wait for a chunk with a timeout to prevent deadlocks
176
+ chunk = await asyncio.wait_for(chunk_queue.get(), timeout=0.1)
177
+
178
+ # Process chunk if processor is provided
179
+ processed_chunk = chunk
180
+ if chunk_processor:
181
+ processed_chunk = chunk_processor(chunk)
182
+
183
+ # Check if this chunk contains an error marker
184
+ if processed_chunk.startswith(MITO_ERROR_MARKER):
185
+ error_message = processed_chunk[len(MITO_ERROR_MARKER):]
186
+ print(f"Detected error in {provider_name} stream: {error_message}")
187
+ raise ProviderCompletionException(error_message, provider_name=provider_name)
188
+
189
+ if reply_fn is not None and message_id is not None:
190
+ # Send the chunk directly to the frontend
191
+ reply_fn(CompletionStreamChunk(
192
+ parent_id=message_id,
193
+ chunk=CompletionItem(
194
+ content=processed_chunk,
195
+ isIncomplete=True,
196
+ token=message_id,
197
+ ),
198
+ done=False,
199
+ ))
200
+
201
+ yield chunk
202
+ except asyncio.TimeoutError:
203
+ # No chunk available within timeout, check if fetch is complete
204
+ if fetch_complete and chunk_queue.empty():
205
+ break
206
+
207
+ # Otherwise continue waiting for chunks
208
+ continue
209
+
210
+ print(f"\n{provider_name} stream completed in {time.time() - start_time:.2f} seconds")
211
+
212
+ if reply_fn is not None and message_id is not None:
213
+ # Send a final chunk to indicate completion
214
+ reply_fn(CompletionStreamChunk(
215
+ parent_id=message_id,
216
+ chunk=CompletionItem(
217
+ content="",
218
+ isIncomplete=False,
219
+ token=message_id,
220
+ ),
221
+ done=True,
222
+ ))
223
+ except Exception as e:
224
+ print(f"\n{provider_name} stream failed after {time.time() - start_time:.2f} seconds with error: {str(e)}")
225
+ # If an exception occurred, ensure the fetch future is awaited to properly clean up
226
+ if fetch_future:
227
+ try:
228
+ await fetch_future
229
+ except Exception:
230
+ pass
231
+ raise
232
+ finally:
233
+ # Clean up resources
234
+ try:
235
+ # We always update the quota, even if there is an error
236
+ update_mito_server_quota(message_type)
237
+ except Exception as e:
238
+ pass
239
+
240
+ http_client.close()
241
+
242
+