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,469 @@
1
+ # Copyright (c) Saga Inc.
2
+ # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
+
4
+ import pytest
5
+ from typing import Callable, List, cast
6
+ from openai.types.chat import ChatCompletionMessageParam
7
+ from mito_ai.utils.message_history_utils import trim_sections_from_message_content, trim_old_messages
8
+ from mito_ai.completions.prompt_builders.chat_prompt import create_chat_prompt
9
+ from mito_ai.completions.prompt_builders.agent_execution_prompt import create_agent_execution_prompt
10
+ from mito_ai.completions.prompt_builders.agent_smart_debug_prompt import create_agent_smart_debug_prompt
11
+ from unittest.mock import Mock, patch
12
+ from mito_ai.completions.message_history import GlobalMessageHistory, ChatThread
13
+ from mito_ai.completions.prompt_builders.smart_debug_prompt import create_error_prompt
14
+ from mito_ai.completions.prompt_builders.explain_code_prompt import create_explain_code_prompt
15
+ from mito_ai.completions.models import (
16
+ AgentExecutionMetadata,
17
+ AgentSmartDebugMetadata,
18
+ AIOptimizedCell,
19
+ ThreadID,
20
+ )
21
+ from mito_ai.completions.prompt_builders.prompt_constants import (
22
+ FILES_SECTION_HEADING,
23
+ VARIABLES_SECTION_HEADING,
24
+ CODE_SECTION_HEADING,
25
+ ACTIVE_CELL_ID_SECTION_HEADING,
26
+ JUPYTER_NOTEBOOK_SECTION_HEADING,
27
+ CONTENT_REMOVED_PLACEHOLDER,
28
+ )
29
+
30
+
31
+
32
+
33
+ # Standard test data for multiple tests
34
+ TEST_VARIABLES = ["'df': pd.DataFrame({'col1': [1, 2, 3], 'col2': [4, 5, 6]})"]
35
+ TEST_FILES = ["data.csv", "script.py"]
36
+ TEST_CODE = "import pandas as pd\ndf = pd.read_csv('data.csv')"
37
+ TEST_INPUT = "Calculate the mean of col1"
38
+ TEST_ERROR = "AttributeError: 'Series' object has no attribute 'mena'"
39
+
40
+ def test_trim_sections_basic() -> None:
41
+ """Test trimming sections on a simple string with all section types."""
42
+ content = f"""Some text before.
43
+
44
+ {FILES_SECTION_HEADING}
45
+ file1.csv
46
+ file2.txt
47
+ file3.py
48
+
49
+ {VARIABLES_SECTION_HEADING}
50
+ var1 = 1
51
+ var2 = "string"
52
+ var3 = [1, 2, 3]
53
+
54
+ {JUPYTER_NOTEBOOK_SECTION_HEADING}
55
+ [
56
+ {{
57
+ "cell_type": "code",
58
+ "id": "cell1",
59
+ "code": "print('hello world')"
60
+ }}
61
+ ]
62
+
63
+ {ACTIVE_CELL_ID_SECTION_HEADING}
64
+ cell1
65
+
66
+ {CODE_SECTION_HEADING}
67
+ ```python
68
+ def hello():
69
+ print("world")
70
+ ```
71
+
72
+ Some text after."""
73
+
74
+ result = trim_sections_from_message_content(content)
75
+
76
+ # Verify sections are replaced with placeholders
77
+ assert f"{FILES_SECTION_HEADING} {CONTENT_REMOVED_PLACEHOLDER}" in result
78
+ assert f"{VARIABLES_SECTION_HEADING} {CONTENT_REMOVED_PLACEHOLDER}" in result
79
+ assert f"{JUPYTER_NOTEBOOK_SECTION_HEADING} {CONTENT_REMOVED_PLACEHOLDER}" in result
80
+ assert f"{ACTIVE_CELL_ID_SECTION_HEADING} {CONTENT_REMOVED_PLACEHOLDER}" in result
81
+
82
+ # Verify sections are not in the result anymore
83
+ assert "file1.csv" not in result
84
+ assert "var1 = 1" not in result
85
+ assert "cell_type" not in result
86
+ assert "cell1" not in result
87
+
88
+ # Verify other content is preserved
89
+ assert "Some text before." in result
90
+ assert "Some text after." in result
91
+ assert f"{CODE_SECTION_HEADING}" in result
92
+ assert "def hello():" in result
93
+
94
+
95
+ # Parameterized test cases for prompt builders (except inline completer)
96
+ PROMPT_BUILDER_TEST_CASES = [
97
+ # Chat prompt
98
+ (
99
+ lambda: create_chat_prompt(TEST_VARIABLES, TEST_FILES, TEST_CODE, "cell1", False, TEST_INPUT),
100
+ ["Your task: Calculate the mean of col1"],
101
+ ["data.csv\nscript.py", f"{VARIABLES_SECTION_HEADING}\n'df': pd.DataFrame"],
102
+ ),
103
+ # Agent execution prompt
104
+ (
105
+ lambda: create_agent_execution_prompt(
106
+ AgentExecutionMetadata(
107
+ variables=TEST_VARIABLES,
108
+ files=TEST_FILES,
109
+ notebookPath='/test-notebook-path.ipynb',
110
+ notebookID='test-notebook-id',
111
+ aiOptimizedCells=[
112
+ AIOptimizedCell(cell_type="code", id="cell1", code=TEST_CODE)
113
+ ],
114
+ input=TEST_INPUT,
115
+ promptType="agent:execution",
116
+ threadId=ThreadID("test-thread-id"),
117
+ activeCellId="cell1",
118
+ isChromeBrowser=True
119
+ )
120
+ ),
121
+ ["Your task: \nCalculate the mean of col1"],
122
+ ["data.csv\nscript.py", f"'df': pd.DataFrame", "import pandas as pd"],
123
+ ),
124
+ # Smart debug prompt
125
+ (
126
+ lambda: create_error_prompt(TEST_ERROR, TEST_CODE, "cell1", TEST_VARIABLES, TEST_FILES),
127
+ ["Error Traceback:", TEST_ERROR],
128
+ [
129
+ f"{FILES_SECTION_HEADING}\ndata.csv\nscript.py",
130
+ f"{VARIABLES_SECTION_HEADING}\n'df': pd.DataFrame",
131
+ f"{ACTIVE_CELL_ID_SECTION_HEADING}\ncell1",
132
+ ],
133
+ ),
134
+ # Explain code prompt (doesn't have sections to trim)
135
+ (
136
+ lambda: create_explain_code_prompt(TEST_CODE),
137
+ ["import pandas as pd"],
138
+ [],
139
+ ),
140
+ # Agent smart debug prompt
141
+ (
142
+ lambda: create_agent_smart_debug_prompt(
143
+ AgentSmartDebugMetadata(
144
+ variables=TEST_VARIABLES,
145
+ files=TEST_FILES,
146
+ aiOptimizedCells=[
147
+ AIOptimizedCell(cell_type="code", id="cell1", code=TEST_CODE)
148
+ ],
149
+ error_message_producing_code_cell_id="cell1",
150
+ errorMessage=TEST_ERROR,
151
+ promptType="agent:autoErrorFixup",
152
+ threadId=ThreadID("test-thread-id"),
153
+ isChromeBrowser=True
154
+ )
155
+ ),
156
+ ["Error Traceback:", TEST_ERROR],
157
+ [
158
+ f"{FILES_SECTION_HEADING}\n{TEST_FILES[0]}",
159
+ f"{VARIABLES_SECTION_HEADING}\n{TEST_VARIABLES[0]}",
160
+ "cell_type",
161
+ ],
162
+ ),
163
+ ]
164
+
165
+
166
+ @pytest.mark.parametrize("prompt_builder,expected_in_result,expected_not_in_result", PROMPT_BUILDER_TEST_CASES)
167
+ def test_prompt_builder_trimming(prompt_builder: Callable[[], str], expected_in_result: List[str], expected_not_in_result: List[str]) -> None:
168
+ """Test trimming for different prompt builders."""
169
+ # Create prompt using the provided builder function
170
+ content = prompt_builder()
171
+
172
+ # Trim the content
173
+ result = trim_sections_from_message_content(content)
174
+
175
+ # If none of the section headings are present, the content shouldn't change
176
+ has_sections_to_trim = any(
177
+ heading in content
178
+ for heading in [FILES_SECTION_HEADING, VARIABLES_SECTION_HEADING, JUPYTER_NOTEBOOK_SECTION_HEADING, ACTIVE_CELL_ID_SECTION_HEADING]
179
+ )
180
+
181
+ if not has_sections_to_trim:
182
+ assert result == content
183
+ # Verify expected content is still in the result
184
+ for expected in expected_in_result:
185
+ assert expected in result
186
+ return
187
+
188
+ # Check for each section if it was present in the original content
189
+ if FILES_SECTION_HEADING in content:
190
+ assert f"{FILES_SECTION_HEADING} {CONTENT_REMOVED_PLACEHOLDER}" in result
191
+
192
+ if VARIABLES_SECTION_HEADING in content:
193
+ assert f"{VARIABLES_SECTION_HEADING} {CONTENT_REMOVED_PLACEHOLDER}" in result
194
+
195
+ if JUPYTER_NOTEBOOK_SECTION_HEADING in content:
196
+ assert (
197
+ f"{JUPYTER_NOTEBOOK_SECTION_HEADING} {CONTENT_REMOVED_PLACEHOLDER}"
198
+ in result
199
+ )
200
+
201
+ if ACTIVE_CELL_ID_SECTION_HEADING in content:
202
+ assert f"{ACTIVE_CELL_ID_SECTION_HEADING} {CONTENT_REMOVED_PLACEHOLDER}" in result
203
+
204
+ # Verify expected content is still in the result
205
+ for expected in expected_in_result:
206
+ assert expected in result
207
+
208
+ # Verify content that should be removed is not in the result
209
+ for not_expected in expected_not_in_result:
210
+ assert not_expected not in result
211
+
212
+ def test_no_sections_to_trim() -> None:
213
+ """Test trimming content with no sections to trim."""
214
+ content = "This is a simple message with no sections to trim."
215
+ result = trim_sections_from_message_content(content)
216
+ assert result == content
217
+
218
+
219
+ # Tests for trim_old_messages function
220
+ def test_trim_old_messages_only_trims_user_messages() -> None:
221
+ """Test that trim_old_messages only trims content from user messages."""
222
+ # Create test messages with different roles
223
+ user_message_with_sections = f"""User prompt with sections.
224
+ {FILES_SECTION_HEADING}
225
+ file1.csv
226
+ file2.txt
227
+ """
228
+ system_message_with_sections = f"""System message with sections.
229
+ {FILES_SECTION_HEADING}
230
+ file1.csv
231
+ file2.txt
232
+ """
233
+ assistant_message_with_sections = f"""Assistant message with sections.
234
+ {FILES_SECTION_HEADING}
235
+ file1.csv
236
+ file2.txt
237
+ """
238
+
239
+ # Create test messages with proper typing
240
+ messages: List[ChatCompletionMessageParam] = [
241
+ {"role": "system", "content": system_message_with_sections},
242
+ {"role": "user", "content": user_message_with_sections},
243
+ {"role": "assistant", "content": assistant_message_with_sections},
244
+ {"role": "user", "content": "Recent user message 1"},
245
+ {"role": "user", "content": "Recent user message 2"},
246
+ {"role": "user", "content": "Recent user message 3"},
247
+ ]
248
+
249
+ result = trim_old_messages(messages)
250
+
251
+ # System message should remain unchanged even though it's old
252
+ system_content = result[0].get("content")
253
+ assert isinstance(system_content, str)
254
+ assert system_content == system_message_with_sections
255
+ assert FILES_SECTION_HEADING in system_content
256
+ assert "file1.csv" in system_content
257
+
258
+ # First user message should be trimmed
259
+ assert result[1]["role"] == "user"
260
+ user_content = result[1].get("content")
261
+ assert isinstance(user_content, str)
262
+ assert f"{FILES_SECTION_HEADING} {CONTENT_REMOVED_PLACEHOLDER}" in user_content
263
+ assert "file1.csv" not in user_content
264
+
265
+ # Assistant message should remain unchanged even though it's old
266
+ assistant_content = result[2].get("content")
267
+ assert isinstance(assistant_content, str)
268
+ assert assistant_content == assistant_message_with_sections
269
+ assert FILES_SECTION_HEADING in assistant_content
270
+ assert "file1.csv" in assistant_content
271
+
272
+ # Recent user messages should remain unchanged
273
+ recent_content_1 = result[3].get("content")
274
+ assert isinstance(recent_content_1, str)
275
+ assert recent_content_1 == "Recent user message 1"
276
+
277
+ recent_content_2 = result[4].get("content")
278
+ assert isinstance(recent_content_2, str)
279
+ assert recent_content_2 == "Recent user message 2"
280
+
281
+ recent_content_3 = result[5].get("content")
282
+ assert isinstance(recent_content_3, str)
283
+ assert recent_content_3 == "Recent user message 3"
284
+
285
+
286
+ def test_trim_old_messages_preserves_recent_messages() -> None:
287
+ """Test that trim_old_messages preserves the most recent messages based on MESSAGE_HISTORY_TRIM_THRESHOLD."""
288
+ # Create test messages
289
+ old_message_1 = f"""Old message 1.
290
+ {FILES_SECTION_HEADING}
291
+ file1.csv
292
+ """
293
+ old_message_2 = f"""Old message 2.
294
+ {FILES_SECTION_HEADING}
295
+ file2.csv
296
+ """
297
+ recent_message_1 = f"""Recent message 1.
298
+ {FILES_SECTION_HEADING}
299
+ file3.csv
300
+ """
301
+ recent_message_2 = f"""Recent message 2.
302
+ {FILES_SECTION_HEADING}
303
+ file4.csv
304
+ """
305
+ recent_message_3 = f"""Recent message 3.
306
+ {FILES_SECTION_HEADING}
307
+ file5.csv
308
+ """
309
+
310
+ # Create test messages with proper typing
311
+ messages: List[ChatCompletionMessageParam] = [
312
+ {"role": "user", "content": old_message_1},
313
+ {"role": "user", "content": old_message_2},
314
+ {"role": "user", "content": recent_message_1},
315
+ {"role": "user", "content": recent_message_2},
316
+ {"role": "user", "content": recent_message_3},
317
+ ]
318
+
319
+ # Test with MESSAGE_HISTORY_TRIM_THRESHOLD (3) - only the first 2 messages should be trimmed
320
+ result = trim_old_messages(messages)
321
+
322
+ # Old messages should be trimmed
323
+ old_content_1 = result[0].get("content")
324
+ assert isinstance(old_content_1, str)
325
+ assert f"{FILES_SECTION_HEADING} {CONTENT_REMOVED_PLACEHOLDER}" in old_content_1
326
+ assert "file1.csv" not in old_content_1
327
+
328
+ old_content_2 = result[1].get("content")
329
+ assert isinstance(old_content_2, str)
330
+ assert f"{FILES_SECTION_HEADING} {CONTENT_REMOVED_PLACEHOLDER}" in old_content_2
331
+ assert "file2.csv" not in old_content_2
332
+
333
+ # Recent messages should remain unchanged
334
+ recent_content_1 = result[2].get("content")
335
+ assert isinstance(recent_content_1, str)
336
+ assert recent_content_1 == recent_message_1
337
+ assert FILES_SECTION_HEADING in recent_content_1
338
+ assert "file3.csv" in recent_content_1
339
+
340
+ recent_content_2 = result[3].get("content")
341
+ assert isinstance(recent_content_2, str)
342
+ assert recent_content_2 == recent_message_2
343
+ assert FILES_SECTION_HEADING in recent_content_2
344
+ assert "file4.csv" in recent_content_2
345
+
346
+ recent_content_3 = result[4].get("content")
347
+ assert isinstance(recent_content_3, str)
348
+ assert recent_content_3 == recent_message_3
349
+ assert FILES_SECTION_HEADING in recent_content_3
350
+ assert "file5.csv" in recent_content_3
351
+
352
+ def test_trim_old_messages_empty_list() -> None:
353
+ """Test that trim_old_messages handles empty message lists correctly."""
354
+ messages: List[ChatCompletionMessageParam] = []
355
+ result = trim_old_messages(messages)
356
+ assert result == []
357
+
358
+
359
+ def test_trim_old_messages_fewer_than_threshold() -> None:
360
+ """Test that trim_old_messages doesn't modify messages if there are fewer than MESSAGE_HISTORY_TRIM_THRESHOLD."""
361
+ messages: List[ChatCompletionMessageParam] = [
362
+ {"role": "user", "content": "User message 1"},
363
+ {"role": "assistant", "content": "Assistant message 1"},
364
+ ]
365
+
366
+ result = trim_old_messages(messages)
367
+
368
+ # Messages should remain unchanged since we have fewer than MESSAGE_HISTORY_TRIM_THRESHOLD (3) messages
369
+ user_content = result[0].get("content")
370
+ assert isinstance(user_content, str)
371
+ assert user_content == "User message 1"
372
+
373
+ assistant_content = result[1].get("content")
374
+ assert isinstance(assistant_content, str)
375
+ assert assistant_content == "Assistant message 1"
376
+
377
+
378
+ def test_trim_mixed_content_messages() -> None:
379
+ """
380
+ Tests that when a message contains sections other than text (like image_url),
381
+ those sections are removed completely, leaving only the text content.
382
+ """
383
+ # Create sample message with mixed content (text and image)
384
+ mixed_content_message = cast(ChatCompletionMessageParam, {
385
+ "role": "user",
386
+ "content": [
387
+ {
388
+ "type": "text",
389
+ "text": "What is in this image?"
390
+ },
391
+ {
392
+ "type": "image_url",
393
+ "image_url": {"url": "data:image/png;base64,someimagedata"}
394
+ }
395
+ ]
396
+ })
397
+
398
+ # Create sample message list with one old message (the mixed content)
399
+ # and enough recent messages to exceed MESSAGE_HISTORY_TRIM_THRESHOLD (3)
400
+ message_list: List[ChatCompletionMessageParam] = [
401
+ mixed_content_message, # This should get trimmed
402
+ {"role": "assistant", "content": "That's a chart showing data trends"},
403
+ {"role": "user", "content": "Can you explain more?"}, # Recent message, should not be trimmed
404
+ {"role": "user", "content": "Another recent message"}, # Recent message, should not be trimmed
405
+ {"role": "user", "content": "Yet another recent message"} # Recent message, should not be trimmed
406
+ ]
407
+
408
+ # Apply the trimming function
409
+ trimmed_messages = trim_old_messages(message_list)
410
+
411
+ # Verify that the first message has been trimmed properly
412
+ assert trimmed_messages[0]["role"] == "user"
413
+ assert trimmed_messages[0]["content"] == "What is in this image?"
414
+
415
+ # Verify that the recent messages are untouched
416
+ assert trimmed_messages[1] == message_list[1]
417
+ assert trimmed_messages[2] == message_list[2]
418
+ assert trimmed_messages[3] == message_list[3]
419
+ assert trimmed_messages[4] == message_list[4]
420
+
421
+
422
+ def test_get_display_history_calls_update_last_interaction() -> None:
423
+ """Test that get_display_history calls _update_last_interaction when retrieving a thread."""
424
+
425
+ # Create a mock thread
426
+ thread_id = ThreadID("test-thread-id")
427
+ mock_thread = Mock(spec=ChatThread)
428
+ mock_thread.display_history = [{"role": "user", "content": "test message"}]
429
+ mock_thread.last_interaction_ts = 1234567890.0
430
+
431
+ # Create message history instance and add the mock thread
432
+ message_history = GlobalMessageHistory()
433
+ message_history._chat_threads = {thread_id: mock_thread}
434
+
435
+ # Mock the _update_last_interaction method
436
+ with patch.object(message_history, '_update_last_interaction') as mock_update:
437
+ with patch.object(message_history, '_save_thread_to_disk') as mock_save:
438
+ # Call get_display_history
439
+ result = message_history.get_display_history(thread_id)
440
+
441
+ # Verify _update_last_interaction was called with the thread
442
+ mock_update.assert_called_once_with(mock_thread)
443
+
444
+ # Verify _save_thread_to_disk was also called
445
+ mock_save.assert_called_once_with(mock_thread)
446
+
447
+ # Verify the result is correct
448
+ assert result == [{"role": "user", "content": "test message"}]
449
+
450
+
451
+ def test_get_display_history_returns_empty_for_nonexistent_thread() -> None:
452
+ """Test that get_display_history returns empty list for non-existent thread."""
453
+ from mito_ai.completions.message_history import GlobalMessageHistory
454
+ from mito_ai.completions.models import ThreadID
455
+
456
+ message_history = GlobalMessageHistory()
457
+ thread_id = ThreadID("nonexistent-thread-id")
458
+
459
+ # Mock the methods to ensure they're not called
460
+ with patch.object(message_history, '_update_last_interaction') as mock_update:
461
+ with patch.object(message_history, '_save_thread_to_disk') as mock_save:
462
+ result = message_history.get_display_history(thread_id)
463
+
464
+ # Verify methods were not called since thread doesn't exist
465
+ mock_update.assert_not_called()
466
+ mock_save.assert_not_called()
467
+
468
+ # Verify empty result
469
+ assert result == []
@@ -0,0 +1,152 @@
1
+ # Copyright (c) Saga Inc.
2
+ # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
+
4
+ import pytest
5
+ from datetime import datetime
6
+ from unittest.mock import patch
7
+ from mito_ai.utils.server_limits import (
8
+ check_mito_server_quota,
9
+ OS_MONTHLY_AI_COMPLETIONS_LIMIT,
10
+ OS_MONTHLY_AUTOCOMPLETE_LIMIT,
11
+ )
12
+ from mito_ai.completions.models import MessageType
13
+ from mito_ai.utils.open_ai_utils import _prepare_request_data_and_headers
14
+
15
+ REALLY_OLD_DATE = "2020-01-01"
16
+ TODAY = datetime.now().strftime("%Y-%m-%d")
17
+
18
+
19
+ def test_check_mito_server_quota_open_source_user() -> None:
20
+ # Under chat completions limit
21
+ with patch("mito_ai.utils.server_limits.get_chat_completion_count", return_value=1) as mock_count, \
22
+ patch("mito_ai.utils.server_limits.get_last_reset_date", return_value=TODAY) as mock_date, \
23
+ patch("mito_ai.utils.server_limits.is_pro", return_value=False):
24
+
25
+ check_mito_server_quota(MessageType.CHAT)
26
+ assert mock_count.called
27
+ assert mock_date.called
28
+ assert mock_count.return_value == 1
29
+ assert mock_date.return_value == TODAY
30
+
31
+ # Under autocomplete limit
32
+ with patch("mito_ai.utils.server_limits.get_autocomplete_count", return_value=1) as mock_count, \
33
+ patch("mito_ai.utils.server_limits.get_last_reset_date", return_value=TODAY) as mock_date, \
34
+ patch("mito_ai.utils.server_limits.is_pro", return_value=False):
35
+
36
+ check_mito_server_quota(MessageType.INLINE_COMPLETION)
37
+ assert mock_count.called
38
+ assert mock_date.called
39
+ assert mock_count.return_value == 1
40
+ assert mock_date.return_value == TODAY
41
+
42
+ # Over chat completions limit
43
+ with pytest.raises(PermissionError), \
44
+ patch("mito_ai.utils.server_limits.get_chat_completion_count", return_value=OS_MONTHLY_AI_COMPLETIONS_LIMIT + 1) as mock_count, \
45
+ patch("mito_ai.utils.server_limits.get_last_reset_date", return_value=TODAY) as mock_date, \
46
+ patch("mito_ai.utils.server_limits.is_pro", return_value=False):
47
+
48
+ check_mito_server_quota(MessageType.CHAT)
49
+ assert mock_count.called
50
+ assert mock_date.called
51
+ assert mock_count.return_value == OS_MONTHLY_AI_COMPLETIONS_LIMIT + 1
52
+ assert mock_date.return_value == TODAY
53
+
54
+ # Over autocomplete limit
55
+ with pytest.raises(PermissionError), \
56
+ patch("mito_ai.utils.server_limits.get_autocomplete_count", return_value=OS_MONTHLY_AUTOCOMPLETE_LIMIT + 1) as mock_count, \
57
+ patch("mito_ai.utils.server_limits.get_last_reset_date", return_value=TODAY) as mock_date, \
58
+ patch("mito_ai.utils.server_limits.is_pro", return_value=False):
59
+
60
+ check_mito_server_quota(MessageType.INLINE_COMPLETION)
61
+ assert mock_count.called
62
+ assert mock_date.called
63
+ assert mock_count.return_value == OS_MONTHLY_AUTOCOMPLETE_LIMIT + 1
64
+ assert mock_date.return_value == TODAY
65
+
66
+
67
+ def test_check_mito_server_quota_pro_user() -> None:
68
+ # No error should be thrown since pro users don't have limits
69
+ with patch("mito_ai.utils.server_limits.is_pro", return_value=True), \
70
+ patch("mito_ai.utils.server_limits.get_chat_completion_count", return_value=1000), \
71
+ patch("mito_ai.utils.server_limits.get_last_reset_date", return_value=REALLY_OLD_DATE):
72
+
73
+ check_mito_server_quota(MessageType.CHAT)
74
+
75
+ def test_prepare_request_data_and_headers_basic() -> None:
76
+ """Test basic functionality of _prepare_request_data_and_headers"""
77
+
78
+ # Mock the user fields
79
+ with patch("mito_ai.utils.open_ai_utils.get_user_field") as mock_get_user_field:
80
+ mock_get_user_field.side_effect = ["test@example.com", "user123"]
81
+
82
+ # Mock the quota check
83
+ data, headers = _prepare_request_data_and_headers(
84
+ last_message_content="test message",
85
+ ai_completion_data={"key": "value"},
86
+ timeout=30,
87
+ max_retries=3,
88
+ message_type=MessageType.CHAT
89
+ )
90
+
91
+ # Verify data structure
92
+ assert data["timeout"] == 30
93
+ assert data["max_retries"] == 3
94
+ assert data["email"] == "test@example.com"
95
+ assert data["user_id"] == "user123"
96
+ assert data["data"] == {"key": "value"}
97
+ assert data["user_input"] == "test message"
98
+
99
+ # Verify headers
100
+ assert headers == {"Content-Type": "application/json"}
101
+
102
+ def test_prepare_request_data_and_headers_null_message() -> None:
103
+ """Test handling of null message content"""
104
+ with patch("mito_ai.utils.open_ai_utils.get_user_field") as mock_get_user_field:
105
+ mock_get_user_field.side_effect = ["test@example.com", "user123"]
106
+
107
+ with patch("mito_ai.utils.open_ai_utils.check_mito_server_quota"):
108
+ data, _ = _prepare_request_data_and_headers(
109
+ last_message_content=None,
110
+ ai_completion_data={},
111
+ timeout=30,
112
+ max_retries=3,
113
+ message_type=MessageType.CHAT
114
+ )
115
+
116
+ # Verify empty string is used for null message
117
+ assert data["user_input"] == ""
118
+
119
+ def test_prepare_request_data_and_headers_caches_user_info() -> None:
120
+ """Test that user info is cached after first call"""
121
+ # Mock both the global variables and the get_user_field function
122
+ with patch("mito_ai.utils.open_ai_utils.__user_email", None), \
123
+ patch("mito_ai.utils.open_ai_utils.__user_id", None), \
124
+ patch("mito_ai.utils.open_ai_utils.get_user_field") as mock_get_user_field:
125
+
126
+ mock_get_user_field.side_effect = ["test@example.com", "user123"]
127
+
128
+ with patch("mito_ai.utils.open_ai_utils.check_mito_server_quota"):
129
+ # First call
130
+ data1, _ = _prepare_request_data_and_headers(
131
+ last_message_content="test",
132
+ ai_completion_data={},
133
+ timeout=30,
134
+ max_retries=3,
135
+ message_type=MessageType.CHAT
136
+ )
137
+
138
+ # Second call
139
+ data2, _ = _prepare_request_data_and_headers(
140
+ last_message_content="test",
141
+ ai_completion_data={},
142
+ timeout=30,
143
+ max_retries=3,
144
+ message_type=MessageType.CHAT
145
+ )
146
+
147
+ # Verify get_user_field was only called twice (once for email, once for user_id)
148
+ assert mock_get_user_field.call_count == 2
149
+
150
+ # Verify both calls return same user info
151
+ assert data1["email"] == data2["email"] == "test@example.com"
152
+ assert data1["user_id"] == data2["user_id"] == "user123"