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.
- mito_ai/__init__.py +114 -0
- mito_ai/_version.py +4 -0
- mito_ai/anthropic_client.py +334 -0
- mito_ai/app_deploy/__init__.py +6 -0
- mito_ai/app_deploy/app_deploy_utils.py +44 -0
- mito_ai/app_deploy/handlers.py +345 -0
- mito_ai/app_deploy/models.py +98 -0
- mito_ai/app_manager/__init__.py +4 -0
- mito_ai/app_manager/handlers.py +167 -0
- mito_ai/app_manager/models.py +71 -0
- mito_ai/app_manager/utils.py +24 -0
- mito_ai/auth/README.md +18 -0
- mito_ai/auth/__init__.py +6 -0
- mito_ai/auth/handlers.py +96 -0
- mito_ai/auth/urls.py +13 -0
- mito_ai/chat_history/handlers.py +63 -0
- mito_ai/chat_history/urls.py +32 -0
- mito_ai/completions/completion_handlers/__init__.py +3 -0
- mito_ai/completions/completion_handlers/agent_auto_error_fixup_handler.py +59 -0
- mito_ai/completions/completion_handlers/agent_execution_handler.py +66 -0
- mito_ai/completions/completion_handlers/chat_completion_handler.py +141 -0
- mito_ai/completions/completion_handlers/code_explain_handler.py +113 -0
- mito_ai/completions/completion_handlers/completion_handler.py +42 -0
- mito_ai/completions/completion_handlers/inline_completer_handler.py +48 -0
- mito_ai/completions/completion_handlers/smart_debug_handler.py +160 -0
- mito_ai/completions/completion_handlers/utils.py +147 -0
- mito_ai/completions/handlers.py +415 -0
- mito_ai/completions/message_history.py +401 -0
- mito_ai/completions/models.py +404 -0
- mito_ai/completions/prompt_builders/__init__.py +3 -0
- mito_ai/completions/prompt_builders/agent_execution_prompt.py +57 -0
- mito_ai/completions/prompt_builders/agent_smart_debug_prompt.py +160 -0
- mito_ai/completions/prompt_builders/agent_system_message.py +472 -0
- mito_ai/completions/prompt_builders/chat_name_prompt.py +15 -0
- mito_ai/completions/prompt_builders/chat_prompt.py +116 -0
- mito_ai/completions/prompt_builders/chat_system_message.py +92 -0
- mito_ai/completions/prompt_builders/explain_code_prompt.py +32 -0
- mito_ai/completions/prompt_builders/inline_completer_prompt.py +197 -0
- mito_ai/completions/prompt_builders/prompt_constants.py +170 -0
- mito_ai/completions/prompt_builders/smart_debug_prompt.py +199 -0
- mito_ai/completions/prompt_builders/utils.py +84 -0
- mito_ai/completions/providers.py +284 -0
- mito_ai/constants.py +63 -0
- mito_ai/db/__init__.py +3 -0
- mito_ai/db/crawlers/__init__.py +6 -0
- mito_ai/db/crawlers/base_crawler.py +61 -0
- mito_ai/db/crawlers/constants.py +43 -0
- mito_ai/db/crawlers/snowflake.py +71 -0
- mito_ai/db/handlers.py +168 -0
- mito_ai/db/models.py +31 -0
- mito_ai/db/urls.py +34 -0
- mito_ai/db/utils.py +185 -0
- mito_ai/docker/mssql/compose.yml +37 -0
- mito_ai/docker/mssql/init/setup.sql +21 -0
- mito_ai/docker/mysql/compose.yml +18 -0
- mito_ai/docker/mysql/init/setup.sql +13 -0
- mito_ai/docker/oracle/compose.yml +17 -0
- mito_ai/docker/oracle/init/setup.sql +20 -0
- mito_ai/docker/postgres/compose.yml +17 -0
- mito_ai/docker/postgres/init/setup.sql +13 -0
- mito_ai/enterprise/__init__.py +3 -0
- mito_ai/enterprise/utils.py +15 -0
- mito_ai/file_uploads/__init__.py +3 -0
- mito_ai/file_uploads/handlers.py +248 -0
- mito_ai/file_uploads/urls.py +21 -0
- mito_ai/gemini_client.py +232 -0
- mito_ai/log/handlers.py +38 -0
- mito_ai/log/urls.py +21 -0
- mito_ai/logger.py +37 -0
- mito_ai/openai_client.py +382 -0
- mito_ai/path_utils.py +70 -0
- mito_ai/rules/handlers.py +44 -0
- mito_ai/rules/urls.py +22 -0
- mito_ai/rules/utils.py +56 -0
- mito_ai/settings/handlers.py +41 -0
- mito_ai/settings/urls.py +20 -0
- mito_ai/settings/utils.py +42 -0
- mito_ai/streamlit_conversion/agent_utils.py +37 -0
- mito_ai/streamlit_conversion/prompts/prompt_constants.py +172 -0
- mito_ai/streamlit_conversion/prompts/prompt_utils.py +10 -0
- mito_ai/streamlit_conversion/prompts/streamlit_app_creation_prompt.py +46 -0
- mito_ai/streamlit_conversion/prompts/streamlit_error_correction_prompt.py +28 -0
- mito_ai/streamlit_conversion/prompts/streamlit_finish_todo_prompt.py +45 -0
- mito_ai/streamlit_conversion/prompts/streamlit_system_prompt.py +56 -0
- mito_ai/streamlit_conversion/prompts/update_existing_app_prompt.py +50 -0
- mito_ai/streamlit_conversion/search_replace_utils.py +94 -0
- mito_ai/streamlit_conversion/streamlit_agent_handler.py +144 -0
- mito_ai/streamlit_conversion/streamlit_utils.py +85 -0
- mito_ai/streamlit_conversion/validate_streamlit_app.py +105 -0
- mito_ai/streamlit_preview/__init__.py +6 -0
- mito_ai/streamlit_preview/handlers.py +111 -0
- mito_ai/streamlit_preview/manager.py +152 -0
- mito_ai/streamlit_preview/urls.py +22 -0
- mito_ai/streamlit_preview/utils.py +29 -0
- mito_ai/tests/__init__.py +3 -0
- mito_ai/tests/chat_history/test_chat_history.py +211 -0
- mito_ai/tests/completions/completion_handlers_utils_test.py +190 -0
- mito_ai/tests/conftest.py +53 -0
- mito_ai/tests/create_agent_system_message_prompt_test.py +22 -0
- mito_ai/tests/data/prompt_lg.py +69 -0
- mito_ai/tests/data/prompt_sm.py +6 -0
- mito_ai/tests/data/prompt_xl.py +13 -0
- mito_ai/tests/data/stock_data.sqlite3 +0 -0
- mito_ai/tests/db/conftest.py +39 -0
- mito_ai/tests/db/connections_test.py +102 -0
- mito_ai/tests/db/mssql_test.py +29 -0
- mito_ai/tests/db/mysql_test.py +29 -0
- mito_ai/tests/db/oracle_test.py +29 -0
- mito_ai/tests/db/postgres_test.py +29 -0
- mito_ai/tests/db/schema_test.py +93 -0
- mito_ai/tests/db/sqlite_test.py +31 -0
- mito_ai/tests/db/test_db_constants.py +61 -0
- mito_ai/tests/deploy_app/test_app_deploy_utils.py +89 -0
- mito_ai/tests/file_uploads/__init__.py +2 -0
- mito_ai/tests/file_uploads/test_handlers.py +282 -0
- mito_ai/tests/message_history/test_generate_short_chat_name.py +120 -0
- mito_ai/tests/message_history/test_message_history_utils.py +469 -0
- mito_ai/tests/open_ai_utils_test.py +152 -0
- mito_ai/tests/performance_test.py +329 -0
- mito_ai/tests/providers/test_anthropic_client.py +447 -0
- mito_ai/tests/providers/test_azure.py +631 -0
- mito_ai/tests/providers/test_capabilities.py +120 -0
- mito_ai/tests/providers/test_gemini_client.py +195 -0
- mito_ai/tests/providers/test_mito_server_utils.py +448 -0
- mito_ai/tests/providers/test_model_resolution.py +130 -0
- mito_ai/tests/providers/test_openai_client.py +57 -0
- mito_ai/tests/providers/test_provider_completion_exception.py +66 -0
- mito_ai/tests/providers/test_provider_limits.py +42 -0
- mito_ai/tests/providers/test_providers.py +382 -0
- mito_ai/tests/providers/test_retry_logic.py +389 -0
- mito_ai/tests/providers/test_stream_mito_server_utils.py +140 -0
- mito_ai/tests/providers/utils.py +85 -0
- mito_ai/tests/rules/conftest.py +26 -0
- mito_ai/tests/rules/rules_test.py +117 -0
- mito_ai/tests/server_limits_test.py +406 -0
- mito_ai/tests/settings/conftest.py +26 -0
- mito_ai/tests/settings/settings_test.py +70 -0
- mito_ai/tests/settings/test_settings_constants.py +9 -0
- mito_ai/tests/streamlit_conversion/__init__.py +3 -0
- mito_ai/tests/streamlit_conversion/test_apply_search_replace.py +240 -0
- mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +246 -0
- mito_ai/tests/streamlit_conversion/test_streamlit_utils.py +193 -0
- mito_ai/tests/streamlit_conversion/test_validate_streamlit_app.py +112 -0
- mito_ai/tests/streamlit_preview/test_streamlit_preview_handler.py +118 -0
- mito_ai/tests/streamlit_preview/test_streamlit_preview_manager.py +292 -0
- mito_ai/tests/test_constants.py +47 -0
- mito_ai/tests/test_telemetry.py +12 -0
- mito_ai/tests/user/__init__.py +2 -0
- mito_ai/tests/user/test_user.py +120 -0
- mito_ai/tests/utils/__init__.py +3 -0
- mito_ai/tests/utils/test_anthropic_utils.py +162 -0
- mito_ai/tests/utils/test_gemini_utils.py +98 -0
- mito_ai/tests/version_check_test.py +169 -0
- mito_ai/user/handlers.py +45 -0
- mito_ai/user/urls.py +21 -0
- mito_ai/utils/__init__.py +3 -0
- mito_ai/utils/anthropic_utils.py +168 -0
- mito_ai/utils/create.py +94 -0
- mito_ai/utils/db.py +74 -0
- mito_ai/utils/error_classes.py +42 -0
- mito_ai/utils/gemini_utils.py +133 -0
- mito_ai/utils/message_history_utils.py +87 -0
- mito_ai/utils/mito_server_utils.py +242 -0
- mito_ai/utils/open_ai_utils.py +200 -0
- mito_ai/utils/provider_utils.py +49 -0
- mito_ai/utils/schema.py +86 -0
- mito_ai/utils/server_limits.py +152 -0
- mito_ai/utils/telemetry_utils.py +480 -0
- mito_ai/utils/utils.py +89 -0
- mito_ai/utils/version_utils.py +94 -0
- mito_ai/utils/websocket_base.py +88 -0
- mito_ai/version_check.py +60 -0
- mito_ai-0.1.50.data/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +7 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/build_log.json +728 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/package.json +243 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/package.json.orig +238 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/toolbar-buttons.json +37 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.8f1845da6bf2b128c049.js +21602 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.8f1845da6bf2b128c049.js.map +1 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js +198 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js.map +1 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.78d3ccb73e7ca1da3aae.js +619 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.78d3ccb73e7ca1da3aae.js.map +1 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/style.js +4 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js +712 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js.map +1 -0
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js +2792 -0
- mito_ai-0.1.50.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js.map +1 -0
- 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
- 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
- mito_ai-0.1.50.dist-info/METADATA +221 -0
- mito_ai-0.1.50.dist-info/RECORD +205 -0
- mito_ai-0.1.50.dist-info/WHEEL +4 -0
- mito_ai-0.1.50.dist-info/entry_points.txt +2 -0
- 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"
|