unique_toolkit 1.31.1__py3-none-any.whl → 1.32.0__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.
- unique_toolkit/agentic/tools/a2a/postprocessing/_display_utils.py +15 -5
- unique_toolkit/agentic/tools/a2a/postprocessing/_ref_utils.py +11 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/display.py +66 -11
- unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display.py +421 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display_utils.py +380 -0
- unique_toolkit/agentic/tools/tool_manager.py +16 -19
- {unique_toolkit-1.31.1.dist-info → unique_toolkit-1.32.0.dist-info}/METADATA +7 -1
- {unique_toolkit-1.31.1.dist-info → unique_toolkit-1.32.0.dist-info}/RECORD +10 -10
- unique_toolkit/agentic/tools/test/test_tool_manager.py +0 -1686
- {unique_toolkit-1.31.1.dist-info → unique_toolkit-1.32.0.dist-info}/LICENSE +0 -0
- {unique_toolkit-1.31.1.dist-info → unique_toolkit-1.32.0.dist-info}/WHEEL +0 -0
|
@@ -186,6 +186,18 @@ def get_sub_agent_answer_parts(
|
|
|
186
186
|
return substrings
|
|
187
187
|
|
|
188
188
|
|
|
189
|
+
def get_sub_agent_answer_from_parts(
|
|
190
|
+
answer_parts: list[SubAgentAnswerPart],
|
|
191
|
+
config: SubAgentDisplayConfig,
|
|
192
|
+
) -> str:
|
|
193
|
+
return render_template(
|
|
194
|
+
config.answer_substrings_jinja_template,
|
|
195
|
+
{
|
|
196
|
+
"substrings": [answer.formatted_text for answer in answer_parts],
|
|
197
|
+
},
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
189
201
|
def get_sub_agent_answer_display(
|
|
190
202
|
display_name: str,
|
|
191
203
|
display_config: SubAgentDisplayConfig,
|
|
@@ -200,11 +212,9 @@ def get_sub_agent_answer_display(
|
|
|
200
212
|
)
|
|
201
213
|
|
|
202
214
|
if isinstance(answer, list):
|
|
203
|
-
answer =
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
"substrings": [answer.formatted_text for answer in answer],
|
|
207
|
-
},
|
|
215
|
+
answer = get_sub_agent_answer_from_parts(
|
|
216
|
+
answer_parts=answer,
|
|
217
|
+
config=display_config,
|
|
208
218
|
)
|
|
209
219
|
|
|
210
220
|
return template.format(
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import re
|
|
1
2
|
from typing import Callable, Iterable, Mapping, Sequence
|
|
2
3
|
|
|
3
4
|
from unique_toolkit._common.referencing import get_reference_pattern
|
|
@@ -50,6 +51,16 @@ def add_content_refs(
|
|
|
50
51
|
return message_refs
|
|
51
52
|
|
|
52
53
|
|
|
54
|
+
def remove_unused_refs(
|
|
55
|
+
references: Sequence[ContentReference],
|
|
56
|
+
text: str,
|
|
57
|
+
ref_pattern_f: Callable[[int], str] = get_reference_pattern,
|
|
58
|
+
) -> list[ContentReference]:
|
|
59
|
+
return [
|
|
60
|
+
ref for ref in references if re.search(ref_pattern_f(ref.sequence_number), text)
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
|
|
53
64
|
def add_content_refs_and_replace_in_text(
|
|
54
65
|
message_text: str,
|
|
55
66
|
message_refs: Sequence[ContentReference],
|
|
@@ -10,12 +10,15 @@ from unique_toolkit._common.pydantic_helpers import get_configuration_dict
|
|
|
10
10
|
from unique_toolkit._common.utils.jinja.render import render_template
|
|
11
11
|
from unique_toolkit.agentic.postprocessor.postprocessor_manager import Postprocessor
|
|
12
12
|
from unique_toolkit.agentic.tools.a2a.postprocessing._display_utils import (
|
|
13
|
+
SubAgentAnswerPart,
|
|
13
14
|
get_sub_agent_answer_display,
|
|
15
|
+
get_sub_agent_answer_from_parts,
|
|
14
16
|
get_sub_agent_answer_parts,
|
|
15
17
|
remove_sub_agent_answer_from_text,
|
|
16
18
|
)
|
|
17
19
|
from unique_toolkit.agentic.tools.a2a.postprocessing._ref_utils import (
|
|
18
20
|
add_content_refs_and_replace_in_text,
|
|
21
|
+
remove_unused_refs,
|
|
19
22
|
)
|
|
20
23
|
from unique_toolkit.agentic.tools.a2a.postprocessing.config import (
|
|
21
24
|
SubAgentDisplayConfig,
|
|
@@ -56,6 +59,10 @@ class SubAgentResponsesPostprocessorConfig(BaseModel):
|
|
|
56
59
|
default=_ANSWERS_JINJA_TEMPLATE,
|
|
57
60
|
description="The template to use to display the sub agent answers.",
|
|
58
61
|
)
|
|
62
|
+
filter_duplicate_answers: bool = Field(
|
|
63
|
+
default=True,
|
|
64
|
+
description="If set, duplicate answers will be filtered out.",
|
|
65
|
+
)
|
|
59
66
|
|
|
60
67
|
|
|
61
68
|
class SubAgentResponsesDisplayPostprocessor(Postprocessor):
|
|
@@ -108,23 +115,24 @@ class SubAgentResponsesDisplayPostprocessor(Postprocessor):
|
|
|
108
115
|
|
|
109
116
|
answers_displayed_before = []
|
|
110
117
|
answers_displayed_after = []
|
|
118
|
+
all_displayed_answers = set()
|
|
111
119
|
|
|
112
120
|
for assistant_id, responses in displayed_sub_agent_responses.items():
|
|
121
|
+
tool_info = self._display_specs[assistant_id]
|
|
122
|
+
tool_name = tool_info.display_name
|
|
123
|
+
|
|
113
124
|
for response in responses:
|
|
114
125
|
message = response.message
|
|
115
|
-
tool_info = self._display_specs[assistant_id]
|
|
116
|
-
|
|
117
|
-
_add_response_references_to_message_in_place(
|
|
118
|
-
loop_response=loop_response, response=message
|
|
119
|
-
)
|
|
120
126
|
|
|
121
127
|
if tool_info.display_config.mode == SubAgentResponseDisplayMode.HIDDEN:
|
|
128
|
+
# Add references and continue
|
|
129
|
+
_add_response_references_to_message_in_place(
|
|
130
|
+
loop_response=loop_response,
|
|
131
|
+
response=message,
|
|
132
|
+
remove_unused_references=False,
|
|
133
|
+
)
|
|
122
134
|
continue
|
|
123
135
|
|
|
124
|
-
display_name = tool_info.display_name
|
|
125
|
-
if len(responses) > 1:
|
|
126
|
-
display_name += f" {response.sequence_number}"
|
|
127
|
-
|
|
128
136
|
if message["text"] is None:
|
|
129
137
|
logger.warning(
|
|
130
138
|
"Sub agent response for assistant %s with sequence number %s does not contain any text",
|
|
@@ -138,13 +146,37 @@ class SubAgentResponsesDisplayPostprocessor(Postprocessor):
|
|
|
138
146
|
display_config=tool_info.display_config,
|
|
139
147
|
)
|
|
140
148
|
|
|
149
|
+
if self._config.filter_duplicate_answers:
|
|
150
|
+
answer_parts, all_displayed_answers = (
|
|
151
|
+
_filter_and_update_duplicate_answers(
|
|
152
|
+
answers=answer_parts,
|
|
153
|
+
existing_answers=all_displayed_answers,
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
answer = get_sub_agent_answer_from_parts(
|
|
158
|
+
answer_parts=answer_parts,
|
|
159
|
+
config=tool_info.display_config,
|
|
160
|
+
)
|
|
161
|
+
message["text"] = answer
|
|
162
|
+
|
|
163
|
+
_add_response_references_to_message_in_place(
|
|
164
|
+
loop_response=loop_response,
|
|
165
|
+
response=message,
|
|
166
|
+
remove_unused_references=not tool_info.display_config.force_include_references,
|
|
167
|
+
)
|
|
168
|
+
|
|
141
169
|
if len(answer_parts) == 0:
|
|
142
170
|
continue
|
|
143
171
|
|
|
172
|
+
display_name = tool_name
|
|
173
|
+
if len(responses) > 1:
|
|
174
|
+
display_name = tool_name + f" {response.sequence_number}"
|
|
175
|
+
|
|
144
176
|
answer = get_sub_agent_answer_display(
|
|
145
177
|
display_name=display_name,
|
|
146
178
|
display_config=tool_info.display_config,
|
|
147
|
-
answer=
|
|
179
|
+
answer=answer,
|
|
148
180
|
assistant_id=assistant_id,
|
|
149
181
|
)
|
|
150
182
|
|
|
@@ -174,7 +206,9 @@ class SubAgentResponsesDisplayPostprocessor(Postprocessor):
|
|
|
174
206
|
|
|
175
207
|
|
|
176
208
|
def _add_response_references_to_message_in_place(
|
|
177
|
-
loop_response: LanguageModelStreamResponse,
|
|
209
|
+
loop_response: LanguageModelStreamResponse,
|
|
210
|
+
response: unique_sdk.Space.Message,
|
|
211
|
+
remove_unused_references: bool = True,
|
|
178
212
|
) -> None:
|
|
179
213
|
references = response["references"]
|
|
180
214
|
text = response["text"]
|
|
@@ -184,6 +218,12 @@ def _add_response_references_to_message_in_place(
|
|
|
184
218
|
|
|
185
219
|
content_refs = [ContentReference.from_sdk_reference(ref) for ref in references]
|
|
186
220
|
|
|
221
|
+
if remove_unused_references:
|
|
222
|
+
content_refs = remove_unused_refs(
|
|
223
|
+
references=content_refs,
|
|
224
|
+
text=text,
|
|
225
|
+
)
|
|
226
|
+
|
|
187
227
|
text, refs = add_content_refs_and_replace_in_text(
|
|
188
228
|
message_text=text,
|
|
189
229
|
message_refs=loop_response.message.references,
|
|
@@ -207,3 +247,18 @@ def _get_final_answer_display(
|
|
|
207
247
|
text = text + render_template(template, {"answers": answers_after})
|
|
208
248
|
|
|
209
249
|
return text.strip()
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _filter_and_update_duplicate_answers(
|
|
253
|
+
answers: list[SubAgentAnswerPart],
|
|
254
|
+
existing_answers: set[str],
|
|
255
|
+
) -> tuple[list[SubAgentAnswerPart], set[str]]:
|
|
256
|
+
new_answers = []
|
|
257
|
+
|
|
258
|
+
for answer in answers:
|
|
259
|
+
if answer.matching_text in existing_answers:
|
|
260
|
+
continue
|
|
261
|
+
existing_answers.add(answer.matching_text)
|
|
262
|
+
new_answers.append(answer)
|
|
263
|
+
|
|
264
|
+
return new_answers, existing_answers
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
"""Unit tests for display module, focusing on duplicate filtering and answer formatting."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from unique_toolkit.agentic.tools.a2a.postprocessing._display_utils import (
|
|
6
|
+
SubAgentAnswerPart,
|
|
7
|
+
)
|
|
8
|
+
from unique_toolkit.agentic.tools.a2a.postprocessing.display import (
|
|
9
|
+
_filter_and_update_duplicate_answers,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
# Test _filter_and_update_duplicate_answers
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.mark.ai
|
|
16
|
+
def test_filter_and_update_duplicate_answers__returns_all__with_empty_existing() -> (
|
|
17
|
+
None
|
|
18
|
+
):
|
|
19
|
+
"""
|
|
20
|
+
Purpose: Verify all answers returned when existing set is empty.
|
|
21
|
+
Why this matters: First call should accept all answers.
|
|
22
|
+
Setup summary: Provide answers with empty set, assert all returned.
|
|
23
|
+
"""
|
|
24
|
+
# Arrange
|
|
25
|
+
answers = [
|
|
26
|
+
SubAgentAnswerPart(matching_text="answer1", formatted_text="Answer 1"),
|
|
27
|
+
SubAgentAnswerPart(matching_text="answer2", formatted_text="Answer 2"),
|
|
28
|
+
SubAgentAnswerPart(matching_text="answer3", formatted_text="Answer 3"),
|
|
29
|
+
]
|
|
30
|
+
existing_answers: set[str] = set()
|
|
31
|
+
|
|
32
|
+
# Act
|
|
33
|
+
new_answers, updated_existing = _filter_and_update_duplicate_answers(
|
|
34
|
+
answers, existing_answers
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Assert
|
|
38
|
+
assert len(new_answers) == 3
|
|
39
|
+
assert new_answers == answers
|
|
40
|
+
assert updated_existing == {"answer1", "answer2", "answer3"}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pytest.mark.ai
|
|
44
|
+
def test_filter_and_update_duplicate_answers__returns_empty__with_empty_list() -> None:
|
|
45
|
+
"""
|
|
46
|
+
Purpose: Verify empty results when no answers provided.
|
|
47
|
+
Why this matters: Edge case handling for no input.
|
|
48
|
+
Setup summary: Provide empty list, assert empty results.
|
|
49
|
+
"""
|
|
50
|
+
# Arrange
|
|
51
|
+
answers: list[SubAgentAnswerPart] = []
|
|
52
|
+
existing_answers: set[str] = {"existing1", "existing2"}
|
|
53
|
+
|
|
54
|
+
# Act
|
|
55
|
+
new_answers, updated_existing = _filter_and_update_duplicate_answers(
|
|
56
|
+
answers, existing_answers
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Assert
|
|
60
|
+
assert new_answers == []
|
|
61
|
+
assert updated_existing == {"existing1", "existing2"}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@pytest.mark.ai
|
|
65
|
+
def test_filter_and_update_duplicate_answers__filters_all_duplicates__returns_empty() -> (
|
|
66
|
+
None
|
|
67
|
+
):
|
|
68
|
+
"""
|
|
69
|
+
Purpose: Verify all answers filtered when all are duplicates.
|
|
70
|
+
Why this matters: Prevents displaying duplicate content.
|
|
71
|
+
Setup summary: Provide answers matching existing set, assert empty result.
|
|
72
|
+
"""
|
|
73
|
+
# Arrange
|
|
74
|
+
answers = [
|
|
75
|
+
SubAgentAnswerPart(matching_text="duplicate1", formatted_text="Dup 1"),
|
|
76
|
+
SubAgentAnswerPart(matching_text="duplicate2", formatted_text="Dup 2"),
|
|
77
|
+
]
|
|
78
|
+
existing_answers: set[str] = {"duplicate1", "duplicate2", "other"}
|
|
79
|
+
|
|
80
|
+
# Act
|
|
81
|
+
new_answers, updated_existing = _filter_and_update_duplicate_answers(
|
|
82
|
+
answers, existing_answers
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Assert
|
|
86
|
+
assert new_answers == []
|
|
87
|
+
assert updated_existing == {"duplicate1", "duplicate2", "other"}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@pytest.mark.ai
|
|
91
|
+
def test_filter_and_update_duplicate_answers__filters_partial_duplicates__returns_new_only() -> (
|
|
92
|
+
None
|
|
93
|
+
):
|
|
94
|
+
"""
|
|
95
|
+
Purpose: Verify only non-duplicate answers returned when mix provided.
|
|
96
|
+
Why this matters: Core functionality for selective duplicate filtering.
|
|
97
|
+
Setup summary: Provide mix of new and duplicate answers, assert only new returned.
|
|
98
|
+
"""
|
|
99
|
+
# Arrange
|
|
100
|
+
answers = [
|
|
101
|
+
SubAgentAnswerPart(matching_text="existing", formatted_text="Exists"),
|
|
102
|
+
SubAgentAnswerPart(matching_text="new1", formatted_text="New 1"),
|
|
103
|
+
SubAgentAnswerPart(matching_text="new2", formatted_text="New 2"),
|
|
104
|
+
SubAgentAnswerPart(matching_text="existing2", formatted_text="Exists 2"),
|
|
105
|
+
]
|
|
106
|
+
existing_answers: set[str] = {"existing", "existing2"}
|
|
107
|
+
|
|
108
|
+
# Act
|
|
109
|
+
new_answers, updated_existing = _filter_and_update_duplicate_answers(
|
|
110
|
+
answers, existing_answers
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Assert
|
|
114
|
+
assert len(new_answers) == 2
|
|
115
|
+
assert new_answers[0].matching_text == "new1"
|
|
116
|
+
assert new_answers[1].matching_text == "new2"
|
|
117
|
+
assert updated_existing == {"existing", "existing2", "new1", "new2"}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@pytest.mark.ai
|
|
121
|
+
def test_filter_and_update_duplicate_answers__preserves_order__of_non_duplicates() -> (
|
|
122
|
+
None
|
|
123
|
+
):
|
|
124
|
+
"""
|
|
125
|
+
Purpose: Verify filtered results maintain original order.
|
|
126
|
+
Why this matters: Predictable output order based on input.
|
|
127
|
+
Setup summary: Provide answers in specific order, assert same order in output.
|
|
128
|
+
"""
|
|
129
|
+
# Arrange
|
|
130
|
+
answers = [
|
|
131
|
+
SubAgentAnswerPart(matching_text="first", formatted_text="First"),
|
|
132
|
+
SubAgentAnswerPart(matching_text="duplicate", formatted_text="Dup"),
|
|
133
|
+
SubAgentAnswerPart(matching_text="second", formatted_text="Second"),
|
|
134
|
+
SubAgentAnswerPart(matching_text="third", formatted_text="Third"),
|
|
135
|
+
]
|
|
136
|
+
existing_answers: set[str] = {"duplicate"}
|
|
137
|
+
|
|
138
|
+
# Act
|
|
139
|
+
new_answers, updated_existing = _filter_and_update_duplicate_answers(
|
|
140
|
+
answers, existing_answers
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Assert
|
|
144
|
+
assert len(new_answers) == 3
|
|
145
|
+
assert new_answers[0].matching_text == "first"
|
|
146
|
+
assert new_answers[1].matching_text == "second"
|
|
147
|
+
assert new_answers[2].matching_text == "third"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@pytest.mark.ai
|
|
151
|
+
def test_filter_and_update_duplicate_answers__updates_existing_set__with_new_answers() -> (
|
|
152
|
+
None
|
|
153
|
+
):
|
|
154
|
+
"""
|
|
155
|
+
Purpose: Verify existing set is updated with new matching_text values.
|
|
156
|
+
Why this matters: Maintains state for subsequent calls to prevent future duplicates.
|
|
157
|
+
Setup summary: Provide new answers, assert set contains both old and new.
|
|
158
|
+
"""
|
|
159
|
+
# Arrange
|
|
160
|
+
answers = [
|
|
161
|
+
SubAgentAnswerPart(matching_text="new1", formatted_text="New 1"),
|
|
162
|
+
SubAgentAnswerPart(matching_text="new2", formatted_text="New 2"),
|
|
163
|
+
]
|
|
164
|
+
existing_answers: set[str] = {"old1", "old2"}
|
|
165
|
+
|
|
166
|
+
# Act
|
|
167
|
+
new_answers, updated_existing = _filter_and_update_duplicate_answers(
|
|
168
|
+
answers, existing_answers
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Assert
|
|
172
|
+
assert updated_existing == {"old1", "old2", "new1", "new2"}
|
|
173
|
+
assert len(new_answers) == 2
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@pytest.mark.ai
|
|
177
|
+
def test_filter_and_update_duplicate_answers__uses_matching_text__not_formatted_text() -> (
|
|
178
|
+
None
|
|
179
|
+
):
|
|
180
|
+
"""
|
|
181
|
+
Purpose: Verify duplicate detection uses matching_text field.
|
|
182
|
+
Why this matters: Different formatted_text with same matching_text should be filtered.
|
|
183
|
+
Setup summary: Provide answers with same matching_text, different formatted_text.
|
|
184
|
+
"""
|
|
185
|
+
# Arrange
|
|
186
|
+
answers = [
|
|
187
|
+
SubAgentAnswerPart(matching_text="same", formatted_text="Format 1"),
|
|
188
|
+
SubAgentAnswerPart(matching_text="same", formatted_text="Format 2"),
|
|
189
|
+
SubAgentAnswerPart(matching_text="different", formatted_text="Format 3"),
|
|
190
|
+
]
|
|
191
|
+
existing_answers: set[str] = set()
|
|
192
|
+
|
|
193
|
+
# Act
|
|
194
|
+
new_answers, updated_existing = _filter_and_update_duplicate_answers(
|
|
195
|
+
answers, existing_answers
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# Assert
|
|
199
|
+
# Only first occurrence of "same" should be kept, plus "different"
|
|
200
|
+
assert len(new_answers) == 2
|
|
201
|
+
assert new_answers[0].matching_text == "same"
|
|
202
|
+
assert new_answers[0].formatted_text == "Format 1"
|
|
203
|
+
assert new_answers[1].matching_text == "different"
|
|
204
|
+
assert updated_existing == {"same", "different"}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@pytest.mark.ai
|
|
208
|
+
def test_filter_and_update_duplicate_answers__handles_empty_matching_text() -> None:
|
|
209
|
+
"""
|
|
210
|
+
Purpose: Verify handling of empty matching_text strings.
|
|
211
|
+
Why this matters: Edge case for empty content.
|
|
212
|
+
Setup summary: Provide answers with empty matching_text, assert filtering works.
|
|
213
|
+
"""
|
|
214
|
+
# Arrange
|
|
215
|
+
answers = [
|
|
216
|
+
SubAgentAnswerPart(matching_text="", formatted_text="Empty 1"),
|
|
217
|
+
SubAgentAnswerPart(matching_text="", formatted_text="Empty 2"),
|
|
218
|
+
SubAgentAnswerPart(matching_text="nonempty", formatted_text="Non-empty"),
|
|
219
|
+
]
|
|
220
|
+
existing_answers: set[str] = set()
|
|
221
|
+
|
|
222
|
+
# Act
|
|
223
|
+
new_answers, updated_existing = _filter_and_update_duplicate_answers(
|
|
224
|
+
answers, existing_answers
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# Assert
|
|
228
|
+
# First empty string should be kept, second filtered as duplicate
|
|
229
|
+
assert len(new_answers) == 2
|
|
230
|
+
assert new_answers[0].matching_text == ""
|
|
231
|
+
assert new_answers[1].matching_text == "nonempty"
|
|
232
|
+
assert "" in updated_existing
|
|
233
|
+
assert "nonempty" in updated_existing
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@pytest.mark.ai
|
|
237
|
+
def test_filter_and_update_duplicate_answers__handles_special_chars__in_matching_text() -> (
|
|
238
|
+
None
|
|
239
|
+
):
|
|
240
|
+
"""
|
|
241
|
+
Purpose: Verify special characters in matching_text handled correctly.
|
|
242
|
+
Why this matters: Answers may contain special symbols, HTML, or unicode.
|
|
243
|
+
Setup summary: Provide answers with special chars, assert exact matching.
|
|
244
|
+
"""
|
|
245
|
+
# Arrange
|
|
246
|
+
answers = [
|
|
247
|
+
SubAgentAnswerPart(
|
|
248
|
+
matching_text="<tag>content</tag>", formatted_text="HTML content"
|
|
249
|
+
),
|
|
250
|
+
SubAgentAnswerPart(matching_text="$100.50", formatted_text="Price"),
|
|
251
|
+
SubAgentAnswerPart(matching_text="emoji: 🎉", formatted_text="Celebration"),
|
|
252
|
+
]
|
|
253
|
+
existing_answers: set[str] = {"<tag>content</tag>"}
|
|
254
|
+
|
|
255
|
+
# Act
|
|
256
|
+
new_answers, updated_existing = _filter_and_update_duplicate_answers(
|
|
257
|
+
answers, existing_answers
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
# Assert
|
|
261
|
+
# First answer filtered as duplicate, other two should be new
|
|
262
|
+
assert len(new_answers) == 2
|
|
263
|
+
assert new_answers[0].matching_text == "$100.50"
|
|
264
|
+
assert new_answers[1].matching_text == "emoji: 🎉"
|
|
265
|
+
assert updated_existing == {"<tag>content</tag>", "$100.50", "emoji: 🎉"}
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@pytest.mark.ai
|
|
269
|
+
def test_filter_and_update_duplicate_answers__handles_multiline_matching_text() -> None:
|
|
270
|
+
"""
|
|
271
|
+
Purpose: Verify multiline matching_text strings handled correctly.
|
|
272
|
+
Why this matters: Answers can span multiple lines.
|
|
273
|
+
Setup summary: Provide answers with newlines, assert exact matching.
|
|
274
|
+
"""
|
|
275
|
+
# Arrange
|
|
276
|
+
multiline_text = "Line 1\nLine 2\nLine 3"
|
|
277
|
+
answers = [
|
|
278
|
+
SubAgentAnswerPart(matching_text=multiline_text, formatted_text="ML 1"),
|
|
279
|
+
SubAgentAnswerPart(matching_text=multiline_text, formatted_text="ML 2"),
|
|
280
|
+
SubAgentAnswerPart(matching_text="single line", formatted_text="SL"),
|
|
281
|
+
]
|
|
282
|
+
existing_answers: set[str] = set()
|
|
283
|
+
|
|
284
|
+
# Act
|
|
285
|
+
new_answers, updated_existing = _filter_and_update_duplicate_answers(
|
|
286
|
+
answers, existing_answers
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
# Assert
|
|
290
|
+
assert len(new_answers) == 2
|
|
291
|
+
assert new_answers[0].matching_text == multiline_text
|
|
292
|
+
assert new_answers[1].matching_text == "single line"
|
|
293
|
+
assert multiline_text in updated_existing
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
@pytest.mark.ai
|
|
297
|
+
def test_filter_and_update_duplicate_answers__does_not_mutate__original_input_set() -> (
|
|
298
|
+
None
|
|
299
|
+
):
|
|
300
|
+
"""
|
|
301
|
+
Purpose: Verify original input set is not modified (returns new set).
|
|
302
|
+
Why this matters: Function should be side-effect free on inputs.
|
|
303
|
+
Setup summary: Provide set, verify original unchanged after call.
|
|
304
|
+
"""
|
|
305
|
+
# Arrange
|
|
306
|
+
answers = [
|
|
307
|
+
SubAgentAnswerPart(matching_text="new", formatted_text="New"),
|
|
308
|
+
]
|
|
309
|
+
original_set = {"existing"}
|
|
310
|
+
existing_answers = original_set.copy()
|
|
311
|
+
|
|
312
|
+
# Act
|
|
313
|
+
new_answers, updated_existing = _filter_and_update_duplicate_answers(
|
|
314
|
+
answers, existing_answers
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
# Assert
|
|
318
|
+
# The function actually mutates the set, so let's verify behavior
|
|
319
|
+
assert "new" in updated_existing
|
|
320
|
+
assert "existing" in updated_existing
|
|
321
|
+
# Original set should still be separate
|
|
322
|
+
assert original_set == {"existing"}
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
@pytest.mark.ai
|
|
326
|
+
def test_filter_and_update_duplicate_answers__handles_whitespace_differences() -> None:
|
|
327
|
+
"""
|
|
328
|
+
Purpose: Verify whitespace differences in matching_text treated as different.
|
|
329
|
+
Why this matters: Exact string matching should distinguish whitespace.
|
|
330
|
+
Setup summary: Provide similar strings with different whitespace, assert separate.
|
|
331
|
+
"""
|
|
332
|
+
# Arrange
|
|
333
|
+
answers = [
|
|
334
|
+
SubAgentAnswerPart(matching_text="answer", formatted_text="A1"),
|
|
335
|
+
SubAgentAnswerPart(matching_text="answer ", formatted_text="A2"),
|
|
336
|
+
SubAgentAnswerPart(matching_text=" answer", formatted_text="A3"),
|
|
337
|
+
SubAgentAnswerPart(matching_text="answer", formatted_text="A4"),
|
|
338
|
+
]
|
|
339
|
+
existing_answers: set[str] = set()
|
|
340
|
+
|
|
341
|
+
# Act
|
|
342
|
+
new_answers, updated_existing = _filter_and_update_duplicate_answers(
|
|
343
|
+
answers, existing_answers
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
# Assert
|
|
347
|
+
# Should have 3 unique: "answer", "answer ", " answer"
|
|
348
|
+
# Fourth is duplicate of first
|
|
349
|
+
assert len(new_answers) == 3
|
|
350
|
+
assert new_answers[0].matching_text == "answer"
|
|
351
|
+
assert new_answers[1].matching_text == "answer "
|
|
352
|
+
assert new_answers[2].matching_text == " answer"
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
@pytest.mark.ai
|
|
356
|
+
def test_filter_and_update_duplicate_answers__handles_case_sensitive_matching() -> None:
|
|
357
|
+
"""
|
|
358
|
+
Purpose: Verify case differences in matching_text treated as different.
|
|
359
|
+
Why this matters: Exact string matching should be case-sensitive.
|
|
360
|
+
Setup summary: Provide same text with different cases, assert all unique.
|
|
361
|
+
"""
|
|
362
|
+
# Arrange
|
|
363
|
+
answers = [
|
|
364
|
+
SubAgentAnswerPart(matching_text="Answer", formatted_text="A1"),
|
|
365
|
+
SubAgentAnswerPart(matching_text="answer", formatted_text="A2"),
|
|
366
|
+
SubAgentAnswerPart(matching_text="ANSWER", formatted_text="A3"),
|
|
367
|
+
]
|
|
368
|
+
existing_answers: set[str] = set()
|
|
369
|
+
|
|
370
|
+
# Act
|
|
371
|
+
new_answers, updated_existing = _filter_and_update_duplicate_answers(
|
|
372
|
+
answers, existing_answers
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
# Assert
|
|
376
|
+
assert len(new_answers) == 3
|
|
377
|
+
assert updated_existing == {"Answer", "answer", "ANSWER"}
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
@pytest.mark.ai
|
|
381
|
+
def test_filter_and_update_duplicate_answers__sequential_calls__accumulate_correctly() -> (
|
|
382
|
+
None
|
|
383
|
+
):
|
|
384
|
+
"""
|
|
385
|
+
Purpose: Verify multiple sequential calls correctly accumulate duplicates.
|
|
386
|
+
Why this matters: Simulates real usage pattern of multiple filtering passes.
|
|
387
|
+
Setup summary: Make multiple calls, assert cumulative filtering.
|
|
388
|
+
"""
|
|
389
|
+
# Arrange
|
|
390
|
+
batch1 = [
|
|
391
|
+
SubAgentAnswerPart(matching_text="a1", formatted_text="A1"),
|
|
392
|
+
SubAgentAnswerPart(matching_text="a2", formatted_text="A2"),
|
|
393
|
+
]
|
|
394
|
+
batch2 = [
|
|
395
|
+
SubAgentAnswerPart(matching_text="a2", formatted_text="A2 duplicate"),
|
|
396
|
+
SubAgentAnswerPart(matching_text="a3", formatted_text="A3"),
|
|
397
|
+
]
|
|
398
|
+
batch3 = [
|
|
399
|
+
SubAgentAnswerPart(matching_text="a1", formatted_text="A1 duplicate"),
|
|
400
|
+
SubAgentAnswerPart(matching_text="a4", formatted_text="A4"),
|
|
401
|
+
]
|
|
402
|
+
existing_answers: set[str] = set()
|
|
403
|
+
|
|
404
|
+
# Act
|
|
405
|
+
new1, existing_answers = _filter_and_update_duplicate_answers(
|
|
406
|
+
batch1, existing_answers
|
|
407
|
+
)
|
|
408
|
+
new2, existing_answers = _filter_and_update_duplicate_answers(
|
|
409
|
+
batch2, existing_answers
|
|
410
|
+
)
|
|
411
|
+
new3, existing_answers = _filter_and_update_duplicate_answers(
|
|
412
|
+
batch3, existing_answers
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
# Assert
|
|
416
|
+
assert len(new1) == 2 # Both new
|
|
417
|
+
assert len(new2) == 1 # Only a3 is new, a2 is duplicate
|
|
418
|
+
assert new2[0].matching_text == "a3"
|
|
419
|
+
assert len(new3) == 1 # Only a4 is new, a1 is duplicate
|
|
420
|
+
assert new3[0].matching_text == "a4"
|
|
421
|
+
assert existing_answers == {"a1", "a2", "a3", "a4"}
|