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.
Files changed (190) hide show
  1. unique_toolkit/__init__.py +36 -3
  2. unique_toolkit/_common/api_calling/human_verification_manager.py +357 -0
  3. unique_toolkit/_common/base_model_type_attribute.py +303 -0
  4. unique_toolkit/_common/chunk_relevancy_sorter/config.py +49 -0
  5. unique_toolkit/_common/chunk_relevancy_sorter/exception.py +5 -0
  6. unique_toolkit/_common/chunk_relevancy_sorter/schemas.py +46 -0
  7. unique_toolkit/_common/chunk_relevancy_sorter/service.py +374 -0
  8. unique_toolkit/_common/chunk_relevancy_sorter/tests/test_service.py +275 -0
  9. unique_toolkit/_common/default_language_model.py +12 -0
  10. unique_toolkit/_common/docx_generator/__init__.py +7 -0
  11. unique_toolkit/_common/docx_generator/config.py +12 -0
  12. unique_toolkit/_common/docx_generator/schemas.py +80 -0
  13. unique_toolkit/_common/docx_generator/service.py +225 -0
  14. unique_toolkit/_common/docx_generator/template/Doc Template.docx +0 -0
  15. unique_toolkit/_common/endpoint_builder.py +368 -0
  16. unique_toolkit/_common/endpoint_requestor.py +480 -0
  17. unique_toolkit/_common/exception.py +24 -0
  18. unique_toolkit/_common/experimental/endpoint_builder.py +368 -0
  19. unique_toolkit/_common/experimental/endpoint_requestor.py +488 -0
  20. unique_toolkit/_common/feature_flags/schema.py +9 -0
  21. unique_toolkit/_common/pydantic/rjsf_tags.py +936 -0
  22. unique_toolkit/_common/pydantic_helpers.py +174 -0
  23. unique_toolkit/_common/referencing.py +53 -0
  24. unique_toolkit/_common/string_utilities.py +140 -0
  25. unique_toolkit/_common/tests/test_referencing.py +521 -0
  26. unique_toolkit/_common/tests/test_string_utilities.py +506 -0
  27. unique_toolkit/_common/token/image_token_counting.py +67 -0
  28. unique_toolkit/_common/token/token_counting.py +204 -0
  29. unique_toolkit/_common/utils/__init__.py +1 -0
  30. unique_toolkit/_common/utils/files.py +43 -0
  31. unique_toolkit/_common/utils/image/encode.py +25 -0
  32. unique_toolkit/_common/utils/jinja/helpers.py +10 -0
  33. unique_toolkit/_common/utils/jinja/render.py +18 -0
  34. unique_toolkit/_common/utils/jinja/schema.py +65 -0
  35. unique_toolkit/_common/utils/jinja/utils.py +80 -0
  36. unique_toolkit/_common/utils/structured_output/__init__.py +1 -0
  37. unique_toolkit/_common/utils/structured_output/schema.py +5 -0
  38. unique_toolkit/_common/utils/write_configuration.py +51 -0
  39. unique_toolkit/_common/validators.py +101 -4
  40. unique_toolkit/agentic/__init__.py +1 -0
  41. unique_toolkit/agentic/debug_info_manager/debug_info_manager.py +28 -0
  42. unique_toolkit/agentic/debug_info_manager/test/test_debug_info_manager.py +278 -0
  43. unique_toolkit/agentic/evaluation/config.py +36 -0
  44. unique_toolkit/{evaluators → agentic/evaluation}/context_relevancy/prompts.py +25 -0
  45. unique_toolkit/agentic/evaluation/context_relevancy/schema.py +80 -0
  46. unique_toolkit/agentic/evaluation/context_relevancy/service.py +273 -0
  47. unique_toolkit/agentic/evaluation/evaluation_manager.py +218 -0
  48. unique_toolkit/agentic/evaluation/hallucination/constants.py +61 -0
  49. unique_toolkit/agentic/evaluation/hallucination/hallucination_evaluation.py +112 -0
  50. unique_toolkit/{evaluators → agentic/evaluation}/hallucination/prompts.py +1 -1
  51. unique_toolkit/{evaluators → agentic/evaluation}/hallucination/service.py +20 -16
  52. unique_toolkit/{evaluators → agentic/evaluation}/hallucination/utils.py +32 -21
  53. unique_toolkit/{evaluators → agentic/evaluation}/output_parser.py +20 -2
  54. unique_toolkit/{evaluators → agentic/evaluation}/schemas.py +27 -7
  55. unique_toolkit/agentic/evaluation/tests/test_context_relevancy_service.py +253 -0
  56. unique_toolkit/agentic/evaluation/tests/test_output_parser.py +87 -0
  57. unique_toolkit/agentic/history_manager/history_construction_with_contents.py +298 -0
  58. unique_toolkit/agentic/history_manager/history_manager.py +241 -0
  59. unique_toolkit/agentic/history_manager/loop_token_reducer.py +484 -0
  60. unique_toolkit/agentic/history_manager/utils.py +96 -0
  61. unique_toolkit/agentic/message_log_manager/__init__.py +5 -0
  62. unique_toolkit/agentic/message_log_manager/service.py +93 -0
  63. unique_toolkit/agentic/postprocessor/postprocessor_manager.py +212 -0
  64. unique_toolkit/agentic/reference_manager/reference_manager.py +103 -0
  65. unique_toolkit/agentic/responses_api/__init__.py +19 -0
  66. unique_toolkit/agentic/responses_api/postprocessors/code_display.py +71 -0
  67. unique_toolkit/agentic/responses_api/postprocessors/generated_files.py +297 -0
  68. unique_toolkit/agentic/responses_api/stream_handler.py +15 -0
  69. unique_toolkit/agentic/short_term_memory_manager/persistent_short_term_memory_manager.py +141 -0
  70. unique_toolkit/agentic/thinking_manager/thinking_manager.py +103 -0
  71. unique_toolkit/agentic/tools/__init__.py +1 -0
  72. unique_toolkit/agentic/tools/a2a/__init__.py +36 -0
  73. unique_toolkit/agentic/tools/a2a/config.py +17 -0
  74. unique_toolkit/agentic/tools/a2a/evaluation/__init__.py +15 -0
  75. unique_toolkit/agentic/tools/a2a/evaluation/_utils.py +66 -0
  76. unique_toolkit/agentic/tools/a2a/evaluation/config.py +55 -0
  77. unique_toolkit/agentic/tools/a2a/evaluation/evaluator.py +260 -0
  78. unique_toolkit/agentic/tools/a2a/evaluation/summarization_user_message.j2 +9 -0
  79. unique_toolkit/agentic/tools/a2a/manager.py +55 -0
  80. unique_toolkit/agentic/tools/a2a/postprocessing/__init__.py +21 -0
  81. unique_toolkit/agentic/tools/a2a/postprocessing/_display_utils.py +240 -0
  82. unique_toolkit/agentic/tools/a2a/postprocessing/_ref_utils.py +84 -0
  83. unique_toolkit/agentic/tools/a2a/postprocessing/config.py +78 -0
  84. unique_toolkit/agentic/tools/a2a/postprocessing/display.py +264 -0
  85. unique_toolkit/agentic/tools/a2a/postprocessing/references.py +101 -0
  86. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display.py +421 -0
  87. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display_utils.py +2103 -0
  88. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_ref_utils.py +603 -0
  89. unique_toolkit/agentic/tools/a2a/prompts.py +46 -0
  90. unique_toolkit/agentic/tools/a2a/response_watcher/__init__.py +6 -0
  91. unique_toolkit/agentic/tools/a2a/response_watcher/service.py +91 -0
  92. unique_toolkit/agentic/tools/a2a/tool/__init__.py +4 -0
  93. unique_toolkit/agentic/tools/a2a/tool/_memory.py +26 -0
  94. unique_toolkit/agentic/tools/a2a/tool/_schema.py +9 -0
  95. unique_toolkit/agentic/tools/a2a/tool/config.py +158 -0
  96. unique_toolkit/agentic/tools/a2a/tool/service.py +393 -0
  97. unique_toolkit/agentic/tools/agent_chunks_hanlder.py +65 -0
  98. unique_toolkit/agentic/tools/config.py +128 -0
  99. unique_toolkit/agentic/tools/factory.py +44 -0
  100. unique_toolkit/agentic/tools/mcp/__init__.py +4 -0
  101. unique_toolkit/agentic/tools/mcp/manager.py +71 -0
  102. unique_toolkit/agentic/tools/mcp/models.py +28 -0
  103. unique_toolkit/agentic/tools/mcp/tool_wrapper.py +234 -0
  104. unique_toolkit/agentic/tools/openai_builtin/__init__.py +11 -0
  105. unique_toolkit/agentic/tools/openai_builtin/base.py +46 -0
  106. unique_toolkit/agentic/tools/openai_builtin/code_interpreter/__init__.py +8 -0
  107. unique_toolkit/agentic/tools/openai_builtin/code_interpreter/config.py +88 -0
  108. unique_toolkit/agentic/tools/openai_builtin/code_interpreter/service.py +250 -0
  109. unique_toolkit/agentic/tools/openai_builtin/manager.py +79 -0
  110. unique_toolkit/agentic/tools/schemas.py +145 -0
  111. unique_toolkit/agentic/tools/test/test_mcp_manager.py +536 -0
  112. unique_toolkit/agentic/tools/test/test_tool_progress_reporter.py +445 -0
  113. unique_toolkit/agentic/tools/tool.py +187 -0
  114. unique_toolkit/agentic/tools/tool_manager.py +492 -0
  115. unique_toolkit/agentic/tools/tool_progress_reporter.py +285 -0
  116. unique_toolkit/agentic/tools/utils/__init__.py +19 -0
  117. unique_toolkit/agentic/tools/utils/execution/__init__.py +1 -0
  118. unique_toolkit/agentic/tools/utils/execution/execution.py +286 -0
  119. unique_toolkit/agentic/tools/utils/source_handling/__init__.py +0 -0
  120. unique_toolkit/agentic/tools/utils/source_handling/schema.py +21 -0
  121. unique_toolkit/agentic/tools/utils/source_handling/source_formatting.py +207 -0
  122. unique_toolkit/agentic/tools/utils/source_handling/tests/test_source_formatting.py +216 -0
  123. unique_toolkit/app/__init__.py +9 -0
  124. unique_toolkit/app/dev_util.py +180 -0
  125. unique_toolkit/app/fast_api_factory.py +131 -0
  126. unique_toolkit/app/init_sdk.py +32 -1
  127. unique_toolkit/app/schemas.py +206 -31
  128. unique_toolkit/app/unique_settings.py +367 -0
  129. unique_toolkit/app/webhook.py +77 -0
  130. unique_toolkit/chat/__init__.py +8 -1
  131. unique_toolkit/chat/deprecated/service.py +232 -0
  132. unique_toolkit/chat/functions.py +648 -78
  133. unique_toolkit/chat/rendering.py +34 -0
  134. unique_toolkit/chat/responses_api.py +461 -0
  135. unique_toolkit/chat/schemas.py +134 -2
  136. unique_toolkit/chat/service.py +115 -767
  137. unique_toolkit/content/functions.py +353 -8
  138. unique_toolkit/content/schemas.py +128 -15
  139. unique_toolkit/content/service.py +321 -45
  140. unique_toolkit/content/smart_rules.py +301 -0
  141. unique_toolkit/content/utils.py +10 -3
  142. unique_toolkit/data_extraction/README.md +96 -0
  143. unique_toolkit/data_extraction/__init__.py +11 -0
  144. unique_toolkit/data_extraction/augmented/__init__.py +5 -0
  145. unique_toolkit/data_extraction/augmented/service.py +93 -0
  146. unique_toolkit/data_extraction/base.py +25 -0
  147. unique_toolkit/data_extraction/basic/__init__.py +11 -0
  148. unique_toolkit/data_extraction/basic/config.py +18 -0
  149. unique_toolkit/data_extraction/basic/prompt.py +13 -0
  150. unique_toolkit/data_extraction/basic/service.py +55 -0
  151. unique_toolkit/embedding/service.py +103 -12
  152. unique_toolkit/framework_utilities/__init__.py +1 -0
  153. unique_toolkit/framework_utilities/langchain/__init__.py +10 -0
  154. unique_toolkit/framework_utilities/langchain/client.py +71 -0
  155. unique_toolkit/framework_utilities/langchain/history.py +19 -0
  156. unique_toolkit/framework_utilities/openai/__init__.py +6 -0
  157. unique_toolkit/framework_utilities/openai/client.py +84 -0
  158. unique_toolkit/framework_utilities/openai/message_builder.py +229 -0
  159. unique_toolkit/framework_utilities/utils.py +23 -0
  160. unique_toolkit/language_model/__init__.py +3 -0
  161. unique_toolkit/language_model/_responses_api_utils.py +93 -0
  162. unique_toolkit/language_model/builder.py +27 -11
  163. unique_toolkit/language_model/default_language_model.py +3 -0
  164. unique_toolkit/language_model/functions.py +345 -43
  165. unique_toolkit/language_model/infos.py +1288 -46
  166. unique_toolkit/language_model/reference.py +242 -0
  167. unique_toolkit/language_model/schemas.py +481 -49
  168. unique_toolkit/language_model/service.py +229 -28
  169. unique_toolkit/protocols/support.py +145 -0
  170. unique_toolkit/services/__init__.py +7 -0
  171. unique_toolkit/services/chat_service.py +1631 -0
  172. unique_toolkit/services/knowledge_base.py +1094 -0
  173. unique_toolkit/short_term_memory/service.py +178 -41
  174. unique_toolkit/smart_rules/__init__.py +0 -0
  175. unique_toolkit/smart_rules/compile.py +56 -0
  176. unique_toolkit/test_utilities/events.py +197 -0
  177. unique_toolkit-1.33.3.dist-info/METADATA +1145 -0
  178. unique_toolkit-1.33.3.dist-info/RECORD +205 -0
  179. unique_toolkit/evaluators/__init__.py +0 -1
  180. unique_toolkit/evaluators/config.py +0 -35
  181. unique_toolkit/evaluators/constants.py +0 -1
  182. unique_toolkit/evaluators/context_relevancy/constants.py +0 -32
  183. unique_toolkit/evaluators/context_relevancy/service.py +0 -53
  184. unique_toolkit/evaluators/context_relevancy/utils.py +0 -142
  185. unique_toolkit/evaluators/hallucination/constants.py +0 -41
  186. unique_toolkit-0.7.9.dist-info/METADATA +0 -413
  187. unique_toolkit-0.7.9.dist-info/RECORD +0 -64
  188. /unique_toolkit/{evaluators → agentic/evaluation}/exception.py +0 -0
  189. {unique_toolkit-0.7.9.dist-info → unique_toolkit-1.33.3.dist-info}/LICENSE +0 -0
  190. {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