mito-ai 0.1.55__py3-none-any.whl → 0.1.57__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 +2 -0
- mito_ai/_version.py +1 -1
- mito_ai/anthropic_client.py +7 -6
- mito_ai/chart_wizard/__init__.py +3 -0
- mito_ai/chart_wizard/handlers.py +52 -0
- mito_ai/chart_wizard/urls.py +23 -0
- mito_ai/completions/completion_handlers/completion_handler.py +11 -2
- mito_ai/completions/completion_handlers/scratchpad_result_handler.py +66 -0
- mito_ai/completions/handlers.py +5 -0
- mito_ai/completions/models.py +24 -3
- mito_ai/completions/prompt_builders/agent_execution_prompt.py +18 -50
- mito_ai/completions/prompt_builders/agent_smart_debug_prompt.py +82 -95
- mito_ai/completions/prompt_builders/agent_system_message.py +304 -276
- mito_ai/completions/prompt_builders/chart_conversion_prompt.py +27 -0
- mito_ai/completions/prompt_builders/chat_prompt.py +15 -100
- mito_ai/completions/prompt_builders/chat_system_message.py +98 -72
- mito_ai/completions/prompt_builders/explain_code_prompt.py +22 -24
- mito_ai/completions/prompt_builders/inline_completer_prompt.py +78 -107
- mito_ai/completions/prompt_builders/prompt_constants.py +35 -45
- mito_ai/completions/prompt_builders/prompt_section_registry/__init__.py +70 -0
- mito_ai/completions/prompt_builders/prompt_section_registry/active_cell_code.py +15 -0
- mito_ai/completions/prompt_builders/prompt_section_registry/active_cell_id.py +10 -0
- mito_ai/completions/prompt_builders/prompt_section_registry/active_cell_output.py +20 -0
- mito_ai/completions/prompt_builders/prompt_section_registry/base.py +37 -0
- mito_ai/completions/prompt_builders/prompt_section_registry/error_traceback.py +17 -0
- mito_ai/completions/prompt_builders/prompt_section_registry/example.py +19 -0
- mito_ai/completions/prompt_builders/prompt_section_registry/files.py +17 -0
- mito_ai/completions/prompt_builders/prompt_section_registry/generic.py +15 -0
- mito_ai/completions/prompt_builders/prompt_section_registry/get_cell_output_tool_response.py +21 -0
- mito_ai/completions/prompt_builders/prompt_section_registry/notebook.py +19 -0
- mito_ai/completions/prompt_builders/prompt_section_registry/rules.py +39 -0
- mito_ai/completions/prompt_builders/{utils.py → prompt_section_registry/selected_context.py} +51 -42
- mito_ai/completions/prompt_builders/prompt_section_registry/streamlit_app_status.py +25 -0
- mito_ai/completions/prompt_builders/prompt_section_registry/task.py +12 -0
- mito_ai/completions/prompt_builders/prompt_section_registry/variables.py +18 -0
- mito_ai/completions/prompt_builders/scratchpad_result_prompt.py +17 -0
- mito_ai/completions/prompt_builders/smart_debug_prompt.py +48 -63
- mito_ai/constants.py +0 -3
- mito_ai/tests/completions/test_prompt_section_registry.py +44 -0
- mito_ai/tests/message_history/test_message_history_utils.py +273 -340
- mito_ai/tests/providers/test_anthropic_client.py +7 -3
- mito_ai/utils/message_history_utils.py +68 -44
- mito_ai/utils/provider_utils.py +8 -1
- {mito_ai-0.1.55.data → mito_ai-0.1.57.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +102 -102
- {mito_ai-0.1.55.data → mito_ai-0.1.57.data}/data/share/jupyter/labextensions/mito_ai/package.json +2 -2
- {mito_ai-0.1.55.data → mito_ai-0.1.57.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/package.json.orig +1 -1
- mito_ai-0.1.55.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.49c79c62671528877c61.js → mito_ai-0.1.57.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.9d26322f3e78beb2b666.js +2778 -297
- mito_ai-0.1.57.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.9d26322f3e78beb2b666.js.map +1 -0
- mito_ai-0.1.55.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.9dfbffc3592eb6f0aef9.js → mito_ai-0.1.57.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.79c1ea8a3cda73a4cb6f.js +17 -17
- mito_ai-0.1.55.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.9dfbffc3592eb6f0aef9.js.map → mito_ai-0.1.57.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.79c1ea8a3cda73a4cb6f.js.map +1 -1
- {mito_ai-0.1.55.data → mito_ai-0.1.57.data}/data/share/jupyter/labextensions/mito_ai/themes/mito_ai/index.css +7 -2
- {mito_ai-0.1.55.dist-info → mito_ai-0.1.57.dist-info}/METADATA +5 -1
- {mito_ai-0.1.55.dist-info → mito_ai-0.1.57.dist-info}/RECORD +78 -56
- mito_ai-0.1.55.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.49c79c62671528877c61.js.map +0 -1
- {mito_ai-0.1.55.data → mito_ai-0.1.57.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
- {mito_ai-0.1.55.data → mito_ai-0.1.57.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/toolbar-buttons.json +0 -0
- {mito_ai-0.1.55.data → mito_ai-0.1.57.data}/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js +0 -0
- {mito_ai-0.1.55.data → mito_ai-0.1.57.data}/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js.map +0 -0
- {mito_ai-0.1.55.data → mito_ai-0.1.57.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
- {mito_ai-0.1.55.data → mito_ai-0.1.57.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.f5d476ac514294615881.js +0 -0
- {mito_ai-0.1.55.data → mito_ai-0.1.57.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.f5d476ac514294615881.js.map +0 -0
- {mito_ai-0.1.55.data → mito_ai-0.1.57.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 +0 -0
- {mito_ai-0.1.55.data → mito_ai-0.1.57.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 +0 -0
- {mito_ai-0.1.55.data → mito_ai-0.1.57.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_tokenProvider_tokenProvider_-72f1c8.a917210f057fcfe224ad.js +0 -0
- {mito_ai-0.1.55.data → mito_ai-0.1.57.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_tokenProvider_tokenProvider_-72f1c8.a917210f057fcfe224ad.js.map +0 -0
- {mito_ai-0.1.55.data → mito_ai-0.1.57.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js +0 -0
- {mito_ai-0.1.55.data → mito_ai-0.1.57.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js.map +0 -0
- {mito_ai-0.1.55.data → mito_ai-0.1.57.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js +0 -0
- {mito_ai-0.1.55.data → mito_ai-0.1.57.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js.map +0 -0
- {mito_ai-0.1.55.data → mito_ai-0.1.57.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 +0 -0
- {mito_ai-0.1.55.data → mito_ai-0.1.57.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 +0 -0
- {mito_ai-0.1.55.data → mito_ai-0.1.57.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js +0 -0
- {mito_ai-0.1.55.data → mito_ai-0.1.57.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js.map +0 -0
- {mito_ai-0.1.55.data → mito_ai-0.1.57.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js +0 -0
- {mito_ai-0.1.55.data → mito_ai-0.1.57.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js.map +0 -0
- {mito_ai-0.1.55.data → mito_ai-0.1.57.data}/data/share/jupyter/labextensions/mito_ai/themes/mito_ai/index.js +0 -0
- {mito_ai-0.1.55.dist-info → mito_ai-0.1.57.dist-info}/WHEEL +0 -0
- {mito_ai-0.1.55.dist-info → mito_ai-0.1.57.dist-info}/entry_points.txt +0 -0
- {mito_ai-0.1.55.dist-info → mito_ai-0.1.57.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,391 +2,311 @@
|
|
|
2
2
|
# Distributed under the terms of the GNU Affero General Public License v3.0 License.
|
|
3
3
|
|
|
4
4
|
import pytest
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import List, cast
|
|
6
6
|
from openai.types.chat import ChatCompletionMessageParam
|
|
7
|
-
from mito_ai.utils.message_history_utils import
|
|
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
|
|
7
|
+
from mito_ai.utils.message_history_utils import trim_message_content, trim_old_messages
|
|
11
8
|
from unittest.mock import Mock, patch
|
|
12
9
|
from mito_ai.completions.message_history import GlobalMessageHistory, ChatThread
|
|
13
|
-
from mito_ai.completions.
|
|
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
|
-
```
|
|
10
|
+
from mito_ai.completions.models import ThreadID
|
|
71
11
|
|
|
72
|
-
Some text after."""
|
|
73
12
|
|
|
74
|
-
|
|
13
|
+
# Tests for trim_message_content function
|
|
14
|
+
|
|
15
|
+
def test_trim_message_content_removes_sections_based_on_threshold() -> None:
|
|
16
|
+
"""Test that sections are removed when message_age >= trim_after_messages threshold."""
|
|
17
|
+
# FilesSection has trim_after_messages = 3
|
|
18
|
+
# VariablesSection has trim_after_messages = 6
|
|
19
|
+
# NotebookSection has trim_after_messages = 6
|
|
20
|
+
|
|
21
|
+
content = """Some text before.
|
|
22
|
+
|
|
23
|
+
<Files>file1.csv
|
|
24
|
+
file2.txt</Files>
|
|
25
|
+
|
|
26
|
+
<Variables>var1 = 1
|
|
27
|
+
var2 = "string"</Variables>
|
|
28
|
+
|
|
29
|
+
<Notebook>[
|
|
30
|
+
{{"cell_type": "code", "id": "cell1"}}
|
|
31
|
+
]</Notebook>
|
|
32
|
+
|
|
33
|
+
Some text after."""
|
|
75
34
|
|
|
76
|
-
#
|
|
77
|
-
|
|
78
|
-
assert
|
|
79
|
-
assert
|
|
80
|
-
assert
|
|
35
|
+
# Test with message_age = 2 (should NOT trim Files, Variables, or Notebook)
|
|
36
|
+
result = trim_message_content(content, message_age=2)
|
|
37
|
+
assert "<Files>" in result
|
|
38
|
+
assert "file1.csv" in result
|
|
39
|
+
assert "<Variables>" in result
|
|
40
|
+
assert "var1 = 1" in result
|
|
41
|
+
assert "<Notebook>" in result
|
|
42
|
+
assert "cell_type" in result
|
|
43
|
+
|
|
44
|
+
# Test with message_age = 3 (should trim Files, but NOT Variables or Notebook)
|
|
45
|
+
result = trim_message_content(content, message_age=3)
|
|
46
|
+
assert "<Files>" not in result
|
|
47
|
+
assert "file1.csv" not in result
|
|
48
|
+
assert "<Variables>" in result
|
|
49
|
+
assert "var1 = 1" in result
|
|
50
|
+
assert "<Notebook>" in result
|
|
51
|
+
assert "cell_type" in result
|
|
81
52
|
|
|
82
|
-
#
|
|
53
|
+
# Test with message_age = 6 (should trim Files, Variables, and Notebook)
|
|
54
|
+
result = trim_message_content(content, message_age=6)
|
|
55
|
+
assert "<Files>" not in result
|
|
83
56
|
assert "file1.csv" not in result
|
|
57
|
+
assert "<Variables>" not in result
|
|
84
58
|
assert "var1 = 1" not in result
|
|
59
|
+
assert "<Notebook>" not in result
|
|
85
60
|
assert "cell_type" not in result
|
|
86
|
-
assert "cell1" not in result
|
|
87
61
|
|
|
88
62
|
# Verify other content is preserved
|
|
89
63
|
assert "Some text before." in result
|
|
90
64
|
assert "Some text after." in result
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
#
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
#
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
)
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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."""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_trim_message_content_removes_nested_xml_tags() -> None:
|
|
68
|
+
"""Test that trimming correctly removes nested XML tags within a section."""
|
|
69
|
+
# ExampleSection can contain nested XML tags like <Files> and <Variables>
|
|
70
|
+
# ExampleSection has trim_after_messages = 3
|
|
71
|
+
|
|
72
|
+
content = """Some text before.
|
|
73
|
+
|
|
74
|
+
<Example name="Example 1">
|
|
75
|
+
<Files>file1.csv
|
|
76
|
+
file2.txt</Files>
|
|
77
|
+
<Variables>var1 = 1</Variables>
|
|
78
|
+
Some example text here.
|
|
79
|
+
</Example>
|
|
80
|
+
|
|
81
|
+
Some text after."""
|
|
82
|
+
|
|
83
|
+
# Test with message_age = 2 (should NOT trim Example)
|
|
84
|
+
result = trim_message_content(content, message_age=2)
|
|
85
|
+
assert "<Example" in result
|
|
86
|
+
assert "file1.csv" in result
|
|
87
|
+
assert "<Variables>" in result
|
|
88
|
+
assert "var1 = 1" in result
|
|
89
|
+
|
|
90
|
+
# Test with message_age = 3 (should trim entire Example including nested tags)
|
|
91
|
+
result = trim_message_content(content, message_age=3)
|
|
92
|
+
assert "<Example" not in result
|
|
93
|
+
assert "file1.csv" not in result
|
|
94
|
+
assert "<Variables>" not in result
|
|
95
|
+
assert "var1 = 1" not in result
|
|
96
|
+
assert "Some example text here." not in result
|
|
97
|
+
|
|
98
|
+
# Verify other content is preserved
|
|
99
|
+
assert "Some text before." in result
|
|
100
|
+
assert "Some text after." in result
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_trim_message_content_handles_multiple_sections_of_same_type() -> None:
|
|
104
|
+
"""Test that all instances of a section type are removed when threshold is met."""
|
|
105
|
+
content = """Text before.
|
|
106
|
+
|
|
107
|
+
<Files>file1.csv</Files>
|
|
108
|
+
|
|
109
|
+
Some middle text.
|
|
110
|
+
|
|
111
|
+
<Files>file2.txt
|
|
112
|
+
file3.py</Files>
|
|
113
|
+
|
|
114
|
+
Text after."""
|
|
115
|
+
|
|
116
|
+
# Test with message_age = 3 (should remove all Files sections)
|
|
117
|
+
result = trim_message_content(content, message_age=3)
|
|
118
|
+
assert "<Files>" not in result
|
|
119
|
+
assert "file1.csv" not in result
|
|
120
|
+
assert "file2.txt" not in result
|
|
121
|
+
assert "file3.py" not in result
|
|
122
|
+
|
|
123
|
+
# Verify other content is preserved
|
|
124
|
+
assert "Text before." in result
|
|
125
|
+
assert "Some middle text." in result
|
|
126
|
+
assert "Text after." in result
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_trim_message_content_preserves_sections_with_none_threshold() -> None:
|
|
130
|
+
"""Test that sections with trim_after_messages = None are never trimmed."""
|
|
131
|
+
# ActiveCellIdSection has trim_after_messages = None
|
|
132
|
+
# TaskSection has trim_after_messages = None
|
|
133
|
+
|
|
134
|
+
content = """Some text.
|
|
135
|
+
|
|
136
|
+
<ActiveCellId>cell1</ActiveCellId>
|
|
137
|
+
|
|
138
|
+
<Task>Your task: Do something</Task>
|
|
139
|
+
|
|
140
|
+
<Files>file1.csv</Files>
|
|
141
|
+
|
|
142
|
+
More text."""
|
|
143
|
+
|
|
144
|
+
# Test with very high message_age
|
|
145
|
+
result = trim_message_content(content, message_age=100)
|
|
146
|
+
|
|
147
|
+
# ActiveCellId and Task should remain (threshold = None)
|
|
148
|
+
assert "<ActiveCellId>" in result
|
|
149
|
+
assert "cell1" in result
|
|
150
|
+
assert "<Task>" in result
|
|
151
|
+
assert "Your task: Do something" in result
|
|
152
|
+
|
|
153
|
+
# Files should be removed (threshold = 3)
|
|
154
|
+
assert "<Files>" not in result
|
|
155
|
+
assert "file1.csv" not in result
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_trim_message_content_handles_empty_content() -> None:
|
|
159
|
+
"""Test that trimming handles empty content correctly."""
|
|
160
|
+
content = ""
|
|
161
|
+
result = trim_message_content(content, message_age=5)
|
|
162
|
+
assert result == ""
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_trim_message_content_handles_content_without_sections() -> None:
|
|
166
|
+
"""Test that trimming preserves content without XML sections."""
|
|
214
167
|
content = "This is a simple message with no sections to trim."
|
|
215
|
-
result =
|
|
168
|
+
result = trim_message_content(content, message_age=5)
|
|
216
169
|
assert result == content
|
|
217
170
|
|
|
218
171
|
|
|
172
|
+
def test_trim_message_content_handles_malformed_xml_gracefully() -> None:
|
|
173
|
+
"""Test that trimming handles malformed XML gracefully."""
|
|
174
|
+
# Content with unclosed tags or mismatched tags
|
|
175
|
+
content = """Some text.
|
|
176
|
+
|
|
177
|
+
<Files>file1.csv
|
|
178
|
+
<Variables>var1 = 1</Variables>
|
|
179
|
+
|
|
180
|
+
More text."""
|
|
181
|
+
|
|
182
|
+
# Should not crash, and should attempt to trim what it can
|
|
183
|
+
result = trim_message_content(content, message_age=3)
|
|
184
|
+
# The behavior depends on regex matching, but should not raise an exception
|
|
185
|
+
assert isinstance(result, str)
|
|
186
|
+
|
|
187
|
+
|
|
219
188
|
# Tests for trim_old_messages function
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
assistant_message_with_sections = f"""Assistant message with sections.
|
|
234
|
-
{FILES_SECTION_HEADING}
|
|
235
|
-
file1.csv
|
|
236
|
-
file2.txt
|
|
237
|
-
"""
|
|
189
|
+
|
|
190
|
+
def test_trim_old_messages_calculates_message_age_correctly() -> None:
|
|
191
|
+
"""Test that message age is calculated correctly based on position in message list."""
|
|
192
|
+
# Message age = total_messages - i - 1
|
|
193
|
+
# So for a list of 5 messages (indices 0-4):
|
|
194
|
+
# - Index 0 (oldest): age = 5 - 0 - 1 = 4
|
|
195
|
+
# - Index 1: age = 5 - 1 - 1 = 3
|
|
196
|
+
# - Index 2: age = 5 - 2 - 1 = 2
|
|
197
|
+
# - Index 3: age = 5 - 3 - 1 = 1
|
|
198
|
+
# - Index 4 (newest): age = 5 - 4 - 1 = 0
|
|
199
|
+
|
|
200
|
+
# FilesSection has trim_after_messages = 3, so it should be trimmed when age >= 3
|
|
201
|
+
# VariablesSection has trim_after_messages = 6, so it should be trimmed when age >= 6
|
|
238
202
|
|
|
239
|
-
# Create test messages with proper typing
|
|
240
203
|
messages: List[ChatCompletionMessageParam] = [
|
|
241
|
-
{"role": "
|
|
242
|
-
{"role": "user", "content":
|
|
243
|
-
{"role": "
|
|
244
|
-
{"role": "user", "content": "
|
|
245
|
-
{"role": "user", "content": "
|
|
246
|
-
{"role": "user", "content": "Recent user message 3"},
|
|
204
|
+
{"role": "user", "content": "Message 0 with <Files>file0.csv</Files> and <Variables>var0</Variables>"},
|
|
205
|
+
{"role": "user", "content": "Message 1 with <Files>file1.csv</Files> and <Variables>var1</Variables>"},
|
|
206
|
+
{"role": "user", "content": "Message 2 with <Files>file2.csv</Files> and <Variables>var2</Variables>"},
|
|
207
|
+
{"role": "user", "content": "Message 3 with <Files>file3.csv</Files> and <Variables>var3</Variables>"},
|
|
208
|
+
{"role": "user", "content": "Message 4 with <Files>file4.csv</Files> and <Variables>var4</Variables>"},
|
|
247
209
|
]
|
|
248
210
|
|
|
249
211
|
result = trim_old_messages(messages)
|
|
250
212
|
|
|
251
|
-
#
|
|
252
|
-
|
|
253
|
-
assert isinstance(
|
|
254
|
-
assert
|
|
255
|
-
assert
|
|
256
|
-
assert "
|
|
213
|
+
# Message 0 (age=4): Files should be trimmed (4 >= 3), Variables should remain (4 < 6)
|
|
214
|
+
content_0 = result[0].get("content")
|
|
215
|
+
assert isinstance(content_0, str)
|
|
216
|
+
assert "<Files>" not in content_0
|
|
217
|
+
assert "file0.csv" not in content_0
|
|
218
|
+
assert "<Variables>" in content_0
|
|
219
|
+
assert "var0" in content_0
|
|
257
220
|
|
|
258
|
-
#
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
assert
|
|
262
|
-
assert
|
|
263
|
-
assert "
|
|
221
|
+
# Message 1 (age=3): Files should be trimmed (3 >= 3), Variables should remain (3 < 6)
|
|
222
|
+
content_1 = result[1].get("content")
|
|
223
|
+
assert isinstance(content_1, str)
|
|
224
|
+
assert "<Files>" not in content_1
|
|
225
|
+
assert "file1.csv" not in content_1
|
|
226
|
+
assert "<Variables>" in content_1
|
|
227
|
+
assert "var1" in content_1
|
|
264
228
|
|
|
265
|
-
#
|
|
266
|
-
|
|
267
|
-
assert isinstance(
|
|
268
|
-
assert
|
|
269
|
-
assert
|
|
270
|
-
assert "
|
|
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"
|
|
229
|
+
# Message 2 (age=2): Files should remain (2 < 3), Variables should remain (2 < 6)
|
|
230
|
+
content_2 = result[2].get("content")
|
|
231
|
+
assert isinstance(content_2, str)
|
|
232
|
+
assert "<Files>" in content_2
|
|
233
|
+
assert "file2.csv" in content_2
|
|
234
|
+
assert "<Variables>" in content_2
|
|
235
|
+
assert "var2" in content_2
|
|
276
236
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
assert
|
|
237
|
+
# Message 3 (age=1): Files should remain (1 < 3), Variables should remain (1 < 6)
|
|
238
|
+
content_3 = result[3].get("content")
|
|
239
|
+
assert isinstance(content_3, str)
|
|
240
|
+
assert "<Files>" in content_3
|
|
241
|
+
assert "file3.csv" in content_3
|
|
242
|
+
assert "<Variables>" in content_3
|
|
243
|
+
assert "var3" in content_3
|
|
280
244
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
assert
|
|
245
|
+
# Message 4 (age=0, newest): Files should remain (0 < 3), Variables should remain (0 < 6)
|
|
246
|
+
content_4 = result[4].get("content")
|
|
247
|
+
assert isinstance(content_4, str)
|
|
248
|
+
assert "<Files>" in content_4
|
|
249
|
+
assert "file4.csv" in content_4
|
|
250
|
+
assert "<Variables>" in content_4
|
|
251
|
+
assert "var4" in content_4
|
|
284
252
|
|
|
285
253
|
|
|
286
|
-
def
|
|
287
|
-
"""Test that trim_old_messages
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
|
254
|
+
def test_trim_old_messages_only_trims_user_messages() -> None:
|
|
255
|
+
"""Test that trim_old_messages only trims content from user messages."""
|
|
256
|
+
user_message_with_sections = """User prompt with sections.
|
|
257
|
+
<Files>file1.csv
|
|
258
|
+
file2.txt</Files>
|
|
300
259
|
"""
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
260
|
+
system_message_with_sections = """System message with sections.
|
|
261
|
+
<Files>file1.csv
|
|
262
|
+
file2.txt</Files>
|
|
304
263
|
"""
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
264
|
+
assistant_message_with_sections = """Assistant message with sections.
|
|
265
|
+
<Files>file1.csv
|
|
266
|
+
file2.txt</Files>
|
|
308
267
|
"""
|
|
309
268
|
|
|
310
|
-
# Create test messages with proper typing
|
|
311
269
|
messages: List[ChatCompletionMessageParam] = [
|
|
312
|
-
{"role": "
|
|
313
|
-
{"role": "user", "content":
|
|
314
|
-
{"role": "
|
|
315
|
-
{"role": "user", "content":
|
|
316
|
-
{"role": "user", "content": recent_message_3},
|
|
270
|
+
{"role": "system", "content": system_message_with_sections},
|
|
271
|
+
{"role": "user", "content": user_message_with_sections},
|
|
272
|
+
{"role": "assistant", "content": assistant_message_with_sections},
|
|
273
|
+
{"role": "user", "content": "Recent user message"},
|
|
317
274
|
]
|
|
318
275
|
|
|
319
|
-
# Test with MESSAGE_HISTORY_TRIM_THRESHOLD (3) - only the first 2 messages should be trimmed
|
|
320
276
|
result = trim_old_messages(messages)
|
|
321
277
|
|
|
322
|
-
#
|
|
323
|
-
|
|
324
|
-
assert isinstance(
|
|
325
|
-
assert
|
|
326
|
-
assert "
|
|
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)
|
|
278
|
+
# System message should remain unchanged (not trimmed)
|
|
279
|
+
system_content = result[0].get("content")
|
|
280
|
+
assert isinstance(system_content, str)
|
|
281
|
+
assert system_content == system_message_with_sections
|
|
282
|
+
assert "<Files>" in system_content
|
|
283
|
+
assert "file1.csv" in system_content
|
|
367
284
|
|
|
368
|
-
#
|
|
369
|
-
|
|
285
|
+
# User message should be trimmed (age=1, but Files threshold is 3, so actually not trimmed in this case)
|
|
286
|
+
# But let's test with an older message to ensure trimming works
|
|
287
|
+
user_content = result[1].get("content")
|
|
370
288
|
assert isinstance(user_content, str)
|
|
371
|
-
|
|
289
|
+
# With age=2, Files should remain (2 < 3)
|
|
290
|
+
assert "<Files>" in user_content
|
|
372
291
|
|
|
373
|
-
|
|
292
|
+
# Assistant message should remain unchanged (not trimmed)
|
|
293
|
+
assistant_content = result[2].get("content")
|
|
374
294
|
assert isinstance(assistant_content, str)
|
|
375
|
-
assert assistant_content ==
|
|
295
|
+
assert assistant_content == assistant_message_with_sections
|
|
296
|
+
assert "<Files>" in assistant_content
|
|
376
297
|
|
|
377
298
|
|
|
378
|
-
def
|
|
299
|
+
def test_trim_old_messages_handles_mixed_content_messages() -> None:
|
|
379
300
|
"""
|
|
380
301
|
Tests that when a message contains sections other than text (like image_url),
|
|
381
|
-
those sections are removed completely, leaving only the text content.
|
|
302
|
+
those sections are removed completely, leaving only the trimmed text content.
|
|
382
303
|
"""
|
|
383
|
-
# Create sample message with mixed content (text and image)
|
|
384
304
|
mixed_content_message = cast(ChatCompletionMessageParam, {
|
|
385
305
|
"role": "user",
|
|
386
306
|
"content": [
|
|
387
307
|
{
|
|
388
308
|
"type": "text",
|
|
389
|
-
"text": "What is in this image?"
|
|
309
|
+
"text": "What is in this image? <Files>file1.csv</Files>"
|
|
390
310
|
},
|
|
391
311
|
{
|
|
392
312
|
"type": "image_url",
|
|
@@ -395,30 +315,43 @@ def test_trim_mixed_content_messages() -> None:
|
|
|
395
315
|
]
|
|
396
316
|
})
|
|
397
317
|
|
|
398
|
-
# Create
|
|
399
|
-
# and enough recent messages to exceed MESSAGE_HISTORY_TRIM_THRESHOLD (3)
|
|
318
|
+
# Create message list with old message (should be trimmed) and recent messages
|
|
400
319
|
message_list: List[ChatCompletionMessageParam] = [
|
|
401
|
-
mixed_content_message, #
|
|
320
|
+
mixed_content_message, # Age = 4, Files should be trimmed (4 >= 3)
|
|
402
321
|
{"role": "assistant", "content": "That's a chart showing data trends"},
|
|
403
|
-
{"role": "user", "content": "Can you explain more?"},
|
|
404
|
-
{"role": "user", "content": "Another recent message"},
|
|
405
|
-
{"role": "user", "content": "Yet another recent message"}
|
|
322
|
+
{"role": "user", "content": "Can you explain more?"},
|
|
323
|
+
{"role": "user", "content": "Another recent message"},
|
|
324
|
+
{"role": "user", "content": "Yet another recent message"}
|
|
406
325
|
]
|
|
407
326
|
|
|
408
|
-
# Apply the trimming function
|
|
409
327
|
trimmed_messages = trim_old_messages(message_list)
|
|
410
328
|
|
|
411
329
|
# Verify that the first message has been trimmed properly
|
|
412
330
|
assert trimmed_messages[0]["role"] == "user"
|
|
413
|
-
|
|
331
|
+
first_content = trimmed_messages[0].get("content")
|
|
332
|
+
assert isinstance(first_content, list)
|
|
333
|
+
# Find the text section
|
|
334
|
+
text_section = next((s for s in first_content if s.get("type") == "text"), None)
|
|
335
|
+
assert text_section is not None
|
|
336
|
+
assert isinstance(text_section.get("text"), str)
|
|
337
|
+
assert "<Files>" not in text_section["text"]
|
|
338
|
+
assert "file1.csv" not in text_section["text"]
|
|
339
|
+
assert "What is in this image?" in text_section["text"]
|
|
414
340
|
|
|
415
|
-
# Verify that
|
|
341
|
+
# Verify that recent messages are untouched
|
|
416
342
|
assert trimmed_messages[1] == message_list[1]
|
|
417
343
|
assert trimmed_messages[2] == message_list[2]
|
|
418
344
|
assert trimmed_messages[3] == message_list[3]
|
|
419
345
|
assert trimmed_messages[4] == message_list[4]
|
|
420
346
|
|
|
421
347
|
|
|
348
|
+
def test_trim_old_messages_empty_list() -> None:
|
|
349
|
+
"""Test that trim_old_messages handles empty message lists correctly."""
|
|
350
|
+
messages: List[ChatCompletionMessageParam] = []
|
|
351
|
+
result = trim_old_messages(messages)
|
|
352
|
+
assert result == []
|
|
353
|
+
|
|
354
|
+
|
|
422
355
|
def test_get_display_history_calls_update_last_interaction() -> None:
|
|
423
356
|
"""Test that get_display_history calls _update_last_interaction when retrieving a thread."""
|
|
424
357
|
|