unique_toolkit 0.7.9__py3-none-any.whl → 1.33.3__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/__init__.py +36 -3
- unique_toolkit/_common/api_calling/human_verification_manager.py +357 -0
- unique_toolkit/_common/base_model_type_attribute.py +303 -0
- unique_toolkit/_common/chunk_relevancy_sorter/config.py +49 -0
- unique_toolkit/_common/chunk_relevancy_sorter/exception.py +5 -0
- unique_toolkit/_common/chunk_relevancy_sorter/schemas.py +46 -0
- unique_toolkit/_common/chunk_relevancy_sorter/service.py +374 -0
- unique_toolkit/_common/chunk_relevancy_sorter/tests/test_service.py +275 -0
- unique_toolkit/_common/default_language_model.py +12 -0
- unique_toolkit/_common/docx_generator/__init__.py +7 -0
- unique_toolkit/_common/docx_generator/config.py +12 -0
- unique_toolkit/_common/docx_generator/schemas.py +80 -0
- unique_toolkit/_common/docx_generator/service.py +225 -0
- unique_toolkit/_common/docx_generator/template/Doc Template.docx +0 -0
- unique_toolkit/_common/endpoint_builder.py +368 -0
- unique_toolkit/_common/endpoint_requestor.py +480 -0
- unique_toolkit/_common/exception.py +24 -0
- unique_toolkit/_common/experimental/endpoint_builder.py +368 -0
- unique_toolkit/_common/experimental/endpoint_requestor.py +488 -0
- unique_toolkit/_common/feature_flags/schema.py +9 -0
- unique_toolkit/_common/pydantic/rjsf_tags.py +936 -0
- unique_toolkit/_common/pydantic_helpers.py +174 -0
- unique_toolkit/_common/referencing.py +53 -0
- unique_toolkit/_common/string_utilities.py +140 -0
- unique_toolkit/_common/tests/test_referencing.py +521 -0
- unique_toolkit/_common/tests/test_string_utilities.py +506 -0
- unique_toolkit/_common/token/image_token_counting.py +67 -0
- unique_toolkit/_common/token/token_counting.py +204 -0
- unique_toolkit/_common/utils/__init__.py +1 -0
- unique_toolkit/_common/utils/files.py +43 -0
- unique_toolkit/_common/utils/image/encode.py +25 -0
- unique_toolkit/_common/utils/jinja/helpers.py +10 -0
- unique_toolkit/_common/utils/jinja/render.py +18 -0
- unique_toolkit/_common/utils/jinja/schema.py +65 -0
- unique_toolkit/_common/utils/jinja/utils.py +80 -0
- unique_toolkit/_common/utils/structured_output/__init__.py +1 -0
- unique_toolkit/_common/utils/structured_output/schema.py +5 -0
- unique_toolkit/_common/utils/write_configuration.py +51 -0
- unique_toolkit/_common/validators.py +101 -4
- unique_toolkit/agentic/__init__.py +1 -0
- unique_toolkit/agentic/debug_info_manager/debug_info_manager.py +28 -0
- unique_toolkit/agentic/debug_info_manager/test/test_debug_info_manager.py +278 -0
- unique_toolkit/agentic/evaluation/config.py +36 -0
- unique_toolkit/{evaluators → agentic/evaluation}/context_relevancy/prompts.py +25 -0
- unique_toolkit/agentic/evaluation/context_relevancy/schema.py +80 -0
- unique_toolkit/agentic/evaluation/context_relevancy/service.py +273 -0
- unique_toolkit/agentic/evaluation/evaluation_manager.py +218 -0
- unique_toolkit/agentic/evaluation/hallucination/constants.py +61 -0
- unique_toolkit/agentic/evaluation/hallucination/hallucination_evaluation.py +112 -0
- unique_toolkit/{evaluators → agentic/evaluation}/hallucination/prompts.py +1 -1
- unique_toolkit/{evaluators → agentic/evaluation}/hallucination/service.py +20 -16
- unique_toolkit/{evaluators → agentic/evaluation}/hallucination/utils.py +32 -21
- unique_toolkit/{evaluators → agentic/evaluation}/output_parser.py +20 -2
- unique_toolkit/{evaluators → agentic/evaluation}/schemas.py +27 -7
- unique_toolkit/agentic/evaluation/tests/test_context_relevancy_service.py +253 -0
- unique_toolkit/agentic/evaluation/tests/test_output_parser.py +87 -0
- unique_toolkit/agentic/history_manager/history_construction_with_contents.py +298 -0
- unique_toolkit/agentic/history_manager/history_manager.py +241 -0
- unique_toolkit/agentic/history_manager/loop_token_reducer.py +484 -0
- unique_toolkit/agentic/history_manager/utils.py +96 -0
- unique_toolkit/agentic/message_log_manager/__init__.py +5 -0
- unique_toolkit/agentic/message_log_manager/service.py +93 -0
- unique_toolkit/agentic/postprocessor/postprocessor_manager.py +212 -0
- unique_toolkit/agentic/reference_manager/reference_manager.py +103 -0
- unique_toolkit/agentic/responses_api/__init__.py +19 -0
- unique_toolkit/agentic/responses_api/postprocessors/code_display.py +71 -0
- unique_toolkit/agentic/responses_api/postprocessors/generated_files.py +297 -0
- unique_toolkit/agentic/responses_api/stream_handler.py +15 -0
- unique_toolkit/agentic/short_term_memory_manager/persistent_short_term_memory_manager.py +141 -0
- unique_toolkit/agentic/thinking_manager/thinking_manager.py +103 -0
- unique_toolkit/agentic/tools/__init__.py +1 -0
- unique_toolkit/agentic/tools/a2a/__init__.py +36 -0
- unique_toolkit/agentic/tools/a2a/config.py +17 -0
- unique_toolkit/agentic/tools/a2a/evaluation/__init__.py +15 -0
- unique_toolkit/agentic/tools/a2a/evaluation/_utils.py +66 -0
- unique_toolkit/agentic/tools/a2a/evaluation/config.py +55 -0
- unique_toolkit/agentic/tools/a2a/evaluation/evaluator.py +260 -0
- unique_toolkit/agentic/tools/a2a/evaluation/summarization_user_message.j2 +9 -0
- unique_toolkit/agentic/tools/a2a/manager.py +55 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/__init__.py +21 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/_display_utils.py +240 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/_ref_utils.py +84 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/config.py +78 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/display.py +264 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/references.py +101 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display.py +421 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display_utils.py +2103 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/test/test_ref_utils.py +603 -0
- unique_toolkit/agentic/tools/a2a/prompts.py +46 -0
- unique_toolkit/agentic/tools/a2a/response_watcher/__init__.py +6 -0
- unique_toolkit/agentic/tools/a2a/response_watcher/service.py +91 -0
- unique_toolkit/agentic/tools/a2a/tool/__init__.py +4 -0
- unique_toolkit/agentic/tools/a2a/tool/_memory.py +26 -0
- unique_toolkit/agentic/tools/a2a/tool/_schema.py +9 -0
- unique_toolkit/agentic/tools/a2a/tool/config.py +158 -0
- unique_toolkit/agentic/tools/a2a/tool/service.py +393 -0
- unique_toolkit/agentic/tools/agent_chunks_hanlder.py +65 -0
- unique_toolkit/agentic/tools/config.py +128 -0
- unique_toolkit/agentic/tools/factory.py +44 -0
- unique_toolkit/agentic/tools/mcp/__init__.py +4 -0
- unique_toolkit/agentic/tools/mcp/manager.py +71 -0
- unique_toolkit/agentic/tools/mcp/models.py +28 -0
- unique_toolkit/agentic/tools/mcp/tool_wrapper.py +234 -0
- unique_toolkit/agentic/tools/openai_builtin/__init__.py +11 -0
- unique_toolkit/agentic/tools/openai_builtin/base.py +46 -0
- unique_toolkit/agentic/tools/openai_builtin/code_interpreter/__init__.py +8 -0
- unique_toolkit/agentic/tools/openai_builtin/code_interpreter/config.py +88 -0
- unique_toolkit/agentic/tools/openai_builtin/code_interpreter/service.py +250 -0
- unique_toolkit/agentic/tools/openai_builtin/manager.py +79 -0
- unique_toolkit/agentic/tools/schemas.py +145 -0
- unique_toolkit/agentic/tools/test/test_mcp_manager.py +536 -0
- unique_toolkit/agentic/tools/test/test_tool_progress_reporter.py +445 -0
- unique_toolkit/agentic/tools/tool.py +187 -0
- unique_toolkit/agentic/tools/tool_manager.py +492 -0
- unique_toolkit/agentic/tools/tool_progress_reporter.py +285 -0
- unique_toolkit/agentic/tools/utils/__init__.py +19 -0
- unique_toolkit/agentic/tools/utils/execution/__init__.py +1 -0
- unique_toolkit/agentic/tools/utils/execution/execution.py +286 -0
- unique_toolkit/agentic/tools/utils/source_handling/__init__.py +0 -0
- unique_toolkit/agentic/tools/utils/source_handling/schema.py +21 -0
- unique_toolkit/agentic/tools/utils/source_handling/source_formatting.py +207 -0
- unique_toolkit/agentic/tools/utils/source_handling/tests/test_source_formatting.py +216 -0
- unique_toolkit/app/__init__.py +9 -0
- unique_toolkit/app/dev_util.py +180 -0
- unique_toolkit/app/fast_api_factory.py +131 -0
- unique_toolkit/app/init_sdk.py +32 -1
- unique_toolkit/app/schemas.py +206 -31
- unique_toolkit/app/unique_settings.py +367 -0
- unique_toolkit/app/webhook.py +77 -0
- unique_toolkit/chat/__init__.py +8 -1
- unique_toolkit/chat/deprecated/service.py +232 -0
- unique_toolkit/chat/functions.py +648 -78
- unique_toolkit/chat/rendering.py +34 -0
- unique_toolkit/chat/responses_api.py +461 -0
- unique_toolkit/chat/schemas.py +134 -2
- unique_toolkit/chat/service.py +115 -767
- unique_toolkit/content/functions.py +353 -8
- unique_toolkit/content/schemas.py +128 -15
- unique_toolkit/content/service.py +321 -45
- unique_toolkit/content/smart_rules.py +301 -0
- unique_toolkit/content/utils.py +10 -3
- unique_toolkit/data_extraction/README.md +96 -0
- unique_toolkit/data_extraction/__init__.py +11 -0
- unique_toolkit/data_extraction/augmented/__init__.py +5 -0
- unique_toolkit/data_extraction/augmented/service.py +93 -0
- unique_toolkit/data_extraction/base.py +25 -0
- unique_toolkit/data_extraction/basic/__init__.py +11 -0
- unique_toolkit/data_extraction/basic/config.py +18 -0
- unique_toolkit/data_extraction/basic/prompt.py +13 -0
- unique_toolkit/data_extraction/basic/service.py +55 -0
- unique_toolkit/embedding/service.py +103 -12
- unique_toolkit/framework_utilities/__init__.py +1 -0
- unique_toolkit/framework_utilities/langchain/__init__.py +10 -0
- unique_toolkit/framework_utilities/langchain/client.py +71 -0
- unique_toolkit/framework_utilities/langchain/history.py +19 -0
- unique_toolkit/framework_utilities/openai/__init__.py +6 -0
- unique_toolkit/framework_utilities/openai/client.py +84 -0
- unique_toolkit/framework_utilities/openai/message_builder.py +229 -0
- unique_toolkit/framework_utilities/utils.py +23 -0
- unique_toolkit/language_model/__init__.py +3 -0
- unique_toolkit/language_model/_responses_api_utils.py +93 -0
- unique_toolkit/language_model/builder.py +27 -11
- unique_toolkit/language_model/default_language_model.py +3 -0
- unique_toolkit/language_model/functions.py +345 -43
- unique_toolkit/language_model/infos.py +1288 -46
- unique_toolkit/language_model/reference.py +242 -0
- unique_toolkit/language_model/schemas.py +481 -49
- unique_toolkit/language_model/service.py +229 -28
- unique_toolkit/protocols/support.py +145 -0
- unique_toolkit/services/__init__.py +7 -0
- unique_toolkit/services/chat_service.py +1631 -0
- unique_toolkit/services/knowledge_base.py +1094 -0
- unique_toolkit/short_term_memory/service.py +178 -41
- unique_toolkit/smart_rules/__init__.py +0 -0
- unique_toolkit/smart_rules/compile.py +56 -0
- unique_toolkit/test_utilities/events.py +197 -0
- unique_toolkit-1.33.3.dist-info/METADATA +1145 -0
- unique_toolkit-1.33.3.dist-info/RECORD +205 -0
- unique_toolkit/evaluators/__init__.py +0 -1
- unique_toolkit/evaluators/config.py +0 -35
- unique_toolkit/evaluators/constants.py +0 -1
- unique_toolkit/evaluators/context_relevancy/constants.py +0 -32
- unique_toolkit/evaluators/context_relevancy/service.py +0 -53
- unique_toolkit/evaluators/context_relevancy/utils.py +0 -142
- unique_toolkit/evaluators/hallucination/constants.py +0 -41
- unique_toolkit-0.7.9.dist-info/METADATA +0 -413
- unique_toolkit-0.7.9.dist-info/RECORD +0 -64
- /unique_toolkit/{evaluators → agentic/evaluation}/exception.py +0 -0
- {unique_toolkit-0.7.9.dist-info → unique_toolkit-1.33.3.dist-info}/LICENSE +0 -0
- {unique_toolkit-0.7.9.dist-info → unique_toolkit-1.33.3.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,2103 @@
|
|
|
1
|
+
"""Unit tests for display module, focusing on HTML formatting and regex removal logic."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from unique_toolkit.agentic.tools.a2a.postprocessing._display_utils import (
|
|
8
|
+
SubAgentAnswerPart,
|
|
9
|
+
_add_line_break,
|
|
10
|
+
_get_display_removal_re,
|
|
11
|
+
_get_display_template,
|
|
12
|
+
_join_text_blocks,
|
|
13
|
+
_wrap_hidden_div,
|
|
14
|
+
_wrap_strong,
|
|
15
|
+
_wrap_text,
|
|
16
|
+
_wrap_with_block_border,
|
|
17
|
+
_wrap_with_details_tag,
|
|
18
|
+
_wrap_with_quote_border,
|
|
19
|
+
get_sub_agent_answer_display,
|
|
20
|
+
get_sub_agent_answer_from_parts,
|
|
21
|
+
get_sub_agent_answer_parts,
|
|
22
|
+
remove_sub_agent_answer_from_text,
|
|
23
|
+
)
|
|
24
|
+
from unique_toolkit.agentic.tools.a2a.postprocessing.config import (
|
|
25
|
+
SubAgentAnswerSubstringConfig,
|
|
26
|
+
SubAgentDisplayConfig,
|
|
27
|
+
SubAgentResponseDisplayMode,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Test _wrap_with_html_block
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.mark.ai
|
|
34
|
+
def test_wrap_with_html_block__wraps_text__with_start_and_end_tags() -> None:
|
|
35
|
+
"""
|
|
36
|
+
Purpose: Verify text is wrapped with opening and closing tags with proper newlines.
|
|
37
|
+
Why this matters: Foundation for all HTML wrapping operations.
|
|
38
|
+
Setup summary: Provide text and tags, assert formatted output.
|
|
39
|
+
"""
|
|
40
|
+
# Arrange
|
|
41
|
+
text = "Hello World"
|
|
42
|
+
start_tag = "<div>"
|
|
43
|
+
end_tag = "</div>"
|
|
44
|
+
|
|
45
|
+
# Act
|
|
46
|
+
result = _wrap_text(text, start_tag, end_tag)
|
|
47
|
+
|
|
48
|
+
# Assert
|
|
49
|
+
assert result == "<div>\nHello World\n</div>"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@pytest.mark.ai
|
|
53
|
+
def test_wrap_with_html_block__strips_whitespace__from_text_and_tags() -> None:
|
|
54
|
+
"""
|
|
55
|
+
Purpose: Ensure whitespace is trimmed from text and tags before wrapping.
|
|
56
|
+
Why this matters: Prevents inconsistent HTML formatting.
|
|
57
|
+
Setup summary: Provide text with whitespace, assert trimmed output.
|
|
58
|
+
"""
|
|
59
|
+
# Arrange
|
|
60
|
+
text = " Hello World "
|
|
61
|
+
start_tag = " <div> "
|
|
62
|
+
end_tag = " </div> "
|
|
63
|
+
|
|
64
|
+
# Act
|
|
65
|
+
result = _wrap_text(text, start_tag, end_tag)
|
|
66
|
+
|
|
67
|
+
# Assert
|
|
68
|
+
assert result == "<div>\nHello World\n</div>"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@pytest.mark.ai
|
|
72
|
+
def test_wrap_with_html_block__handles_empty_tags__no_newlines() -> None:
|
|
73
|
+
"""
|
|
74
|
+
Purpose: Verify empty tags don't add newlines to output.
|
|
75
|
+
Why this matters: Allows flexible HTML composition without unwanted whitespace.
|
|
76
|
+
Setup summary: Provide empty tags, assert text without extra newlines.
|
|
77
|
+
"""
|
|
78
|
+
# Arrange
|
|
79
|
+
text = "Hello World"
|
|
80
|
+
start_tag = ""
|
|
81
|
+
end_tag = ""
|
|
82
|
+
|
|
83
|
+
# Act
|
|
84
|
+
result = _wrap_text(text, start_tag, end_tag)
|
|
85
|
+
|
|
86
|
+
# Assert
|
|
87
|
+
assert result == "Hello World"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@pytest.mark.ai
|
|
91
|
+
def test_wrap_with_html_block__handles_mixed_empty_tags__partial_newlines() -> None:
|
|
92
|
+
"""
|
|
93
|
+
Purpose: Verify behavior with one empty tag and one non-empty tag.
|
|
94
|
+
Why this matters: Ensures consistent formatting in edge cases.
|
|
95
|
+
Setup summary: Provide start tag only, assert newline only after start.
|
|
96
|
+
"""
|
|
97
|
+
# Arrange
|
|
98
|
+
text = "Hello World"
|
|
99
|
+
start_tag = "<div>"
|
|
100
|
+
end_tag = ""
|
|
101
|
+
|
|
102
|
+
# Act
|
|
103
|
+
result = _wrap_text(text, start_tag, end_tag)
|
|
104
|
+
|
|
105
|
+
# Assert
|
|
106
|
+
assert result == "<div>\nHello World"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# Test _join_html_blocks
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@pytest.mark.ai
|
|
113
|
+
def test_join_html_blocks__joins_multiple_blocks__with_newlines() -> None:
|
|
114
|
+
"""
|
|
115
|
+
Purpose: Verify multiple HTML blocks are joined with newline separators.
|
|
116
|
+
Why this matters: Creates properly formatted multi-line HTML output.
|
|
117
|
+
Setup summary: Provide multiple blocks, assert newline-joined output.
|
|
118
|
+
"""
|
|
119
|
+
# Arrange
|
|
120
|
+
block1 = "<div>Block 1</div>"
|
|
121
|
+
block2 = "<div>Block 2</div>"
|
|
122
|
+
block3 = "<div>Block 3</div>"
|
|
123
|
+
|
|
124
|
+
# Act
|
|
125
|
+
result = _join_text_blocks(block1, block2, block3)
|
|
126
|
+
|
|
127
|
+
# Assert
|
|
128
|
+
assert result == "<div>Block 1</div>\n<div>Block 2</div>\n<div>Block 3</div>"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@pytest.mark.ai
|
|
132
|
+
def test_join_html_blocks__strips_whitespace__from_each_block() -> None:
|
|
133
|
+
"""
|
|
134
|
+
Purpose: Ensure whitespace is trimmed from each block before joining.
|
|
135
|
+
Why this matters: Prevents unwanted whitespace in combined HTML.
|
|
136
|
+
Setup summary: Provide blocks with whitespace, assert trimmed joined output.
|
|
137
|
+
"""
|
|
138
|
+
# Arrange
|
|
139
|
+
block1 = " <div>Block 1</div> "
|
|
140
|
+
block2 = " <div>Block 2</div> "
|
|
141
|
+
|
|
142
|
+
# Act
|
|
143
|
+
result = _join_text_blocks(block1, block2)
|
|
144
|
+
|
|
145
|
+
# Assert
|
|
146
|
+
assert result == "<div>Block 1</div>\n<div>Block 2</div>"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@pytest.mark.ai
|
|
150
|
+
def test_join_html_blocks__handles_single_block__no_extra_newlines() -> None:
|
|
151
|
+
"""
|
|
152
|
+
Purpose: Verify single block is returned without modification.
|
|
153
|
+
Why this matters: Edge case handling for variable block counts.
|
|
154
|
+
Setup summary: Provide single block, assert unchanged output.
|
|
155
|
+
"""
|
|
156
|
+
# Arrange
|
|
157
|
+
block = "<div>Single Block</div>"
|
|
158
|
+
|
|
159
|
+
# Act
|
|
160
|
+
result = _join_text_blocks(block)
|
|
161
|
+
|
|
162
|
+
# Assert
|
|
163
|
+
assert result == "<div>Single Block</div>"
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# Test _wrap_with_details_tag
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@pytest.mark.ai
|
|
170
|
+
def test_wrap_with_details_tag__wraps_open__without_summary() -> None:
|
|
171
|
+
"""
|
|
172
|
+
Purpose: Verify open details tag wrapping without summary element.
|
|
173
|
+
Why this matters: Creates collapsible HTML sections in open state.
|
|
174
|
+
Setup summary: Provide text and open mode, assert details open tag.
|
|
175
|
+
"""
|
|
176
|
+
# Arrange
|
|
177
|
+
text = "Content here"
|
|
178
|
+
|
|
179
|
+
# Act
|
|
180
|
+
result = _wrap_with_details_tag(text, mode="open", summary_name=None)
|
|
181
|
+
|
|
182
|
+
# Assert
|
|
183
|
+
assert result == "<details open>\nContent here\n</details>"
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@pytest.mark.ai
|
|
187
|
+
def test_wrap_with_details_tag__wraps_closed__without_summary() -> None:
|
|
188
|
+
"""
|
|
189
|
+
Purpose: Verify closed details tag wrapping without summary element.
|
|
190
|
+
Why this matters: Creates collapsible HTML sections in closed state.
|
|
191
|
+
Setup summary: Provide text and closed mode, assert details tag.
|
|
192
|
+
"""
|
|
193
|
+
# Arrange
|
|
194
|
+
text = "Content here"
|
|
195
|
+
|
|
196
|
+
# Act
|
|
197
|
+
result = _wrap_with_details_tag(text, mode="closed", summary_name=None)
|
|
198
|
+
|
|
199
|
+
# Assert
|
|
200
|
+
assert result == "<details>\nContent here\n</details>"
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@pytest.mark.ai
|
|
204
|
+
def test_wrap_with_details_tag__includes_summary__when_provided() -> None:
|
|
205
|
+
"""
|
|
206
|
+
Purpose: Verify summary element is added when summary_name provided.
|
|
207
|
+
Why this matters: Creates labeled collapsible sections.
|
|
208
|
+
Setup summary: Provide summary_name, assert summary tag before content.
|
|
209
|
+
"""
|
|
210
|
+
# Arrange
|
|
211
|
+
text = "Content here"
|
|
212
|
+
summary_name = "Click to expand"
|
|
213
|
+
|
|
214
|
+
# Act
|
|
215
|
+
result = _wrap_with_details_tag(text, mode="closed", summary_name=summary_name)
|
|
216
|
+
|
|
217
|
+
# Assert
|
|
218
|
+
expected = (
|
|
219
|
+
"<details>\n<summary>\nClick to expand\n</summary>\nContent here\n</details>"
|
|
220
|
+
)
|
|
221
|
+
assert result == expected
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# Test border and style wrappers
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@pytest.mark.ai
|
|
228
|
+
def test_wrap_with_block_border__adds_styled_div__with_border() -> None:
|
|
229
|
+
"""
|
|
230
|
+
Purpose: Verify block border wrapper adds div with border styling.
|
|
231
|
+
Why this matters: Visual separation of content blocks.
|
|
232
|
+
Setup summary: Provide text, assert div with border style.
|
|
233
|
+
"""
|
|
234
|
+
# Arrange
|
|
235
|
+
text = "Bordered content"
|
|
236
|
+
|
|
237
|
+
# Act
|
|
238
|
+
result = _wrap_with_block_border(text)
|
|
239
|
+
|
|
240
|
+
# Assert
|
|
241
|
+
assert result.startswith("<div style='overflow-y: auto; border: 1px solid #ccc;")
|
|
242
|
+
assert "Bordered content" in result
|
|
243
|
+
assert result.endswith("\n</div>")
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@pytest.mark.ai
|
|
247
|
+
def test_wrap_with_quote_border__adds_styled_div__with_left_border() -> None:
|
|
248
|
+
"""
|
|
249
|
+
Purpose: Verify quote border wrapper adds div with left border styling.
|
|
250
|
+
Why this matters: Visual indication of quoted content.
|
|
251
|
+
Setup summary: Provide text, assert div with left border style.
|
|
252
|
+
"""
|
|
253
|
+
# Arrange
|
|
254
|
+
text = "Quoted content"
|
|
255
|
+
|
|
256
|
+
# Act
|
|
257
|
+
result = _wrap_with_quote_border(text)
|
|
258
|
+
|
|
259
|
+
# Assert
|
|
260
|
+
assert result.startswith(
|
|
261
|
+
"<div style='margin-left: 20px; border-left: 2px solid #ccc;"
|
|
262
|
+
)
|
|
263
|
+
assert "Quoted content" in result
|
|
264
|
+
assert result.endswith("\n</div>")
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
@pytest.mark.ai
|
|
268
|
+
def test_wrap_strong__wraps_text__with_strong_tags() -> None:
|
|
269
|
+
"""
|
|
270
|
+
Purpose: Verify text is wrapped with strong tags for bold formatting.
|
|
271
|
+
Why this matters: Text emphasis in HTML output.
|
|
272
|
+
Setup summary: Provide text, assert strong tag wrapping.
|
|
273
|
+
"""
|
|
274
|
+
# Arrange
|
|
275
|
+
text = "Bold text"
|
|
276
|
+
|
|
277
|
+
# Act
|
|
278
|
+
result = _wrap_strong(text)
|
|
279
|
+
|
|
280
|
+
# Assert
|
|
281
|
+
assert result == "<strong>\nBold text\n</strong>"
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
@pytest.mark.ai
|
|
285
|
+
def test_wrap_hidden_div__wraps_text__with_display_none() -> None:
|
|
286
|
+
"""
|
|
287
|
+
Purpose: Verify text is wrapped in hidden div with display:none style.
|
|
288
|
+
Why this matters: Hides content from visual display while keeping it in DOM.
|
|
289
|
+
Setup summary: Provide text, assert div with display:none.
|
|
290
|
+
"""
|
|
291
|
+
# Arrange
|
|
292
|
+
text = "Hidden content"
|
|
293
|
+
|
|
294
|
+
# Act
|
|
295
|
+
result = _wrap_hidden_div(text)
|
|
296
|
+
|
|
297
|
+
# Assert
|
|
298
|
+
assert result == '<div style="display: none;">\nHidden content\n</div>'
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
# Test _add_line_break
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
@pytest.mark.ai
|
|
305
|
+
def test_add_line_break__adds_both__by_default() -> None:
|
|
306
|
+
"""
|
|
307
|
+
Purpose: Verify line breaks are added before and after text by default.
|
|
308
|
+
Why this matters: Default spacing behavior for content.
|
|
309
|
+
Setup summary: Provide text with defaults, assert br tags both sides.
|
|
310
|
+
"""
|
|
311
|
+
# Arrange
|
|
312
|
+
text = "Text content"
|
|
313
|
+
|
|
314
|
+
# Act
|
|
315
|
+
result = _add_line_break(text)
|
|
316
|
+
|
|
317
|
+
# Assert
|
|
318
|
+
assert result == "<br>\nText content\n<br>"
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
@pytest.mark.ai
|
|
322
|
+
def test_add_line_break__adds_only_before__when_after_false() -> None:
|
|
323
|
+
"""
|
|
324
|
+
Purpose: Verify line break only before text when after=False.
|
|
325
|
+
Why this matters: Flexible spacing control.
|
|
326
|
+
Setup summary: Set after=False, assert br only before.
|
|
327
|
+
"""
|
|
328
|
+
# Arrange
|
|
329
|
+
text = "Text content"
|
|
330
|
+
|
|
331
|
+
# Act
|
|
332
|
+
result = _add_line_break(text, before=True, after=False)
|
|
333
|
+
|
|
334
|
+
# Assert
|
|
335
|
+
assert result == "<br>\nText content"
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
@pytest.mark.ai
|
|
339
|
+
def test_add_line_break__adds_only_after__when_before_false() -> None:
|
|
340
|
+
"""
|
|
341
|
+
Purpose: Verify line break only after text when before=False.
|
|
342
|
+
Why this matters: Flexible spacing control.
|
|
343
|
+
Setup summary: Set before=False, assert br only after.
|
|
344
|
+
"""
|
|
345
|
+
# Arrange
|
|
346
|
+
text = "Text content"
|
|
347
|
+
|
|
348
|
+
# Act
|
|
349
|
+
result = _add_line_break(text, before=False, after=True)
|
|
350
|
+
|
|
351
|
+
# Assert
|
|
352
|
+
assert result == "Text content\n<br>"
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
@pytest.mark.ai
|
|
356
|
+
def test_add_line_break__adds_none__when_both_false() -> None:
|
|
357
|
+
"""
|
|
358
|
+
Purpose: Verify no line breaks added when both flags false.
|
|
359
|
+
Why this matters: Complete control over spacing.
|
|
360
|
+
Setup summary: Set both flags false, assert no br tags.
|
|
361
|
+
"""
|
|
362
|
+
# Arrange
|
|
363
|
+
text = "Text content"
|
|
364
|
+
|
|
365
|
+
# Act
|
|
366
|
+
result = _add_line_break(text, before=False, after=False)
|
|
367
|
+
|
|
368
|
+
# Assert
|
|
369
|
+
assert result == "Text content"
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
# Test _get_display_template
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
@pytest.mark.ai
|
|
376
|
+
def test_get_display_template__returns_empty__when_hidden_mode() -> None:
|
|
377
|
+
"""
|
|
378
|
+
Purpose: Verify empty string returned for HIDDEN display mode.
|
|
379
|
+
Why this matters: Content should not be displayed when hidden.
|
|
380
|
+
Setup summary: Set mode to HIDDEN, assert empty string.
|
|
381
|
+
"""
|
|
382
|
+
# Arrange
|
|
383
|
+
mode = SubAgentResponseDisplayMode.HIDDEN
|
|
384
|
+
|
|
385
|
+
# Act
|
|
386
|
+
result = _get_display_template(
|
|
387
|
+
mode=mode,
|
|
388
|
+
add_quote_border=False,
|
|
389
|
+
add_block_border=False,
|
|
390
|
+
display_title_template="Answer from <strong>{}</strong>",
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
# Assert
|
|
394
|
+
assert result == ""
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
@pytest.mark.ai
|
|
398
|
+
def test_get_display_template__includes_placeholders__for_all_modes() -> None:
|
|
399
|
+
"""
|
|
400
|
+
Purpose: Verify all required placeholders present in non-hidden modes.
|
|
401
|
+
Why this matters: Template must support variable substitution.
|
|
402
|
+
Setup summary: Test each display mode, assert placeholders exist.
|
|
403
|
+
"""
|
|
404
|
+
# Arrange
|
|
405
|
+
modes = [
|
|
406
|
+
SubAgentResponseDisplayMode.PLAIN,
|
|
407
|
+
SubAgentResponseDisplayMode.DETAILS_OPEN,
|
|
408
|
+
SubAgentResponseDisplayMode.DETAILS_CLOSED,
|
|
409
|
+
]
|
|
410
|
+
|
|
411
|
+
for mode in modes:
|
|
412
|
+
# Act
|
|
413
|
+
result = _get_display_template(
|
|
414
|
+
mode=mode,
|
|
415
|
+
add_quote_border=False,
|
|
416
|
+
add_block_border=False,
|
|
417
|
+
display_title_template="Answer from <strong>{}</strong>",
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
# Assert
|
|
421
|
+
assert "{assistant_id}" in result, f"assistant_id missing in {mode}"
|
|
422
|
+
assert "{answer}" in result, f"answer missing in {mode}"
|
|
423
|
+
assert "{display_name}" in result, f"display_name missing in {mode}"
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
@pytest.mark.ai
|
|
427
|
+
def test_get_display_template__wraps_assistant_id__as_hidden_div() -> None:
|
|
428
|
+
"""
|
|
429
|
+
Purpose: Verify assistant_id is always wrapped in hidden div.
|
|
430
|
+
Why this matters: Assistant ID should not be visible to users.
|
|
431
|
+
Setup summary: Check template contains hidden div with assistant_id.
|
|
432
|
+
"""
|
|
433
|
+
# Arrange
|
|
434
|
+
mode = SubAgentResponseDisplayMode.PLAIN
|
|
435
|
+
|
|
436
|
+
# Act
|
|
437
|
+
result = _get_display_template(
|
|
438
|
+
mode=mode,
|
|
439
|
+
add_quote_border=False,
|
|
440
|
+
add_block_border=False,
|
|
441
|
+
display_title_template="Answer from <strong>{}</strong>",
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
# Assert
|
|
445
|
+
assert '<div style="display: none;">' in result
|
|
446
|
+
assert "{assistant_id}" in result
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
@pytest.mark.ai
|
|
450
|
+
def test_get_display_template__wraps_display_name__as_strong() -> None:
|
|
451
|
+
"""
|
|
452
|
+
Purpose: Verify display_name is wrapped in strong tags for emphasis.
|
|
453
|
+
Why this matters: Display name should be bold for visibility.
|
|
454
|
+
Setup summary: Check template contains strong tags with display_name.
|
|
455
|
+
"""
|
|
456
|
+
# Arrange
|
|
457
|
+
mode = SubAgentResponseDisplayMode.PLAIN
|
|
458
|
+
|
|
459
|
+
# Act
|
|
460
|
+
result = _get_display_template(
|
|
461
|
+
mode=mode,
|
|
462
|
+
add_quote_border=False,
|
|
463
|
+
add_block_border=False,
|
|
464
|
+
display_title_template="Answer from <strong>{}</strong>",
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
# Assert
|
|
468
|
+
assert "<strong>" in result
|
|
469
|
+
assert "{display_name}" in result
|
|
470
|
+
assert "</strong>" in result
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
@pytest.mark.ai
|
|
474
|
+
def test_get_display_template__adds_details_open__when_details_open_mode() -> None:
|
|
475
|
+
"""
|
|
476
|
+
Purpose: Verify details open tags present in DETAILS_OPEN mode.
|
|
477
|
+
Why this matters: Creates expandable section in open state.
|
|
478
|
+
Setup summary: Set DETAILS_OPEN mode, assert details open tags.
|
|
479
|
+
"""
|
|
480
|
+
# Arrange
|
|
481
|
+
mode = SubAgentResponseDisplayMode.DETAILS_OPEN
|
|
482
|
+
|
|
483
|
+
# Act
|
|
484
|
+
result = _get_display_template(
|
|
485
|
+
mode=mode,
|
|
486
|
+
add_quote_border=False,
|
|
487
|
+
add_block_border=False,
|
|
488
|
+
display_title_template="Answer from <strong>{}</strong>",
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
# Assert
|
|
492
|
+
assert "<details open>" in result
|
|
493
|
+
assert "</details>" in result
|
|
494
|
+
assert "<summary>" in result
|
|
495
|
+
assert "</summary>" in result
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
@pytest.mark.ai
|
|
499
|
+
def test_get_display_template__adds_details_closed__when_details_closed_mode() -> None:
|
|
500
|
+
"""
|
|
501
|
+
Purpose: Verify details tags present without open in DETAILS_CLOSED mode.
|
|
502
|
+
Why this matters: Creates expandable section in closed state.
|
|
503
|
+
Setup summary: Set DETAILS_CLOSED mode, assert details tags without open.
|
|
504
|
+
"""
|
|
505
|
+
# Arrange
|
|
506
|
+
mode = SubAgentResponseDisplayMode.DETAILS_CLOSED
|
|
507
|
+
|
|
508
|
+
# Act
|
|
509
|
+
result = _get_display_template(
|
|
510
|
+
mode=mode,
|
|
511
|
+
add_quote_border=False,
|
|
512
|
+
add_block_border=False,
|
|
513
|
+
display_title_template="Answer from <strong>{}</strong>",
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
# Assert
|
|
517
|
+
assert "<details>" in result
|
|
518
|
+
assert "<details open>" not in result
|
|
519
|
+
assert "</details>" in result
|
|
520
|
+
assert "<summary>" in result
|
|
521
|
+
assert "</summary>" in result
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
@pytest.mark.ai
|
|
525
|
+
def test_get_display_template__adds_line_break_after_name__in_plain_mode() -> None:
|
|
526
|
+
"""
|
|
527
|
+
Purpose: Verify line break added after display name in PLAIN mode.
|
|
528
|
+
Why this matters: Separates display name from content visually.
|
|
529
|
+
Setup summary: Set PLAIN mode, assert br tag after display_name.
|
|
530
|
+
"""
|
|
531
|
+
# Arrange
|
|
532
|
+
mode = SubAgentResponseDisplayMode.PLAIN
|
|
533
|
+
|
|
534
|
+
# Act
|
|
535
|
+
result = _get_display_template(
|
|
536
|
+
mode=mode,
|
|
537
|
+
add_quote_border=False,
|
|
538
|
+
add_block_border=False,
|
|
539
|
+
display_title_template="Answer from <strong>{}</strong>",
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
# Assert
|
|
543
|
+
# The display_name should be wrapped with line break (before=False, after=True)
|
|
544
|
+
assert "<br>" in result
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
@pytest.mark.ai
|
|
548
|
+
def test_get_display_template__adds_quote_border__when_flag_true() -> None:
|
|
549
|
+
"""
|
|
550
|
+
Purpose: Verify quote border styling added when add_quote_border=True.
|
|
551
|
+
Why this matters: Visual indication of quoted content.
|
|
552
|
+
Setup summary: Set add_quote_border=True, assert quote border style.
|
|
553
|
+
"""
|
|
554
|
+
# Arrange
|
|
555
|
+
mode = SubAgentResponseDisplayMode.PLAIN
|
|
556
|
+
|
|
557
|
+
# Act
|
|
558
|
+
result = _get_display_template(
|
|
559
|
+
mode=mode,
|
|
560
|
+
add_quote_border=True,
|
|
561
|
+
add_block_border=False,
|
|
562
|
+
display_title_template="Answer from <strong>{}</strong>",
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
# Assert
|
|
566
|
+
assert "margin-left: 20px" in result
|
|
567
|
+
assert "border-left: 2px solid #ccc" in result
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
@pytest.mark.ai
|
|
571
|
+
def test_get_display_template__adds_block_border__when_flag_true() -> None:
|
|
572
|
+
"""
|
|
573
|
+
Purpose: Verify block border styling added when add_block_border=True.
|
|
574
|
+
Why this matters: Visual separation of content blocks.
|
|
575
|
+
Setup summary: Set add_block_border=True, assert block border style.
|
|
576
|
+
"""
|
|
577
|
+
# Arrange
|
|
578
|
+
mode = SubAgentResponseDisplayMode.PLAIN
|
|
579
|
+
|
|
580
|
+
# Act
|
|
581
|
+
result = _get_display_template(
|
|
582
|
+
mode=mode,
|
|
583
|
+
add_quote_border=False,
|
|
584
|
+
add_block_border=True,
|
|
585
|
+
display_title_template="Answer from <strong>{}</strong>",
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
# Assert
|
|
589
|
+
assert "overflow-y: auto" in result
|
|
590
|
+
assert "border: 1px solid #ccc" in result
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
@pytest.mark.ai
|
|
594
|
+
def test_get_display_template__adds_both_borders__when_both_flags_true() -> None:
|
|
595
|
+
"""
|
|
596
|
+
Purpose: Verify both border styles added when both flags true.
|
|
597
|
+
Why this matters: Support for combining visual styles.
|
|
598
|
+
Setup summary: Set both border flags true, assert both styles present.
|
|
599
|
+
"""
|
|
600
|
+
# Arrange
|
|
601
|
+
mode = SubAgentResponseDisplayMode.PLAIN
|
|
602
|
+
|
|
603
|
+
# Act
|
|
604
|
+
result = _get_display_template(
|
|
605
|
+
mode=mode,
|
|
606
|
+
add_quote_border=True,
|
|
607
|
+
add_block_border=True,
|
|
608
|
+
display_title_template="Answer from <strong>{}</strong>",
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
# Assert
|
|
612
|
+
# Quote border should be inside block border
|
|
613
|
+
assert "overflow-y: auto" in result
|
|
614
|
+
assert "border: 1px solid #ccc" in result
|
|
615
|
+
assert "margin-left: 20px" in result
|
|
616
|
+
assert "border-left: 2px solid #ccc" in result
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
# Test _get_display_removal_re (regex pattern generation)
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
@pytest.mark.ai
|
|
623
|
+
def test_get_display_removal_re__returns_pattern__for_plain_mode() -> None:
|
|
624
|
+
"""
|
|
625
|
+
Purpose: Verify regex pattern is created for PLAIN display mode.
|
|
626
|
+
Why this matters: Enables removal of displayed content from text.
|
|
627
|
+
Setup summary: Generate pattern for PLAIN mode, assert Pattern type.
|
|
628
|
+
"""
|
|
629
|
+
# Arrange
|
|
630
|
+
assistant_id = "test-assistant-123"
|
|
631
|
+
mode = SubAgentResponseDisplayMode.PLAIN
|
|
632
|
+
|
|
633
|
+
# Act
|
|
634
|
+
result = _get_display_removal_re(
|
|
635
|
+
assistant_id=assistant_id,
|
|
636
|
+
mode=mode,
|
|
637
|
+
add_quote_border=False,
|
|
638
|
+
add_block_border=False,
|
|
639
|
+
display_title_template="Answer from <strong>{}</strong>",
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
# Assert
|
|
643
|
+
assert isinstance(result, re.Pattern)
|
|
644
|
+
assert result.flags & re.DOTALL # Should have DOTALL flag
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
@pytest.mark.ai
|
|
648
|
+
def test_get_display_removal_re__returns_pattern__for_details_modes() -> None:
|
|
649
|
+
"""
|
|
650
|
+
Purpose: Verify regex patterns created for both DETAILS modes.
|
|
651
|
+
Why this matters: Ensures removal works for collapsible sections.
|
|
652
|
+
Setup summary: Generate patterns for DETAILS modes, assert Pattern types.
|
|
653
|
+
"""
|
|
654
|
+
# Arrange
|
|
655
|
+
assistant_id = "test-assistant-123"
|
|
656
|
+
modes = [
|
|
657
|
+
SubAgentResponseDisplayMode.DETAILS_OPEN,
|
|
658
|
+
SubAgentResponseDisplayMode.DETAILS_CLOSED,
|
|
659
|
+
]
|
|
660
|
+
|
|
661
|
+
for mode in modes:
|
|
662
|
+
# Act
|
|
663
|
+
result = _get_display_removal_re(
|
|
664
|
+
assistant_id=assistant_id,
|
|
665
|
+
mode=mode,
|
|
666
|
+
add_quote_border=False,
|
|
667
|
+
add_block_border=False,
|
|
668
|
+
display_title_template="Answer from <strong>{}</strong>",
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
# Assert
|
|
672
|
+
assert isinstance(result, re.Pattern), f"Pattern not created for {mode}"
|
|
673
|
+
assert result.flags & re.DOTALL, f"DOTALL flag missing for {mode}"
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
@pytest.mark.ai
|
|
677
|
+
def test_get_display_removal_re__includes_assistant_id__in_pattern() -> None:
|
|
678
|
+
"""
|
|
679
|
+
Purpose: Verify assistant_id is embedded in regex pattern.
|
|
680
|
+
Why this matters: Ensures only specific assistant's content is removed.
|
|
681
|
+
Setup summary: Generate pattern with assistant_id, assert ID in pattern.
|
|
682
|
+
"""
|
|
683
|
+
# Arrange
|
|
684
|
+
assistant_id = "unique-assistant-xyz"
|
|
685
|
+
mode = SubAgentResponseDisplayMode.PLAIN
|
|
686
|
+
|
|
687
|
+
# Act
|
|
688
|
+
result = _get_display_removal_re(
|
|
689
|
+
assistant_id=assistant_id,
|
|
690
|
+
mode=mode,
|
|
691
|
+
add_quote_border=False,
|
|
692
|
+
add_block_border=False,
|
|
693
|
+
display_title_template="Answer from <strong>{}</strong>",
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
# Assert
|
|
697
|
+
assert re.escape(assistant_id) in result.pattern
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
@pytest.mark.ai
|
|
701
|
+
def test_get_display_removal_re__has_capture_groups__for_answer_and_name() -> None:
|
|
702
|
+
"""
|
|
703
|
+
Purpose: Verify regex pattern includes capture groups for dynamic content.
|
|
704
|
+
Why this matters: Allows flexible matching of variable content.
|
|
705
|
+
Setup summary: Check pattern contains regex capture groups.
|
|
706
|
+
"""
|
|
707
|
+
# Arrange
|
|
708
|
+
assistant_id = "test-assistant"
|
|
709
|
+
mode = SubAgentResponseDisplayMode.PLAIN
|
|
710
|
+
|
|
711
|
+
# Act
|
|
712
|
+
result = _get_display_removal_re(
|
|
713
|
+
assistant_id=assistant_id,
|
|
714
|
+
mode=mode,
|
|
715
|
+
add_quote_border=False,
|
|
716
|
+
add_block_border=False,
|
|
717
|
+
display_title_template="Answer from <strong>{}</strong>",
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
# Assert
|
|
721
|
+
# Pattern should contain (.*?) for capturing groups
|
|
722
|
+
assert "(.*?)" in result.pattern
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
# Test _build_sub_agent_answer_display
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
@pytest.mark.ai
|
|
729
|
+
def test_build_sub_agent_answer_display__creates_html__for_plain_mode() -> None:
|
|
730
|
+
"""
|
|
731
|
+
Purpose: Verify HTML output is generated for PLAIN display mode.
|
|
732
|
+
Why this matters: Core functionality for displaying agent responses.
|
|
733
|
+
Setup summary: Build display with PLAIN mode, assert HTML structure.
|
|
734
|
+
"""
|
|
735
|
+
# Arrange
|
|
736
|
+
display_name = "Test Agent"
|
|
737
|
+
answer = "This is the answer"
|
|
738
|
+
assistant_id = "agent-123"
|
|
739
|
+
config = SubAgentDisplayConfig(
|
|
740
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
741
|
+
add_quote_border=False,
|
|
742
|
+
add_block_border=False,
|
|
743
|
+
display_title_template="Answer from <strong>{}</strong>",
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
# Act
|
|
747
|
+
result = get_sub_agent_answer_display(
|
|
748
|
+
display_name=display_name,
|
|
749
|
+
display_config=config,
|
|
750
|
+
answer=answer,
|
|
751
|
+
assistant_id=assistant_id,
|
|
752
|
+
)
|
|
753
|
+
|
|
754
|
+
# Assert
|
|
755
|
+
assert "Test Agent" in result
|
|
756
|
+
assert "This is the answer" in result
|
|
757
|
+
assert "agent-123" in result
|
|
758
|
+
assert '<div style="display: none;">' in result
|
|
759
|
+
assert "<strong>" in result
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
@pytest.mark.ai
|
|
763
|
+
def test_build_sub_agent_answer_display__creates_details__for_details_open() -> None:
|
|
764
|
+
"""
|
|
765
|
+
Purpose: Verify details HTML with open attribute for DETAILS_OPEN mode.
|
|
766
|
+
Why this matters: Creates expandable sections in open state.
|
|
767
|
+
Setup summary: Build display with DETAILS_OPEN, assert details open tags.
|
|
768
|
+
"""
|
|
769
|
+
# Arrange
|
|
770
|
+
display_name = "Test Agent"
|
|
771
|
+
answer = "This is the answer"
|
|
772
|
+
assistant_id = "agent-123"
|
|
773
|
+
config = SubAgentDisplayConfig(
|
|
774
|
+
mode=SubAgentResponseDisplayMode.DETAILS_OPEN,
|
|
775
|
+
add_quote_border=False,
|
|
776
|
+
add_block_border=False,
|
|
777
|
+
display_title_template="Answer from <strong>{}</strong>",
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
# Act
|
|
781
|
+
result = get_sub_agent_answer_display(
|
|
782
|
+
display_name=display_name,
|
|
783
|
+
display_config=config,
|
|
784
|
+
answer=answer,
|
|
785
|
+
assistant_id=assistant_id,
|
|
786
|
+
)
|
|
787
|
+
|
|
788
|
+
# Assert
|
|
789
|
+
assert "<details open>" in result
|
|
790
|
+
assert "</details>" in result
|
|
791
|
+
assert "<summary>" in result
|
|
792
|
+
assert "Test Agent" in result
|
|
793
|
+
assert "This is the answer" in result
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
@pytest.mark.ai
|
|
797
|
+
def test_build_sub_agent_answer_display__creates_details__for_details_closed() -> None:
|
|
798
|
+
"""
|
|
799
|
+
Purpose: Verify details HTML without open for DETAILS_CLOSED mode.
|
|
800
|
+
Why this matters: Creates expandable sections in closed state.
|
|
801
|
+
Setup summary: Build display with DETAILS_CLOSED, assert details tags.
|
|
802
|
+
"""
|
|
803
|
+
# Arrange
|
|
804
|
+
display_name = "Test Agent"
|
|
805
|
+
answer = "This is the answer"
|
|
806
|
+
assistant_id = "agent-123"
|
|
807
|
+
config = SubAgentDisplayConfig(
|
|
808
|
+
mode=SubAgentResponseDisplayMode.DETAILS_CLOSED,
|
|
809
|
+
add_quote_border=False,
|
|
810
|
+
add_block_border=False,
|
|
811
|
+
display_title_template="Answer from <strong>{}</strong>",
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
# Act
|
|
815
|
+
result = get_sub_agent_answer_display(
|
|
816
|
+
display_name=display_name,
|
|
817
|
+
display_config=config,
|
|
818
|
+
answer=answer,
|
|
819
|
+
assistant_id=assistant_id,
|
|
820
|
+
)
|
|
821
|
+
|
|
822
|
+
# Assert
|
|
823
|
+
assert "<details>" in result
|
|
824
|
+
assert "<details open>" not in result
|
|
825
|
+
assert "</details>" in result
|
|
826
|
+
assert "<summary>" in result
|
|
827
|
+
assert "Test Agent" in result
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
@pytest.mark.ai
|
|
831
|
+
def test_build_sub_agent_answer_display__returns_empty__for_hidden_mode() -> None:
|
|
832
|
+
"""
|
|
833
|
+
Purpose: Verify empty string returned for HIDDEN display mode.
|
|
834
|
+
Why this matters: Hidden content should not generate any HTML.
|
|
835
|
+
Setup summary: Build display with HIDDEN mode, assert empty string.
|
|
836
|
+
"""
|
|
837
|
+
# Arrange
|
|
838
|
+
display_name = "Test Agent"
|
|
839
|
+
answer = "This is the answer"
|
|
840
|
+
assistant_id = "agent-123"
|
|
841
|
+
config = SubAgentDisplayConfig(
|
|
842
|
+
mode=SubAgentResponseDisplayMode.HIDDEN,
|
|
843
|
+
add_quote_border=False,
|
|
844
|
+
add_block_border=False,
|
|
845
|
+
display_title_template="Answer from <strong>{}</strong>",
|
|
846
|
+
)
|
|
847
|
+
|
|
848
|
+
# Act
|
|
849
|
+
result = get_sub_agent_answer_display(
|
|
850
|
+
display_name=display_name,
|
|
851
|
+
display_config=config,
|
|
852
|
+
answer=answer,
|
|
853
|
+
assistant_id=assistant_id,
|
|
854
|
+
)
|
|
855
|
+
|
|
856
|
+
# Assert
|
|
857
|
+
assert result == ""
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
# Test _remove_sub_agent_answer_from_text (regex removal logic)
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
@pytest.mark.ai
|
|
864
|
+
def test_remove_sub_agent_answer__removes_plain_display__from_text() -> None:
|
|
865
|
+
"""
|
|
866
|
+
Purpose: Verify PLAIN mode display content is removed from text via regex.
|
|
867
|
+
Why this matters: Core removal functionality for cleaning history.
|
|
868
|
+
Setup summary: Build display, embed in text, remove via regex, assert removal.
|
|
869
|
+
"""
|
|
870
|
+
# Arrange
|
|
871
|
+
assistant_id = "agent-123"
|
|
872
|
+
display_name = "Test Agent"
|
|
873
|
+
answer = "This is the answer"
|
|
874
|
+
config = SubAgentDisplayConfig(
|
|
875
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
876
|
+
add_quote_border=False,
|
|
877
|
+
add_block_border=False,
|
|
878
|
+
display_title_template="Answer from <strong>{}</strong>",
|
|
879
|
+
)
|
|
880
|
+
|
|
881
|
+
# Build the display
|
|
882
|
+
display = get_sub_agent_answer_display(
|
|
883
|
+
display_name=display_name,
|
|
884
|
+
display_config=config,
|
|
885
|
+
answer=answer,
|
|
886
|
+
assistant_id=assistant_id,
|
|
887
|
+
)
|
|
888
|
+
|
|
889
|
+
text_with_display = f"Before content\n{display}\nAfter content"
|
|
890
|
+
|
|
891
|
+
# Act
|
|
892
|
+
result = remove_sub_agent_answer_from_text(
|
|
893
|
+
display_config=config,
|
|
894
|
+
text=text_with_display,
|
|
895
|
+
assistant_id=assistant_id,
|
|
896
|
+
)
|
|
897
|
+
|
|
898
|
+
# Assert
|
|
899
|
+
assert "This is the answer" not in result
|
|
900
|
+
assert "Test Agent" not in result
|
|
901
|
+
assert "Before content" in result
|
|
902
|
+
assert "After content" in result
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
@pytest.mark.ai
|
|
906
|
+
def test_remove_sub_agent_answer__removes_details_open__from_text() -> None:
|
|
907
|
+
"""
|
|
908
|
+
Purpose: Verify DETAILS_OPEN mode display is removed via regex.
|
|
909
|
+
Why this matters: Ensures removal works for collapsible open sections.
|
|
910
|
+
Setup summary: Build details open display, embed and remove, assert removal.
|
|
911
|
+
"""
|
|
912
|
+
# Arrange
|
|
913
|
+
assistant_id = "agent-456"
|
|
914
|
+
display_name = "Research Agent"
|
|
915
|
+
answer = "Research findings here"
|
|
916
|
+
config = SubAgentDisplayConfig(
|
|
917
|
+
mode=SubAgentResponseDisplayMode.DETAILS_OPEN,
|
|
918
|
+
add_quote_border=False,
|
|
919
|
+
add_block_border=False,
|
|
920
|
+
display_title_template="Answer from <strong>{}</strong>",
|
|
921
|
+
)
|
|
922
|
+
|
|
923
|
+
# Build the display
|
|
924
|
+
display = get_sub_agent_answer_display(
|
|
925
|
+
display_name=display_name,
|
|
926
|
+
display_config=config,
|
|
927
|
+
answer=answer,
|
|
928
|
+
assistant_id=assistant_id,
|
|
929
|
+
)
|
|
930
|
+
|
|
931
|
+
text_with_display = f"Start\n{display}\nEnd"
|
|
932
|
+
|
|
933
|
+
# Act
|
|
934
|
+
result = remove_sub_agent_answer_from_text(
|
|
935
|
+
display_config=config,
|
|
936
|
+
text=text_with_display,
|
|
937
|
+
assistant_id=assistant_id,
|
|
938
|
+
)
|
|
939
|
+
|
|
940
|
+
# Assert
|
|
941
|
+
assert "Research findings here" not in result
|
|
942
|
+
assert "Research Agent" not in result
|
|
943
|
+
assert "<details open>" not in result
|
|
944
|
+
assert "Start" in result
|
|
945
|
+
assert "End" in result
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
@pytest.mark.ai
|
|
949
|
+
def test_remove_sub_agent_answer__removes_details_closed__from_text() -> None:
|
|
950
|
+
"""
|
|
951
|
+
Purpose: Verify DETAILS_CLOSED mode display is removed via regex.
|
|
952
|
+
Why this matters: Ensures removal works for collapsible closed sections.
|
|
953
|
+
Setup summary: Build details closed display, embed and remove, assert removal.
|
|
954
|
+
"""
|
|
955
|
+
# Arrange
|
|
956
|
+
assistant_id = "agent-789"
|
|
957
|
+
display_name = "Analysis Agent"
|
|
958
|
+
answer = "Analysis results"
|
|
959
|
+
config = SubAgentDisplayConfig(
|
|
960
|
+
mode=SubAgentResponseDisplayMode.DETAILS_CLOSED,
|
|
961
|
+
add_quote_border=False,
|
|
962
|
+
add_block_border=False,
|
|
963
|
+
display_title_template="Answer from <strong>{}</strong>",
|
|
964
|
+
)
|
|
965
|
+
|
|
966
|
+
# Build the display
|
|
967
|
+
display = get_sub_agent_answer_display(
|
|
968
|
+
display_name=display_name,
|
|
969
|
+
display_config=config,
|
|
970
|
+
answer=answer,
|
|
971
|
+
assistant_id=assistant_id,
|
|
972
|
+
)
|
|
973
|
+
|
|
974
|
+
text_with_display = f"Beginning\n{display}\nEnding"
|
|
975
|
+
|
|
976
|
+
# Act
|
|
977
|
+
result = remove_sub_agent_answer_from_text(
|
|
978
|
+
display_config=config,
|
|
979
|
+
text=text_with_display,
|
|
980
|
+
assistant_id=assistant_id,
|
|
981
|
+
)
|
|
982
|
+
|
|
983
|
+
# Assert
|
|
984
|
+
assert "Analysis results" not in result
|
|
985
|
+
assert "Analysis Agent" not in result
|
|
986
|
+
assert "<details>" not in result
|
|
987
|
+
assert "Beginning" in result
|
|
988
|
+
assert "Ending" in result
|
|
989
|
+
|
|
990
|
+
|
|
991
|
+
@pytest.mark.ai
|
|
992
|
+
def test_remove_sub_agent_answer__removes_with_quote_border__from_text() -> None:
|
|
993
|
+
"""
|
|
994
|
+
Purpose: Verify removal works when quote border styling is present.
|
|
995
|
+
Why this matters: Regex must handle additional div wrapper.
|
|
996
|
+
Setup summary: Build display with quote border, remove, assert successful removal.
|
|
997
|
+
"""
|
|
998
|
+
# Arrange
|
|
999
|
+
assistant_id = "agent-quote"
|
|
1000
|
+
display_name = "Quote Agent"
|
|
1001
|
+
answer = "Quoted answer"
|
|
1002
|
+
config = SubAgentDisplayConfig(
|
|
1003
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1004
|
+
add_quote_border=True,
|
|
1005
|
+
add_block_border=False,
|
|
1006
|
+
display_title_template="Answer from <strong>{}</strong>",
|
|
1007
|
+
)
|
|
1008
|
+
|
|
1009
|
+
# Build with quote border
|
|
1010
|
+
display = get_sub_agent_answer_display(
|
|
1011
|
+
display_name=display_name,
|
|
1012
|
+
display_config=config,
|
|
1013
|
+
answer=answer,
|
|
1014
|
+
assistant_id=assistant_id,
|
|
1015
|
+
)
|
|
1016
|
+
|
|
1017
|
+
text_with_display = f"Before\n{display}\nAfter"
|
|
1018
|
+
|
|
1019
|
+
# Act
|
|
1020
|
+
result = remove_sub_agent_answer_from_text(
|
|
1021
|
+
display_config=config,
|
|
1022
|
+
text=text_with_display,
|
|
1023
|
+
assistant_id=assistant_id,
|
|
1024
|
+
)
|
|
1025
|
+
|
|
1026
|
+
# Assert
|
|
1027
|
+
assert "Quoted answer" not in result
|
|
1028
|
+
assert "Quote Agent" not in result
|
|
1029
|
+
assert "margin-left: 20px" not in result
|
|
1030
|
+
assert "Before" in result
|
|
1031
|
+
assert "After" in result
|
|
1032
|
+
|
|
1033
|
+
|
|
1034
|
+
@pytest.mark.ai
|
|
1035
|
+
def test_remove_sub_agent_answer__removes_with_block_border__from_text() -> None:
|
|
1036
|
+
"""
|
|
1037
|
+
Purpose: Verify removal works when block border styling is present.
|
|
1038
|
+
Why this matters: Regex must handle block border div wrapper.
|
|
1039
|
+
Setup summary: Build display with block border, remove, assert successful removal.
|
|
1040
|
+
"""
|
|
1041
|
+
# Arrange
|
|
1042
|
+
assistant_id = "agent-block"
|
|
1043
|
+
display_name = "Block Agent"
|
|
1044
|
+
answer = "Block answer"
|
|
1045
|
+
config = SubAgentDisplayConfig(
|
|
1046
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1047
|
+
add_quote_border=False,
|
|
1048
|
+
add_block_border=True,
|
|
1049
|
+
display_title_template="Answer from <strong>{}</strong>",
|
|
1050
|
+
)
|
|
1051
|
+
|
|
1052
|
+
# Build with block border
|
|
1053
|
+
display = get_sub_agent_answer_display(
|
|
1054
|
+
display_name=display_name,
|
|
1055
|
+
display_config=config,
|
|
1056
|
+
answer=answer,
|
|
1057
|
+
assistant_id=assistant_id,
|
|
1058
|
+
)
|
|
1059
|
+
|
|
1060
|
+
text_with_display = f"Start\n{display}\nFinish"
|
|
1061
|
+
|
|
1062
|
+
# Act
|
|
1063
|
+
result = remove_sub_agent_answer_from_text(
|
|
1064
|
+
display_config=config,
|
|
1065
|
+
text=text_with_display,
|
|
1066
|
+
assistant_id=assistant_id,
|
|
1067
|
+
)
|
|
1068
|
+
|
|
1069
|
+
# Assert
|
|
1070
|
+
assert "Block answer" not in result
|
|
1071
|
+
assert "Block Agent" not in result
|
|
1072
|
+
assert "overflow-y: auto" not in result
|
|
1073
|
+
assert "Start" in result
|
|
1074
|
+
assert "Finish" in result
|
|
1075
|
+
|
|
1076
|
+
|
|
1077
|
+
@pytest.mark.ai
|
|
1078
|
+
def test_remove_sub_agent_answer__removes_with_both_borders__from_text() -> None:
|
|
1079
|
+
"""
|
|
1080
|
+
Purpose: Verify removal works with both quote and block borders.
|
|
1081
|
+
Why this matters: Regex must handle nested div wrappers.
|
|
1082
|
+
Setup summary: Build display with both borders, remove, assert successful removal.
|
|
1083
|
+
"""
|
|
1084
|
+
# Arrange
|
|
1085
|
+
assistant_id = "agent-both"
|
|
1086
|
+
display_name = "Both Borders Agent"
|
|
1087
|
+
answer = "Answer with borders"
|
|
1088
|
+
config = SubAgentDisplayConfig(
|
|
1089
|
+
mode=SubAgentResponseDisplayMode.DETAILS_OPEN,
|
|
1090
|
+
add_quote_border=True,
|
|
1091
|
+
add_block_border=True,
|
|
1092
|
+
display_title_template="Answer from <strong>{}</strong>",
|
|
1093
|
+
)
|
|
1094
|
+
|
|
1095
|
+
# Build with both borders
|
|
1096
|
+
display = get_sub_agent_answer_display(
|
|
1097
|
+
display_name=display_name,
|
|
1098
|
+
display_config=config,
|
|
1099
|
+
answer=answer,
|
|
1100
|
+
assistant_id=assistant_id,
|
|
1101
|
+
)
|
|
1102
|
+
|
|
1103
|
+
text_with_display = f"Prefix\n{display}\nSuffix"
|
|
1104
|
+
|
|
1105
|
+
# Act
|
|
1106
|
+
result = remove_sub_agent_answer_from_text(
|
|
1107
|
+
display_config=config,
|
|
1108
|
+
text=text_with_display,
|
|
1109
|
+
assistant_id=assistant_id,
|
|
1110
|
+
)
|
|
1111
|
+
|
|
1112
|
+
# Assert
|
|
1113
|
+
assert "Answer with borders" not in result
|
|
1114
|
+
assert "Both Borders Agent" not in result
|
|
1115
|
+
assert "<details open>" not in result
|
|
1116
|
+
assert "Prefix" in result
|
|
1117
|
+
assert "Suffix" in result
|
|
1118
|
+
|
|
1119
|
+
|
|
1120
|
+
@pytest.mark.ai
|
|
1121
|
+
def test_remove_sub_agent_answer__preserves_other_content__with_multiple_displays() -> (
|
|
1122
|
+
None
|
|
1123
|
+
):
|
|
1124
|
+
"""
|
|
1125
|
+
Purpose: Verify removal only affects specified assistant_id.
|
|
1126
|
+
Why this matters: Must not remove content from different assistants.
|
|
1127
|
+
Setup summary: Embed multiple assistants, remove one, assert selective removal.
|
|
1128
|
+
"""
|
|
1129
|
+
# Arrange
|
|
1130
|
+
assistant_id_1 = "agent-1"
|
|
1131
|
+
assistant_id_2 = "agent-2"
|
|
1132
|
+
config = SubAgentDisplayConfig(
|
|
1133
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1134
|
+
add_quote_border=False,
|
|
1135
|
+
add_block_border=False,
|
|
1136
|
+
display_title_template="Answer from <strong>{}</strong>",
|
|
1137
|
+
)
|
|
1138
|
+
|
|
1139
|
+
# Build displays for two different assistants
|
|
1140
|
+
display_1 = get_sub_agent_answer_display(
|
|
1141
|
+
display_name="Agent 1",
|
|
1142
|
+
display_config=config,
|
|
1143
|
+
answer="Answer from agent 1",
|
|
1144
|
+
assistant_id=assistant_id_1,
|
|
1145
|
+
)
|
|
1146
|
+
|
|
1147
|
+
display_2 = get_sub_agent_answer_display(
|
|
1148
|
+
display_name="Agent 2",
|
|
1149
|
+
display_config=config,
|
|
1150
|
+
answer="Answer from agent 2",
|
|
1151
|
+
assistant_id=assistant_id_2,
|
|
1152
|
+
)
|
|
1153
|
+
|
|
1154
|
+
text_with_displays = f"Start\n{display_1}\nMiddle\n{display_2}\nEnd"
|
|
1155
|
+
|
|
1156
|
+
# Act - Remove only agent-1's display
|
|
1157
|
+
result = remove_sub_agent_answer_from_text(
|
|
1158
|
+
display_config=config,
|
|
1159
|
+
text=text_with_displays,
|
|
1160
|
+
assistant_id=assistant_id_1,
|
|
1161
|
+
)
|
|
1162
|
+
|
|
1163
|
+
# Assert
|
|
1164
|
+
assert "Answer from agent 1" not in result # Removed
|
|
1165
|
+
assert "Agent 1" not in result # Removed
|
|
1166
|
+
assert "Answer from agent 2" in result # Preserved
|
|
1167
|
+
assert "Agent 2" in result # Preserved
|
|
1168
|
+
assert "Start" in result
|
|
1169
|
+
assert "Middle" in result
|
|
1170
|
+
assert "End" in result
|
|
1171
|
+
|
|
1172
|
+
|
|
1173
|
+
@pytest.mark.ai
|
|
1174
|
+
def test_remove_sub_agent_answer__handles_multiline_answer__with_dotall_flag() -> None:
|
|
1175
|
+
"""
|
|
1176
|
+
Purpose: Verify removal works for multiline answers using DOTALL regex flag.
|
|
1177
|
+
Why this matters: Answers can span multiple lines with newlines.
|
|
1178
|
+
Setup summary: Build display with multiline answer, remove, assert removal.
|
|
1179
|
+
"""
|
|
1180
|
+
# Arrange
|
|
1181
|
+
assistant_id = "agent-multiline"
|
|
1182
|
+
display_name = "Multiline Agent"
|
|
1183
|
+
answer = "Line 1\nLine 2\nLine 3\nWith many\nnewlines"
|
|
1184
|
+
config = SubAgentDisplayConfig(
|
|
1185
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1186
|
+
add_quote_border=False,
|
|
1187
|
+
add_block_border=False,
|
|
1188
|
+
display_title_template="Answer from <strong>{}</strong>",
|
|
1189
|
+
)
|
|
1190
|
+
|
|
1191
|
+
# Build the display
|
|
1192
|
+
display = get_sub_agent_answer_display(
|
|
1193
|
+
display_name=display_name,
|
|
1194
|
+
display_config=config,
|
|
1195
|
+
answer=answer,
|
|
1196
|
+
assistant_id=assistant_id,
|
|
1197
|
+
)
|
|
1198
|
+
|
|
1199
|
+
text_with_display = f"Before\n{display}\nAfter"
|
|
1200
|
+
|
|
1201
|
+
# Act
|
|
1202
|
+
result = remove_sub_agent_answer_from_text(
|
|
1203
|
+
display_config=config,
|
|
1204
|
+
text=text_with_display,
|
|
1205
|
+
assistant_id=assistant_id,
|
|
1206
|
+
)
|
|
1207
|
+
|
|
1208
|
+
# Assert
|
|
1209
|
+
assert "Line 1" not in result
|
|
1210
|
+
assert "Line 2" not in result
|
|
1211
|
+
assert "Line 3" not in result
|
|
1212
|
+
assert "With many" not in result
|
|
1213
|
+
assert "Multiline Agent" not in result
|
|
1214
|
+
assert "Before" in result
|
|
1215
|
+
assert "After" in result
|
|
1216
|
+
|
|
1217
|
+
|
|
1218
|
+
@pytest.mark.ai
|
|
1219
|
+
def test_remove_sub_agent_answer__handles_special_regex_chars__in_answer() -> None:
|
|
1220
|
+
"""
|
|
1221
|
+
Purpose: Verify removal works when answer contains regex special characters.
|
|
1222
|
+
Why this matters: Template uses (.*?) which should match any content safely.
|
|
1223
|
+
Setup summary: Build display with regex chars in answer, remove, assert removal.
|
|
1224
|
+
"""
|
|
1225
|
+
# Arrange
|
|
1226
|
+
assistant_id = "agent-special"
|
|
1227
|
+
display_name = "Special Chars"
|
|
1228
|
+
answer = "Answer with $pecial ch@rs: .* + ? [ ] { } ( ) | \\"
|
|
1229
|
+
config = SubAgentDisplayConfig(
|
|
1230
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1231
|
+
add_quote_border=False,
|
|
1232
|
+
add_block_border=False,
|
|
1233
|
+
display_title_template="Answer from <strong>{}</strong>",
|
|
1234
|
+
)
|
|
1235
|
+
|
|
1236
|
+
# Build the display
|
|
1237
|
+
display = get_sub_agent_answer_display(
|
|
1238
|
+
display_name=display_name,
|
|
1239
|
+
display_config=config,
|
|
1240
|
+
answer=answer,
|
|
1241
|
+
assistant_id=assistant_id,
|
|
1242
|
+
)
|
|
1243
|
+
|
|
1244
|
+
text_with_display = f"Start\n{display}\nEnd"
|
|
1245
|
+
|
|
1246
|
+
# Act
|
|
1247
|
+
result = remove_sub_agent_answer_from_text(
|
|
1248
|
+
display_config=config,
|
|
1249
|
+
text=text_with_display,
|
|
1250
|
+
assistant_id=assistant_id,
|
|
1251
|
+
)
|
|
1252
|
+
|
|
1253
|
+
# Assert
|
|
1254
|
+
assert "$pecial ch@rs" not in result
|
|
1255
|
+
assert "Special Chars" not in result
|
|
1256
|
+
assert "Start" in result
|
|
1257
|
+
assert "End" in result
|
|
1258
|
+
|
|
1259
|
+
|
|
1260
|
+
@pytest.mark.ai
|
|
1261
|
+
def test_remove_sub_agent_answer__handles_empty_answer__successfully() -> None:
|
|
1262
|
+
"""
|
|
1263
|
+
Purpose: Verify removal works when answer is empty string.
|
|
1264
|
+
Why this matters: Edge case handling for empty content.
|
|
1265
|
+
Setup summary: Build display with empty answer, remove, assert removal.
|
|
1266
|
+
"""
|
|
1267
|
+
# Arrange
|
|
1268
|
+
assistant_id = "agent-empty"
|
|
1269
|
+
display_name = "Empty Answer Agent"
|
|
1270
|
+
answer = ""
|
|
1271
|
+
config = SubAgentDisplayConfig(
|
|
1272
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1273
|
+
add_quote_border=False,
|
|
1274
|
+
add_block_border=False,
|
|
1275
|
+
display_title_template="Answer from <strong>{}</strong>",
|
|
1276
|
+
)
|
|
1277
|
+
|
|
1278
|
+
# Build the display
|
|
1279
|
+
display = get_sub_agent_answer_display(
|
|
1280
|
+
display_name=display_name,
|
|
1281
|
+
display_config=config,
|
|
1282
|
+
answer=answer,
|
|
1283
|
+
assistant_id=assistant_id,
|
|
1284
|
+
)
|
|
1285
|
+
|
|
1286
|
+
text_with_display = f"Beginning\n{display}\nEnding"
|
|
1287
|
+
|
|
1288
|
+
# Act
|
|
1289
|
+
result = remove_sub_agent_answer_from_text(
|
|
1290
|
+
display_config=config,
|
|
1291
|
+
text=text_with_display,
|
|
1292
|
+
assistant_id=assistant_id,
|
|
1293
|
+
)
|
|
1294
|
+
|
|
1295
|
+
# Assert
|
|
1296
|
+
assert "Empty Answer Agent" not in result
|
|
1297
|
+
assert "Beginning" in result
|
|
1298
|
+
assert "Ending" in result
|
|
1299
|
+
|
|
1300
|
+
|
|
1301
|
+
@pytest.mark.ai
|
|
1302
|
+
def test_remove_sub_agent_answer__no_op_when_assistant_not_found() -> None:
|
|
1303
|
+
"""
|
|
1304
|
+
Purpose: Verify text unchanged when assistant_id has no matching display.
|
|
1305
|
+
Why this matters: Should not modify text when target not present.
|
|
1306
|
+
Setup summary: Build display for one assistant, try removing different one.
|
|
1307
|
+
"""
|
|
1308
|
+
# Arrange
|
|
1309
|
+
assistant_id_present = "agent-present"
|
|
1310
|
+
assistant_id_absent = "agent-absent"
|
|
1311
|
+
config = SubAgentDisplayConfig(
|
|
1312
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1313
|
+
add_quote_border=False,
|
|
1314
|
+
add_block_border=False,
|
|
1315
|
+
display_title_template="Answer from <strong>{}</strong>",
|
|
1316
|
+
)
|
|
1317
|
+
|
|
1318
|
+
# Build display for present assistant
|
|
1319
|
+
display = get_sub_agent_answer_display(
|
|
1320
|
+
display_name="Present Agent",
|
|
1321
|
+
display_config=config,
|
|
1322
|
+
answer="Present answer",
|
|
1323
|
+
assistant_id=assistant_id_present,
|
|
1324
|
+
)
|
|
1325
|
+
|
|
1326
|
+
text_with_display = f"Start\n{display}\nEnd"
|
|
1327
|
+
original_text = text_with_display
|
|
1328
|
+
|
|
1329
|
+
# Act - Try to remove absent assistant
|
|
1330
|
+
result = remove_sub_agent_answer_from_text(
|
|
1331
|
+
display_config=config,
|
|
1332
|
+
text=text_with_display,
|
|
1333
|
+
assistant_id=assistant_id_absent,
|
|
1334
|
+
)
|
|
1335
|
+
|
|
1336
|
+
# Assert
|
|
1337
|
+
assert result == original_text
|
|
1338
|
+
assert "Present answer" in result
|
|
1339
|
+
assert "Present Agent" in result
|
|
1340
|
+
|
|
1341
|
+
|
|
1342
|
+
# Test get_sub_agent_answer_parts
|
|
1343
|
+
|
|
1344
|
+
|
|
1345
|
+
@pytest.mark.ai
|
|
1346
|
+
def test_get_sub_agent_answer_parts__returns_empty__when_hidden_mode() -> None:
|
|
1347
|
+
"""
|
|
1348
|
+
Purpose: Verify empty list returned for HIDDEN display mode.
|
|
1349
|
+
Why this matters: Hidden mode should not extract any answer parts.
|
|
1350
|
+
Setup summary: Set mode to HIDDEN, assert empty list.
|
|
1351
|
+
"""
|
|
1352
|
+
# Arrange
|
|
1353
|
+
answer = "Some answer text"
|
|
1354
|
+
config = SubAgentDisplayConfig(
|
|
1355
|
+
mode=SubAgentResponseDisplayMode.HIDDEN,
|
|
1356
|
+
)
|
|
1357
|
+
|
|
1358
|
+
# Act
|
|
1359
|
+
result = get_sub_agent_answer_parts(answer=answer, display_config=config)
|
|
1360
|
+
|
|
1361
|
+
# Assert
|
|
1362
|
+
assert result == []
|
|
1363
|
+
|
|
1364
|
+
|
|
1365
|
+
@pytest.mark.ai
|
|
1366
|
+
def test_get_sub_agent_answer_parts__returns_full_answer__when_no_config() -> None:
|
|
1367
|
+
"""
|
|
1368
|
+
Purpose: Verify full answer returned when no substring config provided.
|
|
1369
|
+
Why this matters: Default behavior should return entire answer.
|
|
1370
|
+
Setup summary: Provide answer without substring config, assert full answer.
|
|
1371
|
+
"""
|
|
1372
|
+
# Arrange
|
|
1373
|
+
answer = "This is the complete answer"
|
|
1374
|
+
config = SubAgentDisplayConfig(
|
|
1375
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1376
|
+
answer_substrings_config=[],
|
|
1377
|
+
)
|
|
1378
|
+
|
|
1379
|
+
# Act
|
|
1380
|
+
result = get_sub_agent_answer_parts(answer=answer, display_config=config)
|
|
1381
|
+
|
|
1382
|
+
# Assert
|
|
1383
|
+
assert len(result) == 1
|
|
1384
|
+
assert result[0].matching_text == answer
|
|
1385
|
+
assert result[0].formatted_text == answer
|
|
1386
|
+
|
|
1387
|
+
|
|
1388
|
+
@pytest.mark.ai
|
|
1389
|
+
def test_get_sub_agent_answer_parts__extracts_single_match__with_one_regexp() -> None:
|
|
1390
|
+
"""
|
|
1391
|
+
Purpose: Verify single substring extracted with one regexp config.
|
|
1392
|
+
Why this matters: Core functionality for extracting specific answer parts.
|
|
1393
|
+
Setup summary: Provide answer with single regexp config, assert match extracted.
|
|
1394
|
+
"""
|
|
1395
|
+
# Arrange
|
|
1396
|
+
answer = "The price is $42.99 for the item"
|
|
1397
|
+
config = SubAgentDisplayConfig(
|
|
1398
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1399
|
+
answer_substrings_config=[
|
|
1400
|
+
SubAgentAnswerSubstringConfig(regexp=r"\$\d+\.\d+"),
|
|
1401
|
+
],
|
|
1402
|
+
)
|
|
1403
|
+
|
|
1404
|
+
# Act
|
|
1405
|
+
result = get_sub_agent_answer_parts(answer=answer, display_config=config)
|
|
1406
|
+
|
|
1407
|
+
# Assert
|
|
1408
|
+
assert len(result) == 1
|
|
1409
|
+
assert result[0].matching_text == "$42.99"
|
|
1410
|
+
assert result[0].formatted_text == "$42.99"
|
|
1411
|
+
|
|
1412
|
+
|
|
1413
|
+
@pytest.mark.ai
|
|
1414
|
+
def test_get_sub_agent_answer_parts__extracts_multiple_matches__with_multiple_regexps() -> (
|
|
1415
|
+
None
|
|
1416
|
+
):
|
|
1417
|
+
"""
|
|
1418
|
+
Purpose: Verify multiple substrings extracted with multiple regexp configs.
|
|
1419
|
+
Why this matters: Supports extracting different types of information.
|
|
1420
|
+
Setup summary: Provide answer with multiple regexp configs, assert all matches.
|
|
1421
|
+
"""
|
|
1422
|
+
# Arrange
|
|
1423
|
+
answer = "Contact John at john@example.com or call 555-1234"
|
|
1424
|
+
config = SubAgentDisplayConfig(
|
|
1425
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1426
|
+
answer_substrings_config=[
|
|
1427
|
+
SubAgentAnswerSubstringConfig(
|
|
1428
|
+
regexp=r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b"
|
|
1429
|
+
),
|
|
1430
|
+
SubAgentAnswerSubstringConfig(regexp=r"\d{3}-\d{4}"),
|
|
1431
|
+
],
|
|
1432
|
+
)
|
|
1433
|
+
|
|
1434
|
+
# Act
|
|
1435
|
+
result = get_sub_agent_answer_parts(answer=answer, display_config=config)
|
|
1436
|
+
|
|
1437
|
+
# Assert
|
|
1438
|
+
assert len(result) == 2
|
|
1439
|
+
assert result[0].matching_text == "john@example.com"
|
|
1440
|
+
assert result[1].matching_text == "555-1234"
|
|
1441
|
+
|
|
1442
|
+
|
|
1443
|
+
@pytest.mark.ai
|
|
1444
|
+
def test_get_sub_agent_answer_parts__applies_display_template__to_matched_text() -> (
|
|
1445
|
+
None
|
|
1446
|
+
):
|
|
1447
|
+
"""
|
|
1448
|
+
Purpose: Verify display template is applied to format matched text.
|
|
1449
|
+
Why this matters: Allows customization of how extracted parts are displayed.
|
|
1450
|
+
Setup summary: Provide template with placeholder, assert formatted output.
|
|
1451
|
+
"""
|
|
1452
|
+
# Arrange
|
|
1453
|
+
answer = "The temperature is 72 degrees"
|
|
1454
|
+
config = SubAgentDisplayConfig(
|
|
1455
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1456
|
+
answer_substrings_config=[
|
|
1457
|
+
SubAgentAnswerSubstringConfig(
|
|
1458
|
+
regexp=r"\d+",
|
|
1459
|
+
display_template="Temperature: {}°F",
|
|
1460
|
+
),
|
|
1461
|
+
],
|
|
1462
|
+
)
|
|
1463
|
+
|
|
1464
|
+
# Act
|
|
1465
|
+
result = get_sub_agent_answer_parts(answer=answer, display_config=config)
|
|
1466
|
+
|
|
1467
|
+
# Assert
|
|
1468
|
+
assert len(result) == 1
|
|
1469
|
+
assert result[0].matching_text == "72"
|
|
1470
|
+
assert result[0].formatted_text == "Temperature: 72°F"
|
|
1471
|
+
|
|
1472
|
+
|
|
1473
|
+
@pytest.mark.ai
|
|
1474
|
+
def test_get_sub_agent_answer_parts__returns_empty_list__when_no_matches() -> None:
|
|
1475
|
+
"""
|
|
1476
|
+
Purpose: Verify empty list returned when regexp doesn't match answer.
|
|
1477
|
+
Why this matters: Handles cases where expected pattern not present.
|
|
1478
|
+
Setup summary: Provide regexp that doesn't match, assert empty list.
|
|
1479
|
+
"""
|
|
1480
|
+
# Arrange
|
|
1481
|
+
answer = "This is plain text without numbers"
|
|
1482
|
+
config = SubAgentDisplayConfig(
|
|
1483
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1484
|
+
answer_substrings_config=[
|
|
1485
|
+
SubAgentAnswerSubstringConfig(regexp=r"\d+"),
|
|
1486
|
+
],
|
|
1487
|
+
)
|
|
1488
|
+
|
|
1489
|
+
# Act
|
|
1490
|
+
result = get_sub_agent_answer_parts(answer=answer, display_config=config)
|
|
1491
|
+
|
|
1492
|
+
# Assert
|
|
1493
|
+
assert result == []
|
|
1494
|
+
|
|
1495
|
+
|
|
1496
|
+
@pytest.mark.ai
|
|
1497
|
+
def test_get_sub_agent_answer_parts__extracts_all_matches__for_each_regexp() -> None:
|
|
1498
|
+
"""
|
|
1499
|
+
Purpose: Verify only first match per regexp is extracted.
|
|
1500
|
+
Why this matters: Function uses re.search which finds first occurrence.
|
|
1501
|
+
Setup summary: Provide answer with multiple numbers, assert only first extracted.
|
|
1502
|
+
"""
|
|
1503
|
+
# Arrange
|
|
1504
|
+
answer = "First number is 42 and second is 99"
|
|
1505
|
+
config = SubAgentDisplayConfig(
|
|
1506
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1507
|
+
answer_substrings_config=[
|
|
1508
|
+
SubAgentAnswerSubstringConfig(regexp=r"\d+"),
|
|
1509
|
+
],
|
|
1510
|
+
)
|
|
1511
|
+
|
|
1512
|
+
# Act
|
|
1513
|
+
result = get_sub_agent_answer_parts(answer=answer, display_config=config)
|
|
1514
|
+
|
|
1515
|
+
# Assert
|
|
1516
|
+
assert len(result) == 2
|
|
1517
|
+
assert result[0].matching_text == "42"
|
|
1518
|
+
|
|
1519
|
+
|
|
1520
|
+
@pytest.mark.ai
|
|
1521
|
+
def test_get_sub_agent_answer_parts__handles_empty_answer__with_no_config() -> None:
|
|
1522
|
+
"""
|
|
1523
|
+
Purpose: Verify empty answer returned as single part when no config.
|
|
1524
|
+
Why this matters: Edge case handling for empty content.
|
|
1525
|
+
Setup summary: Provide empty answer, assert single empty part.
|
|
1526
|
+
"""
|
|
1527
|
+
# Arrange
|
|
1528
|
+
answer = ""
|
|
1529
|
+
config = SubAgentDisplayConfig(
|
|
1530
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1531
|
+
answer_substrings_config=[],
|
|
1532
|
+
)
|
|
1533
|
+
|
|
1534
|
+
# Act
|
|
1535
|
+
result = get_sub_agent_answer_parts(answer=answer, display_config=config)
|
|
1536
|
+
|
|
1537
|
+
# Assert
|
|
1538
|
+
assert len(result) == 1
|
|
1539
|
+
assert result[0].matching_text == ""
|
|
1540
|
+
assert result[0].formatted_text == ""
|
|
1541
|
+
|
|
1542
|
+
|
|
1543
|
+
@pytest.mark.ai
|
|
1544
|
+
def test_get_sub_agent_answer_parts__handles_empty_answer__with_regexp_config() -> None:
|
|
1545
|
+
"""
|
|
1546
|
+
Purpose: Verify empty list returned for empty answer with regexp config.
|
|
1547
|
+
Why this matters: No matches possible in empty string.
|
|
1548
|
+
Setup summary: Provide empty answer with regexp, assert empty list.
|
|
1549
|
+
"""
|
|
1550
|
+
# Arrange
|
|
1551
|
+
answer = ""
|
|
1552
|
+
config = SubAgentDisplayConfig(
|
|
1553
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1554
|
+
answer_substrings_config=[
|
|
1555
|
+
SubAgentAnswerSubstringConfig(regexp=r"\d+"),
|
|
1556
|
+
],
|
|
1557
|
+
)
|
|
1558
|
+
|
|
1559
|
+
# Act
|
|
1560
|
+
result = get_sub_agent_answer_parts(answer=answer, display_config=config)
|
|
1561
|
+
|
|
1562
|
+
# Assert
|
|
1563
|
+
assert result == []
|
|
1564
|
+
|
|
1565
|
+
|
|
1566
|
+
@pytest.mark.ai
|
|
1567
|
+
def test_get_sub_agent_answer_parts__handles_multiline_answer__with_regexp() -> None:
|
|
1568
|
+
"""
|
|
1569
|
+
Purpose: Verify regexp matching works across multiple lines.
|
|
1570
|
+
Why this matters: Answers can span multiple lines.
|
|
1571
|
+
Setup summary: Provide multiline answer with pattern, assert match found.
|
|
1572
|
+
"""
|
|
1573
|
+
# Arrange
|
|
1574
|
+
answer = "Line 1\nThe code is ABC123\nLine 3"
|
|
1575
|
+
config = SubAgentDisplayConfig(
|
|
1576
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1577
|
+
answer_substrings_config=[
|
|
1578
|
+
SubAgentAnswerSubstringConfig(regexp=r"[A-Z]{3}\d{3}"),
|
|
1579
|
+
],
|
|
1580
|
+
)
|
|
1581
|
+
|
|
1582
|
+
# Act
|
|
1583
|
+
result = get_sub_agent_answer_parts(answer=answer, display_config=config)
|
|
1584
|
+
|
|
1585
|
+
# Assert
|
|
1586
|
+
assert len(result) == 1
|
|
1587
|
+
assert result[0].matching_text == "ABC123"
|
|
1588
|
+
|
|
1589
|
+
|
|
1590
|
+
@pytest.mark.ai
|
|
1591
|
+
def test_get_sub_agent_answer_parts__handles_special_regex_chars__in_answer() -> None:
|
|
1592
|
+
"""
|
|
1593
|
+
Purpose: Verify regexp can match content with special regex characters.
|
|
1594
|
+
Why this matters: Answers may contain special characters.
|
|
1595
|
+
Setup summary: Provide answer with special chars, use proper escaping in regexp.
|
|
1596
|
+
"""
|
|
1597
|
+
# Arrange
|
|
1598
|
+
answer = "The expression is: [test] (value)"
|
|
1599
|
+
config = SubAgentDisplayConfig(
|
|
1600
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1601
|
+
answer_substrings_config=[
|
|
1602
|
+
SubAgentAnswerSubstringConfig(regexp=r"\[test\]"),
|
|
1603
|
+
],
|
|
1604
|
+
)
|
|
1605
|
+
|
|
1606
|
+
# Act
|
|
1607
|
+
result = get_sub_agent_answer_parts(answer=answer, display_config=config)
|
|
1608
|
+
|
|
1609
|
+
# Assert
|
|
1610
|
+
assert len(result) == 1
|
|
1611
|
+
assert result[0].matching_text == "[test]"
|
|
1612
|
+
|
|
1613
|
+
|
|
1614
|
+
@pytest.mark.ai
|
|
1615
|
+
def test_get_sub_agent_answer_parts__skips_non_matching_configs__returns_matches_only() -> (
|
|
1616
|
+
None
|
|
1617
|
+
):
|
|
1618
|
+
"""
|
|
1619
|
+
Purpose: Verify only matching regexp configs produce results.
|
|
1620
|
+
Why this matters: Should not fail on partial matches, only return what matches.
|
|
1621
|
+
Setup summary: Provide multiple configs where only some match, assert partial results.
|
|
1622
|
+
"""
|
|
1623
|
+
# Arrange
|
|
1624
|
+
answer = "Value is 42"
|
|
1625
|
+
config = SubAgentDisplayConfig(
|
|
1626
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1627
|
+
answer_substrings_config=[
|
|
1628
|
+
SubAgentAnswerSubstringConfig(regexp=r"\d+"), # Matches
|
|
1629
|
+
SubAgentAnswerSubstringConfig(regexp=r"[A-Z]{3}"), # Doesn't match
|
|
1630
|
+
SubAgentAnswerSubstringConfig(regexp=r"Value"), # Matches
|
|
1631
|
+
],
|
|
1632
|
+
)
|
|
1633
|
+
|
|
1634
|
+
# Act
|
|
1635
|
+
result = get_sub_agent_answer_parts(answer=answer, display_config=config)
|
|
1636
|
+
|
|
1637
|
+
# Assert
|
|
1638
|
+
assert len(result) == 2
|
|
1639
|
+
assert result[0].matching_text == "42"
|
|
1640
|
+
assert result[1].matching_text == "Value"
|
|
1641
|
+
|
|
1642
|
+
|
|
1643
|
+
@pytest.mark.ai
|
|
1644
|
+
def test_get_sub_agent_answer_parts__preserves_order__of_configs_not_matches() -> None:
|
|
1645
|
+
"""
|
|
1646
|
+
Purpose: Verify results follow config order, not match order in text.
|
|
1647
|
+
Why this matters: Predictable output order based on configuration.
|
|
1648
|
+
Setup summary: Provide configs in specific order, assert results match config order.
|
|
1649
|
+
"""
|
|
1650
|
+
# Arrange
|
|
1651
|
+
answer = "first 123 then abc"
|
|
1652
|
+
config = SubAgentDisplayConfig(
|
|
1653
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1654
|
+
answer_substrings_config=[
|
|
1655
|
+
SubAgentAnswerSubstringConfig(regexp=r"[a-z]{3,}"), # Matches "first"
|
|
1656
|
+
SubAgentAnswerSubstringConfig(regexp=r"\d+"), # Matches "123"
|
|
1657
|
+
],
|
|
1658
|
+
)
|
|
1659
|
+
|
|
1660
|
+
# Act
|
|
1661
|
+
result = get_sub_agent_answer_parts(answer=answer, display_config=config)
|
|
1662
|
+
|
|
1663
|
+
# Assert
|
|
1664
|
+
assert len(result) == 4
|
|
1665
|
+
# Results follow config order, not text order
|
|
1666
|
+
assert result[0].matching_text == "first"
|
|
1667
|
+
assert result[1].matching_text == "then"
|
|
1668
|
+
assert result[2].matching_text == "abc"
|
|
1669
|
+
assert result[3].matching_text == "123"
|
|
1670
|
+
|
|
1671
|
+
|
|
1672
|
+
@pytest.mark.ai
|
|
1673
|
+
def test_get_sub_agent_answer_parts__handles_complex_template__with_multiple_placeholders() -> (
|
|
1674
|
+
None
|
|
1675
|
+
):
|
|
1676
|
+
"""
|
|
1677
|
+
Purpose: Verify complex display templates with formatting work correctly.
|
|
1678
|
+
Why this matters: Supports rich formatting of extracted content.
|
|
1679
|
+
Setup summary: Provide template with additional text, assert formatted correctly.
|
|
1680
|
+
"""
|
|
1681
|
+
# Arrange
|
|
1682
|
+
answer = "User score: 95"
|
|
1683
|
+
config = SubAgentDisplayConfig(
|
|
1684
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1685
|
+
answer_substrings_config=[
|
|
1686
|
+
SubAgentAnswerSubstringConfig(
|
|
1687
|
+
regexp=r"\d+",
|
|
1688
|
+
display_template="**Score: {}%**",
|
|
1689
|
+
),
|
|
1690
|
+
],
|
|
1691
|
+
)
|
|
1692
|
+
|
|
1693
|
+
# Act
|
|
1694
|
+
result = get_sub_agent_answer_parts(answer=answer, display_config=config)
|
|
1695
|
+
|
|
1696
|
+
# Assert
|
|
1697
|
+
assert len(result) == 1
|
|
1698
|
+
assert result[0].matching_text == "95"
|
|
1699
|
+
assert result[0].formatted_text == "**Score: 95%**"
|
|
1700
|
+
|
|
1701
|
+
|
|
1702
|
+
@pytest.mark.ai
|
|
1703
|
+
def test_get_sub_agent_answer_parts__works_with_details_modes__extracts_normally() -> (
|
|
1704
|
+
None
|
|
1705
|
+
):
|
|
1706
|
+
"""
|
|
1707
|
+
Purpose: Verify extraction works regardless of display mode (except HIDDEN).
|
|
1708
|
+
Why this matters: Substring extraction independent of display mode.
|
|
1709
|
+
Setup summary: Use DETAILS modes, assert extraction still works.
|
|
1710
|
+
"""
|
|
1711
|
+
# Arrange
|
|
1712
|
+
answer = "Result: SUCCESS"
|
|
1713
|
+
config = SubAgentDisplayConfig(
|
|
1714
|
+
mode=SubAgentResponseDisplayMode.DETAILS_CLOSED,
|
|
1715
|
+
answer_substrings_config=[
|
|
1716
|
+
SubAgentAnswerSubstringConfig(regexp=r"SUCCESS"),
|
|
1717
|
+
],
|
|
1718
|
+
)
|
|
1719
|
+
|
|
1720
|
+
# Act
|
|
1721
|
+
result = get_sub_agent_answer_parts(answer=answer, display_config=config)
|
|
1722
|
+
|
|
1723
|
+
# Assert
|
|
1724
|
+
assert len(result) == 1
|
|
1725
|
+
assert result[0].matching_text == "SUCCESS"
|
|
1726
|
+
|
|
1727
|
+
|
|
1728
|
+
# Test get_sub_agent_answer_from_parts
|
|
1729
|
+
|
|
1730
|
+
|
|
1731
|
+
@pytest.mark.ai
|
|
1732
|
+
def test_get_sub_agent_answer_from_parts__returns_empty__with_empty_list() -> None:
|
|
1733
|
+
"""
|
|
1734
|
+
Purpose: Verify empty or minimal output when no answer parts provided.
|
|
1735
|
+
Why this matters: Handles edge case of no extracted content.
|
|
1736
|
+
Setup summary: Provide empty list, assert minimal rendered output.
|
|
1737
|
+
"""
|
|
1738
|
+
# Arrange
|
|
1739
|
+
answer_parts: list[SubAgentAnswerPart] = []
|
|
1740
|
+
config = SubAgentDisplayConfig(
|
|
1741
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1742
|
+
)
|
|
1743
|
+
|
|
1744
|
+
# Act
|
|
1745
|
+
result = get_sub_agent_answer_from_parts(
|
|
1746
|
+
answer_parts=answer_parts,
|
|
1747
|
+
config=config,
|
|
1748
|
+
)
|
|
1749
|
+
|
|
1750
|
+
# Assert
|
|
1751
|
+
assert result == ""
|
|
1752
|
+
|
|
1753
|
+
|
|
1754
|
+
@pytest.mark.ai
|
|
1755
|
+
def test_get_sub_agent_answer_from_parts__renders_single_part__with_default_template() -> (
|
|
1756
|
+
None
|
|
1757
|
+
):
|
|
1758
|
+
"""
|
|
1759
|
+
Purpose: Verify single answer part is rendered using default template.
|
|
1760
|
+
Why this matters: Core functionality for single substring display.
|
|
1761
|
+
Setup summary: Provide single part with default template, assert rendered text.
|
|
1762
|
+
"""
|
|
1763
|
+
# Arrange
|
|
1764
|
+
answer_parts = [
|
|
1765
|
+
SubAgentAnswerPart(matching_text="42", formatted_text="The answer is 42"),
|
|
1766
|
+
]
|
|
1767
|
+
config = SubAgentDisplayConfig(
|
|
1768
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1769
|
+
)
|
|
1770
|
+
|
|
1771
|
+
# Act
|
|
1772
|
+
result = get_sub_agent_answer_from_parts(
|
|
1773
|
+
answer_parts=answer_parts,
|
|
1774
|
+
config=config,
|
|
1775
|
+
)
|
|
1776
|
+
|
|
1777
|
+
# Assert
|
|
1778
|
+
assert "The answer is 42" in result
|
|
1779
|
+
|
|
1780
|
+
|
|
1781
|
+
@pytest.mark.ai
|
|
1782
|
+
def test_get_sub_agent_answer_from_parts__renders_multiple_parts__with_default_template() -> (
|
|
1783
|
+
None
|
|
1784
|
+
):
|
|
1785
|
+
"""
|
|
1786
|
+
Purpose: Verify multiple answer parts are rendered with default template.
|
|
1787
|
+
Why this matters: Supports displaying multiple extracted substrings.
|
|
1788
|
+
Setup summary: Provide multiple parts, assert all rendered in output.
|
|
1789
|
+
"""
|
|
1790
|
+
# Arrange
|
|
1791
|
+
answer_parts = [
|
|
1792
|
+
SubAgentAnswerPart(
|
|
1793
|
+
matching_text="john@example.com", formatted_text="Email: john@example.com"
|
|
1794
|
+
),
|
|
1795
|
+
SubAgentAnswerPart(matching_text="555-1234", formatted_text="Phone: 555-1234"),
|
|
1796
|
+
SubAgentAnswerPart(matching_text="John Doe", formatted_text="Name: John Doe"),
|
|
1797
|
+
]
|
|
1798
|
+
config = SubAgentDisplayConfig(
|
|
1799
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1800
|
+
)
|
|
1801
|
+
|
|
1802
|
+
# Act
|
|
1803
|
+
result = get_sub_agent_answer_from_parts(
|
|
1804
|
+
answer_parts=answer_parts,
|
|
1805
|
+
config=config,
|
|
1806
|
+
)
|
|
1807
|
+
|
|
1808
|
+
# Assert
|
|
1809
|
+
assert "Email: john@example.com" in result
|
|
1810
|
+
assert "Phone: 555-1234" in result
|
|
1811
|
+
assert "Name: John Doe" in result
|
|
1812
|
+
|
|
1813
|
+
|
|
1814
|
+
@pytest.mark.ai
|
|
1815
|
+
def test_get_sub_agent_answer_from_parts__uses_formatted_text__not_matching_text() -> (
|
|
1816
|
+
None
|
|
1817
|
+
):
|
|
1818
|
+
"""
|
|
1819
|
+
Purpose: Verify function uses formatted_text from parts, not matching_text.
|
|
1820
|
+
Why this matters: Formatted text includes display template application.
|
|
1821
|
+
Setup summary: Provide parts with different matching vs formatted text, assert formatted used.
|
|
1822
|
+
"""
|
|
1823
|
+
# Arrange
|
|
1824
|
+
answer_parts = [
|
|
1825
|
+
SubAgentAnswerPart(matching_text="72", formatted_text="Temperature: 72°F"),
|
|
1826
|
+
]
|
|
1827
|
+
config = SubAgentDisplayConfig(
|
|
1828
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1829
|
+
)
|
|
1830
|
+
|
|
1831
|
+
# Act
|
|
1832
|
+
result = get_sub_agent_answer_from_parts(
|
|
1833
|
+
answer_parts=answer_parts,
|
|
1834
|
+
config=config,
|
|
1835
|
+
)
|
|
1836
|
+
|
|
1837
|
+
# Assert
|
|
1838
|
+
assert "Temperature: 72°F" in result
|
|
1839
|
+
assert result.count("72") == 1 # Only formatted version, not raw matching_text
|
|
1840
|
+
|
|
1841
|
+
|
|
1842
|
+
@pytest.mark.ai
|
|
1843
|
+
def test_get_sub_agent_answer_from_parts__renders_with_custom_template__single_part() -> (
|
|
1844
|
+
None
|
|
1845
|
+
):
|
|
1846
|
+
"""
|
|
1847
|
+
Purpose: Verify custom Jinja template is applied correctly for single part.
|
|
1848
|
+
Why this matters: Supports custom formatting via configuration.
|
|
1849
|
+
Setup summary: Provide custom template with HTML, assert custom rendering.
|
|
1850
|
+
"""
|
|
1851
|
+
# Arrange
|
|
1852
|
+
answer_parts = [
|
|
1853
|
+
SubAgentAnswerPart(matching_text="Success", formatted_text="Status: Success"),
|
|
1854
|
+
]
|
|
1855
|
+
custom_template = "<div class='result'>{{ substrings[0] }}</div>"
|
|
1856
|
+
config = SubAgentDisplayConfig(
|
|
1857
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1858
|
+
answer_substrings_jinja_template=custom_template,
|
|
1859
|
+
)
|
|
1860
|
+
|
|
1861
|
+
# Act
|
|
1862
|
+
result = get_sub_agent_answer_from_parts(
|
|
1863
|
+
answer_parts=answer_parts,
|
|
1864
|
+
config=config,
|
|
1865
|
+
)
|
|
1866
|
+
|
|
1867
|
+
# Assert
|
|
1868
|
+
assert result == "<div class='result'>Status: Success</div>"
|
|
1869
|
+
|
|
1870
|
+
|
|
1871
|
+
@pytest.mark.ai
|
|
1872
|
+
def test_get_sub_agent_answer_from_parts__renders_with_custom_template__multiple_parts() -> (
|
|
1873
|
+
None
|
|
1874
|
+
):
|
|
1875
|
+
"""
|
|
1876
|
+
Purpose: Verify custom template works with multiple parts and loop constructs.
|
|
1877
|
+
Why this matters: Supports complex formatting with iteration.
|
|
1878
|
+
Setup summary: Provide custom template with for loop, assert all parts rendered.
|
|
1879
|
+
"""
|
|
1880
|
+
# Arrange
|
|
1881
|
+
answer_parts = [
|
|
1882
|
+
SubAgentAnswerPart(matching_text="Item1", formatted_text="- Item 1"),
|
|
1883
|
+
SubAgentAnswerPart(matching_text="Item2", formatted_text="- Item 2"),
|
|
1884
|
+
SubAgentAnswerPart(matching_text="Item3", formatted_text="- Item 3"),
|
|
1885
|
+
]
|
|
1886
|
+
custom_template = """
|
|
1887
|
+
<ul>
|
|
1888
|
+
{% for substring in substrings %}
|
|
1889
|
+
<li>{{ substring }}</li>
|
|
1890
|
+
{% endfor %}
|
|
1891
|
+
</ul>
|
|
1892
|
+
""".strip()
|
|
1893
|
+
config = SubAgentDisplayConfig(
|
|
1894
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1895
|
+
answer_substrings_jinja_template=custom_template,
|
|
1896
|
+
)
|
|
1897
|
+
|
|
1898
|
+
# Act
|
|
1899
|
+
result = get_sub_agent_answer_from_parts(
|
|
1900
|
+
answer_parts=answer_parts,
|
|
1901
|
+
config=config,
|
|
1902
|
+
)
|
|
1903
|
+
|
|
1904
|
+
# Assert
|
|
1905
|
+
assert "<ul>" in result
|
|
1906
|
+
assert "</ul>" in result
|
|
1907
|
+
assert "<li>- Item 1</li>" in result
|
|
1908
|
+
assert "<li>- Item 2</li>" in result
|
|
1909
|
+
assert "<li>- Item 3</li>" in result
|
|
1910
|
+
|
|
1911
|
+
|
|
1912
|
+
@pytest.mark.ai
|
|
1913
|
+
def test_get_sub_agent_answer_from_parts__preserves_order__of_parts() -> None:
|
|
1914
|
+
"""
|
|
1915
|
+
Purpose: Verify parts are rendered in the order they appear in the list.
|
|
1916
|
+
Why this matters: Predictable output order based on input order.
|
|
1917
|
+
Setup summary: Provide parts in specific order, assert same order in output.
|
|
1918
|
+
"""
|
|
1919
|
+
# Arrange
|
|
1920
|
+
answer_parts = [
|
|
1921
|
+
SubAgentAnswerPart(matching_text="First", formatted_text="1. First"),
|
|
1922
|
+
SubAgentAnswerPart(matching_text="Second", formatted_text="2. Second"),
|
|
1923
|
+
SubAgentAnswerPart(matching_text="Third", formatted_text="3. Third"),
|
|
1924
|
+
]
|
|
1925
|
+
config = SubAgentDisplayConfig(
|
|
1926
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1927
|
+
)
|
|
1928
|
+
|
|
1929
|
+
# Act
|
|
1930
|
+
result = get_sub_agent_answer_from_parts(
|
|
1931
|
+
answer_parts=answer_parts,
|
|
1932
|
+
config=config,
|
|
1933
|
+
)
|
|
1934
|
+
|
|
1935
|
+
# Assert
|
|
1936
|
+
# Check order by finding positions
|
|
1937
|
+
pos_first = result.find("1. First")
|
|
1938
|
+
pos_second = result.find("2. Second")
|
|
1939
|
+
pos_third = result.find("3. Third")
|
|
1940
|
+
assert pos_first < pos_second < pos_third
|
|
1941
|
+
|
|
1942
|
+
|
|
1943
|
+
@pytest.mark.ai
|
|
1944
|
+
def test_get_sub_agent_answer_from_parts__handles_special_chars__in_formatted_text() -> (
|
|
1945
|
+
None
|
|
1946
|
+
):
|
|
1947
|
+
"""
|
|
1948
|
+
Purpose: Verify formatted text with special characters renders correctly.
|
|
1949
|
+
Why this matters: Answers may contain HTML entities or special symbols.
|
|
1950
|
+
Setup summary: Provide parts with special chars, assert rendered as-is.
|
|
1951
|
+
"""
|
|
1952
|
+
# Arrange
|
|
1953
|
+
answer_parts = [
|
|
1954
|
+
SubAgentAnswerPart(
|
|
1955
|
+
matching_text="test",
|
|
1956
|
+
formatted_text="Result: <tag> & 'quotes' & \"double\" & 50% & $100",
|
|
1957
|
+
),
|
|
1958
|
+
]
|
|
1959
|
+
config = SubAgentDisplayConfig(
|
|
1960
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1961
|
+
)
|
|
1962
|
+
|
|
1963
|
+
# Act
|
|
1964
|
+
result = get_sub_agent_answer_from_parts(
|
|
1965
|
+
answer_parts=answer_parts,
|
|
1966
|
+
config=config,
|
|
1967
|
+
)
|
|
1968
|
+
|
|
1969
|
+
# Assert
|
|
1970
|
+
# Jinja2 default behavior does not escape, so special chars should be preserved
|
|
1971
|
+
assert "<tag>" in result
|
|
1972
|
+
assert "&" in result
|
|
1973
|
+
assert "'" in result
|
|
1974
|
+
assert '"' in result
|
|
1975
|
+
assert "%" in result
|
|
1976
|
+
assert "$" in result
|
|
1977
|
+
|
|
1978
|
+
|
|
1979
|
+
@pytest.mark.ai
|
|
1980
|
+
def test_get_sub_agent_answer_from_parts__handles_multiline_formatted_text() -> None:
|
|
1981
|
+
"""
|
|
1982
|
+
Purpose: Verify formatted text with newlines renders correctly.
|
|
1983
|
+
Why this matters: Formatted content may span multiple lines.
|
|
1984
|
+
Setup summary: Provide parts with newlines, assert multiline output.
|
|
1985
|
+
"""
|
|
1986
|
+
# Arrange
|
|
1987
|
+
answer_parts = [
|
|
1988
|
+
SubAgentAnswerPart(
|
|
1989
|
+
matching_text="multiline",
|
|
1990
|
+
formatted_text="Line 1\nLine 2\nLine 3",
|
|
1991
|
+
),
|
|
1992
|
+
]
|
|
1993
|
+
config = SubAgentDisplayConfig(
|
|
1994
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1995
|
+
)
|
|
1996
|
+
|
|
1997
|
+
# Act
|
|
1998
|
+
result = get_sub_agent_answer_from_parts(
|
|
1999
|
+
answer_parts=answer_parts,
|
|
2000
|
+
config=config,
|
|
2001
|
+
)
|
|
2002
|
+
|
|
2003
|
+
# Assert
|
|
2004
|
+
assert "Line 1" in result
|
|
2005
|
+
assert "Line 2" in result
|
|
2006
|
+
assert "Line 3" in result
|
|
2007
|
+
|
|
2008
|
+
|
|
2009
|
+
@pytest.mark.ai
|
|
2010
|
+
def test_get_sub_agent_answer_from_parts__works_with_custom_template__conditional_logic() -> (
|
|
2011
|
+
None
|
|
2012
|
+
):
|
|
2013
|
+
"""
|
|
2014
|
+
Purpose: Verify custom template with Jinja conditionals works correctly.
|
|
2015
|
+
Why this matters: Supports advanced formatting with conditional rendering.
|
|
2016
|
+
Setup summary: Provide custom template with if statement, assert conditional output.
|
|
2017
|
+
"""
|
|
2018
|
+
# Arrange
|
|
2019
|
+
answer_parts = [
|
|
2020
|
+
SubAgentAnswerPart(matching_text="a", formatted_text="Item A"),
|
|
2021
|
+
SubAgentAnswerPart(matching_text="b", formatted_text="Item B"),
|
|
2022
|
+
]
|
|
2023
|
+
custom_template = """
|
|
2024
|
+
{% if substrings|length > 1 %}
|
|
2025
|
+
Multiple items: {{ substrings|join(', ') }}
|
|
2026
|
+
{% else %}
|
|
2027
|
+
Single item: {{ substrings[0] }}
|
|
2028
|
+
{% endif %}
|
|
2029
|
+
""".strip()
|
|
2030
|
+
config = SubAgentDisplayConfig(
|
|
2031
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
2032
|
+
answer_substrings_jinja_template=custom_template,
|
|
2033
|
+
)
|
|
2034
|
+
|
|
2035
|
+
# Act
|
|
2036
|
+
result = get_sub_agent_answer_from_parts(
|
|
2037
|
+
answer_parts=answer_parts,
|
|
2038
|
+
config=config,
|
|
2039
|
+
)
|
|
2040
|
+
|
|
2041
|
+
# Assert
|
|
2042
|
+
assert "Multiple items:" in result
|
|
2043
|
+
assert "Item A, Item B" in result
|
|
2044
|
+
|
|
2045
|
+
|
|
2046
|
+
@pytest.mark.ai
|
|
2047
|
+
def test_get_sub_agent_answer_from_parts__empty_formatted_text__renders_empty() -> None:
|
|
2048
|
+
"""
|
|
2049
|
+
Purpose: Verify parts with empty formatted_text render as empty.
|
|
2050
|
+
Why this matters: Edge case handling for empty content.
|
|
2051
|
+
Setup summary: Provide parts with empty formatted_text, assert minimal output.
|
|
2052
|
+
"""
|
|
2053
|
+
# Arrange
|
|
2054
|
+
answer_parts = [
|
|
2055
|
+
SubAgentAnswerPart(matching_text="something", formatted_text=""),
|
|
2056
|
+
]
|
|
2057
|
+
config = SubAgentDisplayConfig(
|
|
2058
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
2059
|
+
)
|
|
2060
|
+
|
|
2061
|
+
# Act
|
|
2062
|
+
result = get_sub_agent_answer_from_parts(
|
|
2063
|
+
answer_parts=answer_parts,
|
|
2064
|
+
config=config,
|
|
2065
|
+
)
|
|
2066
|
+
|
|
2067
|
+
# Assert
|
|
2068
|
+
# Default template renders empty string for empty formatted_text
|
|
2069
|
+
assert result.strip() == ""
|
|
2070
|
+
|
|
2071
|
+
|
|
2072
|
+
@pytest.mark.ai
|
|
2073
|
+
def test_get_sub_agent_answer_from_parts__integration__with_get_sub_agent_answer_parts() -> (
|
|
2074
|
+
None
|
|
2075
|
+
):
|
|
2076
|
+
"""
|
|
2077
|
+
Purpose: Verify integration between get_sub_agent_answer_parts and get_sub_agent_answer_from_parts.
|
|
2078
|
+
Why this matters: These functions work together in the display pipeline.
|
|
2079
|
+
Setup summary: Use get_sub_agent_answer_parts output as input, assert complete workflow.
|
|
2080
|
+
"""
|
|
2081
|
+
# Arrange
|
|
2082
|
+
answer = "Contact: john@example.com or call 555-1234"
|
|
2083
|
+
config = SubAgentDisplayConfig(
|
|
2084
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
2085
|
+
answer_substrings_config=[
|
|
2086
|
+
SubAgentAnswerSubstringConfig(
|
|
2087
|
+
regexp=r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b",
|
|
2088
|
+
display_template="Email: {}",
|
|
2089
|
+
),
|
|
2090
|
+
SubAgentAnswerSubstringConfig(
|
|
2091
|
+
regexp=r"\d{3}-\d{4}",
|
|
2092
|
+
display_template="Phone: {}",
|
|
2093
|
+
),
|
|
2094
|
+
],
|
|
2095
|
+
)
|
|
2096
|
+
|
|
2097
|
+
# Act
|
|
2098
|
+
parts = get_sub_agent_answer_parts(answer=answer, display_config=config)
|
|
2099
|
+
result = get_sub_agent_answer_from_parts(answer_parts=parts, config=config)
|
|
2100
|
+
|
|
2101
|
+
# Assert
|
|
2102
|
+
assert "Email: john@example.com" in result
|
|
2103
|
+
assert "Phone: 555-1234" in result
|