unique_toolkit 0.7.7__py3-none-any.whl → 1.23.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of unique_toolkit might be problematic. Click here for more details.

Files changed (166) hide show
  1. unique_toolkit/__init__.py +28 -1
  2. unique_toolkit/_common/api_calling/human_verification_manager.py +343 -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 +252 -0
  14. unique_toolkit/_common/docx_generator/template/Doc Template.docx +0 -0
  15. unique_toolkit/_common/endpoint_builder.py +305 -0
  16. unique_toolkit/_common/endpoint_requestor.py +430 -0
  17. unique_toolkit/_common/exception.py +24 -0
  18. unique_toolkit/_common/feature_flags/schema.py +9 -0
  19. unique_toolkit/_common/pydantic/rjsf_tags.py +936 -0
  20. unique_toolkit/_common/pydantic_helpers.py +154 -0
  21. unique_toolkit/_common/referencing.py +53 -0
  22. unique_toolkit/_common/string_utilities.py +140 -0
  23. unique_toolkit/_common/tests/test_referencing.py +521 -0
  24. unique_toolkit/_common/tests/test_string_utilities.py +506 -0
  25. unique_toolkit/_common/token/image_token_counting.py +67 -0
  26. unique_toolkit/_common/token/token_counting.py +204 -0
  27. unique_toolkit/_common/utils/__init__.py +1 -0
  28. unique_toolkit/_common/utils/files.py +43 -0
  29. unique_toolkit/_common/utils/structured_output/__init__.py +1 -0
  30. unique_toolkit/_common/utils/structured_output/schema.py +5 -0
  31. unique_toolkit/_common/utils/write_configuration.py +51 -0
  32. unique_toolkit/_common/validators.py +101 -4
  33. unique_toolkit/agentic/__init__.py +1 -0
  34. unique_toolkit/agentic/debug_info_manager/debug_info_manager.py +28 -0
  35. unique_toolkit/agentic/debug_info_manager/test/test_debug_info_manager.py +278 -0
  36. unique_toolkit/agentic/evaluation/config.py +36 -0
  37. unique_toolkit/{evaluators → agentic/evaluation}/context_relevancy/prompts.py +25 -0
  38. unique_toolkit/agentic/evaluation/context_relevancy/schema.py +80 -0
  39. unique_toolkit/agentic/evaluation/context_relevancy/service.py +273 -0
  40. unique_toolkit/agentic/evaluation/evaluation_manager.py +218 -0
  41. unique_toolkit/agentic/evaluation/hallucination/constants.py +61 -0
  42. unique_toolkit/agentic/evaluation/hallucination/hallucination_evaluation.py +111 -0
  43. unique_toolkit/{evaluators → agentic/evaluation}/hallucination/prompts.py +1 -1
  44. unique_toolkit/{evaluators → agentic/evaluation}/hallucination/service.py +16 -15
  45. unique_toolkit/{evaluators → agentic/evaluation}/hallucination/utils.py +30 -20
  46. unique_toolkit/{evaluators → agentic/evaluation}/output_parser.py +20 -2
  47. unique_toolkit/{evaluators → agentic/evaluation}/schemas.py +27 -7
  48. unique_toolkit/agentic/evaluation/tests/test_context_relevancy_service.py +253 -0
  49. unique_toolkit/agentic/evaluation/tests/test_output_parser.py +87 -0
  50. unique_toolkit/agentic/history_manager/history_construction_with_contents.py +297 -0
  51. unique_toolkit/agentic/history_manager/history_manager.py +242 -0
  52. unique_toolkit/agentic/history_manager/loop_token_reducer.py +484 -0
  53. unique_toolkit/agentic/history_manager/utils.py +96 -0
  54. unique_toolkit/agentic/postprocessor/postprocessor_manager.py +212 -0
  55. unique_toolkit/agentic/reference_manager/reference_manager.py +103 -0
  56. unique_toolkit/agentic/responses_api/__init__.py +19 -0
  57. unique_toolkit/agentic/responses_api/postprocessors/code_display.py +63 -0
  58. unique_toolkit/agentic/responses_api/postprocessors/generated_files.py +145 -0
  59. unique_toolkit/agentic/responses_api/stream_handler.py +15 -0
  60. unique_toolkit/agentic/short_term_memory_manager/persistent_short_term_memory_manager.py +141 -0
  61. unique_toolkit/agentic/thinking_manager/thinking_manager.py +103 -0
  62. unique_toolkit/agentic/tools/__init__.py +1 -0
  63. unique_toolkit/agentic/tools/a2a/__init__.py +36 -0
  64. unique_toolkit/agentic/tools/a2a/config.py +17 -0
  65. unique_toolkit/agentic/tools/a2a/evaluation/__init__.py +15 -0
  66. unique_toolkit/agentic/tools/a2a/evaluation/_utils.py +66 -0
  67. unique_toolkit/agentic/tools/a2a/evaluation/config.py +55 -0
  68. unique_toolkit/agentic/tools/a2a/evaluation/evaluator.py +260 -0
  69. unique_toolkit/agentic/tools/a2a/evaluation/summarization_user_message.j2 +9 -0
  70. unique_toolkit/agentic/tools/a2a/manager.py +55 -0
  71. unique_toolkit/agentic/tools/a2a/postprocessing/__init__.py +21 -0
  72. unique_toolkit/agentic/tools/a2a/postprocessing/_display_utils.py +185 -0
  73. unique_toolkit/agentic/tools/a2a/postprocessing/_ref_utils.py +73 -0
  74. unique_toolkit/agentic/tools/a2a/postprocessing/config.py +45 -0
  75. unique_toolkit/agentic/tools/a2a/postprocessing/display.py +180 -0
  76. unique_toolkit/agentic/tools/a2a/postprocessing/references.py +101 -0
  77. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display_utils.py +1335 -0
  78. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_ref_utils.py +603 -0
  79. unique_toolkit/agentic/tools/a2a/prompts.py +46 -0
  80. unique_toolkit/agentic/tools/a2a/response_watcher/__init__.py +6 -0
  81. unique_toolkit/agentic/tools/a2a/response_watcher/service.py +91 -0
  82. unique_toolkit/agentic/tools/a2a/tool/__init__.py +4 -0
  83. unique_toolkit/agentic/tools/a2a/tool/_memory.py +26 -0
  84. unique_toolkit/agentic/tools/a2a/tool/_schema.py +9 -0
  85. unique_toolkit/agentic/tools/a2a/tool/config.py +73 -0
  86. unique_toolkit/agentic/tools/a2a/tool/service.py +306 -0
  87. unique_toolkit/agentic/tools/agent_chunks_hanlder.py +65 -0
  88. unique_toolkit/agentic/tools/config.py +167 -0
  89. unique_toolkit/agentic/tools/factory.py +44 -0
  90. unique_toolkit/agentic/tools/mcp/__init__.py +4 -0
  91. unique_toolkit/agentic/tools/mcp/manager.py +71 -0
  92. unique_toolkit/agentic/tools/mcp/models.py +28 -0
  93. unique_toolkit/agentic/tools/mcp/tool_wrapper.py +234 -0
  94. unique_toolkit/agentic/tools/openai_builtin/__init__.py +11 -0
  95. unique_toolkit/agentic/tools/openai_builtin/base.py +30 -0
  96. unique_toolkit/agentic/tools/openai_builtin/code_interpreter/__init__.py +8 -0
  97. unique_toolkit/agentic/tools/openai_builtin/code_interpreter/config.py +57 -0
  98. unique_toolkit/agentic/tools/openai_builtin/code_interpreter/service.py +230 -0
  99. unique_toolkit/agentic/tools/openai_builtin/manager.py +62 -0
  100. unique_toolkit/agentic/tools/schemas.py +141 -0
  101. unique_toolkit/agentic/tools/test/test_mcp_manager.py +536 -0
  102. unique_toolkit/agentic/tools/test/test_tool_progress_reporter.py +445 -0
  103. unique_toolkit/agentic/tools/tool.py +183 -0
  104. unique_toolkit/agentic/tools/tool_manager.py +523 -0
  105. unique_toolkit/agentic/tools/tool_progress_reporter.py +285 -0
  106. unique_toolkit/agentic/tools/utils/__init__.py +19 -0
  107. unique_toolkit/agentic/tools/utils/execution/__init__.py +1 -0
  108. unique_toolkit/agentic/tools/utils/execution/execution.py +286 -0
  109. unique_toolkit/agentic/tools/utils/source_handling/__init__.py +0 -0
  110. unique_toolkit/agentic/tools/utils/source_handling/schema.py +21 -0
  111. unique_toolkit/agentic/tools/utils/source_handling/source_formatting.py +207 -0
  112. unique_toolkit/agentic/tools/utils/source_handling/tests/test_source_formatting.py +216 -0
  113. unique_toolkit/app/__init__.py +6 -0
  114. unique_toolkit/app/dev_util.py +180 -0
  115. unique_toolkit/app/init_sdk.py +32 -1
  116. unique_toolkit/app/schemas.py +198 -31
  117. unique_toolkit/app/unique_settings.py +367 -0
  118. unique_toolkit/chat/__init__.py +8 -1
  119. unique_toolkit/chat/deprecated/service.py +232 -0
  120. unique_toolkit/chat/functions.py +642 -77
  121. unique_toolkit/chat/rendering.py +34 -0
  122. unique_toolkit/chat/responses_api.py +461 -0
  123. unique_toolkit/chat/schemas.py +133 -2
  124. unique_toolkit/chat/service.py +115 -767
  125. unique_toolkit/content/functions.py +153 -4
  126. unique_toolkit/content/schemas.py +122 -15
  127. unique_toolkit/content/service.py +278 -44
  128. unique_toolkit/content/smart_rules.py +301 -0
  129. unique_toolkit/content/utils.py +8 -3
  130. unique_toolkit/embedding/service.py +102 -11
  131. unique_toolkit/framework_utilities/__init__.py +1 -0
  132. unique_toolkit/framework_utilities/langchain/client.py +71 -0
  133. unique_toolkit/framework_utilities/langchain/history.py +19 -0
  134. unique_toolkit/framework_utilities/openai/__init__.py +6 -0
  135. unique_toolkit/framework_utilities/openai/client.py +83 -0
  136. unique_toolkit/framework_utilities/openai/message_builder.py +229 -0
  137. unique_toolkit/framework_utilities/utils.py +23 -0
  138. unique_toolkit/language_model/__init__.py +3 -0
  139. unique_toolkit/language_model/builder.py +27 -11
  140. unique_toolkit/language_model/default_language_model.py +3 -0
  141. unique_toolkit/language_model/functions.py +327 -43
  142. unique_toolkit/language_model/infos.py +992 -50
  143. unique_toolkit/language_model/reference.py +242 -0
  144. unique_toolkit/language_model/schemas.py +475 -48
  145. unique_toolkit/language_model/service.py +228 -27
  146. unique_toolkit/protocols/support.py +145 -0
  147. unique_toolkit/services/__init__.py +7 -0
  148. unique_toolkit/services/chat_service.py +1630 -0
  149. unique_toolkit/services/knowledge_base.py +861 -0
  150. unique_toolkit/short_term_memory/service.py +178 -41
  151. unique_toolkit/smart_rules/__init__.py +0 -0
  152. unique_toolkit/smart_rules/compile.py +56 -0
  153. unique_toolkit/test_utilities/events.py +197 -0
  154. {unique_toolkit-0.7.7.dist-info → unique_toolkit-1.23.0.dist-info}/METADATA +606 -7
  155. unique_toolkit-1.23.0.dist-info/RECORD +182 -0
  156. unique_toolkit/evaluators/__init__.py +0 -1
  157. unique_toolkit/evaluators/config.py +0 -35
  158. unique_toolkit/evaluators/constants.py +0 -1
  159. unique_toolkit/evaluators/context_relevancy/constants.py +0 -32
  160. unique_toolkit/evaluators/context_relevancy/service.py +0 -53
  161. unique_toolkit/evaluators/context_relevancy/utils.py +0 -142
  162. unique_toolkit/evaluators/hallucination/constants.py +0 -41
  163. unique_toolkit-0.7.7.dist-info/RECORD +0 -64
  164. /unique_toolkit/{evaluators → agentic/evaluation}/exception.py +0 -0
  165. {unique_toolkit-0.7.7.dist-info → unique_toolkit-1.23.0.dist-info}/LICENSE +0 -0
  166. {unique_toolkit-0.7.7.dist-info → unique_toolkit-1.23.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,1335 @@
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
+ _add_line_break,
9
+ _get_display_removal_re,
10
+ _get_display_template,
11
+ _join_text_blocks,
12
+ _wrap_hidden_div,
13
+ _wrap_strong,
14
+ _wrap_text,
15
+ _wrap_with_block_border,
16
+ _wrap_with_details_tag,
17
+ _wrap_with_quote_border,
18
+ get_sub_agent_answer_display,
19
+ remove_sub_agent_answer_from_text,
20
+ )
21
+ from unique_toolkit.agentic.tools.a2a.postprocessing.config import (
22
+ SubAgentDisplayConfig,
23
+ SubAgentResponseDisplayMode,
24
+ )
25
+
26
+ # Test _wrap_with_html_block
27
+
28
+
29
+ @pytest.mark.ai
30
+ def test_wrap_with_html_block__wraps_text__with_start_and_end_tags() -> None:
31
+ """
32
+ Purpose: Verify text is wrapped with opening and closing tags with proper newlines.
33
+ Why this matters: Foundation for all HTML wrapping operations.
34
+ Setup summary: Provide text and tags, assert formatted output.
35
+ """
36
+ # Arrange
37
+ text = "Hello World"
38
+ start_tag = "<div>"
39
+ end_tag = "</div>"
40
+
41
+ # Act
42
+ result = _wrap_text(text, start_tag, end_tag)
43
+
44
+ # Assert
45
+ assert result == "<div>\nHello World\n</div>"
46
+
47
+
48
+ @pytest.mark.ai
49
+ def test_wrap_with_html_block__strips_whitespace__from_text_and_tags() -> None:
50
+ """
51
+ Purpose: Ensure whitespace is trimmed from text and tags before wrapping.
52
+ Why this matters: Prevents inconsistent HTML formatting.
53
+ Setup summary: Provide text with whitespace, assert trimmed output.
54
+ """
55
+ # Arrange
56
+ text = " Hello World "
57
+ start_tag = " <div> "
58
+ end_tag = " </div> "
59
+
60
+ # Act
61
+ result = _wrap_text(text, start_tag, end_tag)
62
+
63
+ # Assert
64
+ assert result == "<div>\nHello World\n</div>"
65
+
66
+
67
+ @pytest.mark.ai
68
+ def test_wrap_with_html_block__handles_empty_tags__no_newlines() -> None:
69
+ """
70
+ Purpose: Verify empty tags don't add newlines to output.
71
+ Why this matters: Allows flexible HTML composition without unwanted whitespace.
72
+ Setup summary: Provide empty tags, assert text without extra newlines.
73
+ """
74
+ # Arrange
75
+ text = "Hello World"
76
+ start_tag = ""
77
+ end_tag = ""
78
+
79
+ # Act
80
+ result = _wrap_text(text, start_tag, end_tag)
81
+
82
+ # Assert
83
+ assert result == "Hello World"
84
+
85
+
86
+ @pytest.mark.ai
87
+ def test_wrap_with_html_block__handles_mixed_empty_tags__partial_newlines() -> None:
88
+ """
89
+ Purpose: Verify behavior with one empty tag and one non-empty tag.
90
+ Why this matters: Ensures consistent formatting in edge cases.
91
+ Setup summary: Provide start tag only, assert newline only after start.
92
+ """
93
+ # Arrange
94
+ text = "Hello World"
95
+ start_tag = "<div>"
96
+ end_tag = ""
97
+
98
+ # Act
99
+ result = _wrap_text(text, start_tag, end_tag)
100
+
101
+ # Assert
102
+ assert result == "<div>\nHello World"
103
+
104
+
105
+ # Test _join_html_blocks
106
+
107
+
108
+ @pytest.mark.ai
109
+ def test_join_html_blocks__joins_multiple_blocks__with_newlines() -> None:
110
+ """
111
+ Purpose: Verify multiple HTML blocks are joined with newline separators.
112
+ Why this matters: Creates properly formatted multi-line HTML output.
113
+ Setup summary: Provide multiple blocks, assert newline-joined output.
114
+ """
115
+ # Arrange
116
+ block1 = "<div>Block 1</div>"
117
+ block2 = "<div>Block 2</div>"
118
+ block3 = "<div>Block 3</div>"
119
+
120
+ # Act
121
+ result = _join_text_blocks(block1, block2, block3)
122
+
123
+ # Assert
124
+ assert result == "<div>Block 1</div>\n<div>Block 2</div>\n<div>Block 3</div>"
125
+
126
+
127
+ @pytest.mark.ai
128
+ def test_join_html_blocks__strips_whitespace__from_each_block() -> None:
129
+ """
130
+ Purpose: Ensure whitespace is trimmed from each block before joining.
131
+ Why this matters: Prevents unwanted whitespace in combined HTML.
132
+ Setup summary: Provide blocks with whitespace, assert trimmed joined output.
133
+ """
134
+ # Arrange
135
+ block1 = " <div>Block 1</div> "
136
+ block2 = " <div>Block 2</div> "
137
+
138
+ # Act
139
+ result = _join_text_blocks(block1, block2)
140
+
141
+ # Assert
142
+ assert result == "<div>Block 1</div>\n<div>Block 2</div>"
143
+
144
+
145
+ @pytest.mark.ai
146
+ def test_join_html_blocks__handles_single_block__no_extra_newlines() -> None:
147
+ """
148
+ Purpose: Verify single block is returned without modification.
149
+ Why this matters: Edge case handling for variable block counts.
150
+ Setup summary: Provide single block, assert unchanged output.
151
+ """
152
+ # Arrange
153
+ block = "<div>Single Block</div>"
154
+
155
+ # Act
156
+ result = _join_text_blocks(block)
157
+
158
+ # Assert
159
+ assert result == "<div>Single Block</div>"
160
+
161
+
162
+ # Test _wrap_with_details_tag
163
+
164
+
165
+ @pytest.mark.ai
166
+ def test_wrap_with_details_tag__wraps_open__without_summary() -> None:
167
+ """
168
+ Purpose: Verify open details tag wrapping without summary element.
169
+ Why this matters: Creates collapsible HTML sections in open state.
170
+ Setup summary: Provide text and open mode, assert details open tag.
171
+ """
172
+ # Arrange
173
+ text = "Content here"
174
+
175
+ # Act
176
+ result = _wrap_with_details_tag(text, mode="open", summary_name=None)
177
+
178
+ # Assert
179
+ assert result == "<details open>\nContent here\n</details>"
180
+
181
+
182
+ @pytest.mark.ai
183
+ def test_wrap_with_details_tag__wraps_closed__without_summary() -> None:
184
+ """
185
+ Purpose: Verify closed details tag wrapping without summary element.
186
+ Why this matters: Creates collapsible HTML sections in closed state.
187
+ Setup summary: Provide text and closed mode, assert details tag.
188
+ """
189
+ # Arrange
190
+ text = "Content here"
191
+
192
+ # Act
193
+ result = _wrap_with_details_tag(text, mode="closed", summary_name=None)
194
+
195
+ # Assert
196
+ assert result == "<details>\nContent here\n</details>"
197
+
198
+
199
+ @pytest.mark.ai
200
+ def test_wrap_with_details_tag__includes_summary__when_provided() -> None:
201
+ """
202
+ Purpose: Verify summary element is added when summary_name provided.
203
+ Why this matters: Creates labeled collapsible sections.
204
+ Setup summary: Provide summary_name, assert summary tag before content.
205
+ """
206
+ # Arrange
207
+ text = "Content here"
208
+ summary_name = "Click to expand"
209
+
210
+ # Act
211
+ result = _wrap_with_details_tag(text, mode="closed", summary_name=summary_name)
212
+
213
+ # Assert
214
+ expected = (
215
+ "<details>\n<summary>\nClick to expand\n</summary>\nContent here\n</details>"
216
+ )
217
+ assert result == expected
218
+
219
+
220
+ # Test border and style wrappers
221
+
222
+
223
+ @pytest.mark.ai
224
+ def test_wrap_with_block_border__adds_styled_div__with_border() -> None:
225
+ """
226
+ Purpose: Verify block border wrapper adds div with border styling.
227
+ Why this matters: Visual separation of content blocks.
228
+ Setup summary: Provide text, assert div with border style.
229
+ """
230
+ # Arrange
231
+ text = "Bordered content"
232
+
233
+ # Act
234
+ result = _wrap_with_block_border(text)
235
+
236
+ # Assert
237
+ assert result.startswith("<div style='overflow-y: auto; border: 1px solid #ccc;")
238
+ assert "Bordered content" in result
239
+ assert result.endswith("\n</div>")
240
+
241
+
242
+ @pytest.mark.ai
243
+ def test_wrap_with_quote_border__adds_styled_div__with_left_border() -> None:
244
+ """
245
+ Purpose: Verify quote border wrapper adds div with left border styling.
246
+ Why this matters: Visual indication of quoted content.
247
+ Setup summary: Provide text, assert div with left border style.
248
+ """
249
+ # Arrange
250
+ text = "Quoted content"
251
+
252
+ # Act
253
+ result = _wrap_with_quote_border(text)
254
+
255
+ # Assert
256
+ assert result.startswith(
257
+ "<div style='margin-left: 20px; border-left: 2px solid #ccc;"
258
+ )
259
+ assert "Quoted content" in result
260
+ assert result.endswith("\n</div>")
261
+
262
+
263
+ @pytest.mark.ai
264
+ def test_wrap_strong__wraps_text__with_strong_tags() -> None:
265
+ """
266
+ Purpose: Verify text is wrapped with strong tags for bold formatting.
267
+ Why this matters: Text emphasis in HTML output.
268
+ Setup summary: Provide text, assert strong tag wrapping.
269
+ """
270
+ # Arrange
271
+ text = "Bold text"
272
+
273
+ # Act
274
+ result = _wrap_strong(text)
275
+
276
+ # Assert
277
+ assert result == "<strong>\nBold text\n</strong>"
278
+
279
+
280
+ @pytest.mark.ai
281
+ def test_wrap_hidden_div__wraps_text__with_display_none() -> None:
282
+ """
283
+ Purpose: Verify text is wrapped in hidden div with display:none style.
284
+ Why this matters: Hides content from visual display while keeping it in DOM.
285
+ Setup summary: Provide text, assert div with display:none.
286
+ """
287
+ # Arrange
288
+ text = "Hidden content"
289
+
290
+ # Act
291
+ result = _wrap_hidden_div(text)
292
+
293
+ # Assert
294
+ assert result == '<div style="display: none;">\nHidden content\n</div>'
295
+
296
+
297
+ # Test _add_line_break
298
+
299
+
300
+ @pytest.mark.ai
301
+ def test_add_line_break__adds_both__by_default() -> None:
302
+ """
303
+ Purpose: Verify line breaks are added before and after text by default.
304
+ Why this matters: Default spacing behavior for content.
305
+ Setup summary: Provide text with defaults, assert br tags both sides.
306
+ """
307
+ # Arrange
308
+ text = "Text content"
309
+
310
+ # Act
311
+ result = _add_line_break(text)
312
+
313
+ # Assert
314
+ assert result == "<br>\nText content\n<br>"
315
+
316
+
317
+ @pytest.mark.ai
318
+ def test_add_line_break__adds_only_before__when_after_false() -> None:
319
+ """
320
+ Purpose: Verify line break only before text when after=False.
321
+ Why this matters: Flexible spacing control.
322
+ Setup summary: Set after=False, assert br only before.
323
+ """
324
+ # Arrange
325
+ text = "Text content"
326
+
327
+ # Act
328
+ result = _add_line_break(text, before=True, after=False)
329
+
330
+ # Assert
331
+ assert result == "<br>\nText content"
332
+
333
+
334
+ @pytest.mark.ai
335
+ def test_add_line_break__adds_only_after__when_before_false() -> None:
336
+ """
337
+ Purpose: Verify line break only after text when before=False.
338
+ Why this matters: Flexible spacing control.
339
+ Setup summary: Set before=False, assert br only after.
340
+ """
341
+ # Arrange
342
+ text = "Text content"
343
+
344
+ # Act
345
+ result = _add_line_break(text, before=False, after=True)
346
+
347
+ # Assert
348
+ assert result == "Text content\n<br>"
349
+
350
+
351
+ @pytest.mark.ai
352
+ def test_add_line_break__adds_none__when_both_false() -> None:
353
+ """
354
+ Purpose: Verify no line breaks added when both flags false.
355
+ Why this matters: Complete control over spacing.
356
+ Setup summary: Set both flags false, assert no br tags.
357
+ """
358
+ # Arrange
359
+ text = "Text content"
360
+
361
+ # Act
362
+ result = _add_line_break(text, before=False, after=False)
363
+
364
+ # Assert
365
+ assert result == "Text content"
366
+
367
+
368
+ # Test _get_display_template
369
+
370
+
371
+ @pytest.mark.ai
372
+ def test_get_display_template__returns_empty__when_hidden_mode() -> None:
373
+ """
374
+ Purpose: Verify empty string returned for HIDDEN display mode.
375
+ Why this matters: Content should not be displayed when hidden.
376
+ Setup summary: Set mode to HIDDEN, assert empty string.
377
+ """
378
+ # Arrange
379
+ mode = SubAgentResponseDisplayMode.HIDDEN
380
+
381
+ # Act
382
+ result = _get_display_template(
383
+ mode=mode,
384
+ add_quote_border=False,
385
+ add_block_border=False,
386
+ display_title_template="Answer from <strong>{}</strong>",
387
+ )
388
+
389
+ # Assert
390
+ assert result == ""
391
+
392
+
393
+ @pytest.mark.ai
394
+ def test_get_display_template__includes_placeholders__for_all_modes() -> None:
395
+ """
396
+ Purpose: Verify all required placeholders present in non-hidden modes.
397
+ Why this matters: Template must support variable substitution.
398
+ Setup summary: Test each display mode, assert placeholders exist.
399
+ """
400
+ # Arrange
401
+ modes = [
402
+ SubAgentResponseDisplayMode.PLAIN,
403
+ SubAgentResponseDisplayMode.DETAILS_OPEN,
404
+ SubAgentResponseDisplayMode.DETAILS_CLOSED,
405
+ ]
406
+
407
+ for mode in modes:
408
+ # Act
409
+ result = _get_display_template(
410
+ mode=mode,
411
+ add_quote_border=False,
412
+ add_block_border=False,
413
+ display_title_template="Answer from <strong>{}</strong>",
414
+ )
415
+
416
+ # Assert
417
+ assert "{assistant_id}" in result, f"assistant_id missing in {mode}"
418
+ assert "{answer}" in result, f"answer missing in {mode}"
419
+ assert "{display_name}" in result, f"display_name missing in {mode}"
420
+
421
+
422
+ @pytest.mark.ai
423
+ def test_get_display_template__wraps_assistant_id__as_hidden_div() -> None:
424
+ """
425
+ Purpose: Verify assistant_id is always wrapped in hidden div.
426
+ Why this matters: Assistant ID should not be visible to users.
427
+ Setup summary: Check template contains hidden div with assistant_id.
428
+ """
429
+ # Arrange
430
+ mode = SubAgentResponseDisplayMode.PLAIN
431
+
432
+ # Act
433
+ result = _get_display_template(
434
+ mode=mode,
435
+ add_quote_border=False,
436
+ add_block_border=False,
437
+ display_title_template="Answer from <strong>{}</strong>",
438
+ )
439
+
440
+ # Assert
441
+ assert '<div style="display: none;">' in result
442
+ assert "{assistant_id}" in result
443
+
444
+
445
+ @pytest.mark.ai
446
+ def test_get_display_template__wraps_display_name__as_strong() -> None:
447
+ """
448
+ Purpose: Verify display_name is wrapped in strong tags for emphasis.
449
+ Why this matters: Display name should be bold for visibility.
450
+ Setup summary: Check template contains strong tags with display_name.
451
+ """
452
+ # Arrange
453
+ mode = SubAgentResponseDisplayMode.PLAIN
454
+
455
+ # Act
456
+ result = _get_display_template(
457
+ mode=mode,
458
+ add_quote_border=False,
459
+ add_block_border=False,
460
+ display_title_template="Answer from <strong>{}</strong>",
461
+ )
462
+
463
+ # Assert
464
+ assert "<strong>" in result
465
+ assert "{display_name}" in result
466
+ assert "</strong>" in result
467
+
468
+
469
+ @pytest.mark.ai
470
+ def test_get_display_template__adds_details_open__when_details_open_mode() -> None:
471
+ """
472
+ Purpose: Verify details open tags present in DETAILS_OPEN mode.
473
+ Why this matters: Creates expandable section in open state.
474
+ Setup summary: Set DETAILS_OPEN mode, assert details open tags.
475
+ """
476
+ # Arrange
477
+ mode = SubAgentResponseDisplayMode.DETAILS_OPEN
478
+
479
+ # Act
480
+ result = _get_display_template(
481
+ mode=mode,
482
+ add_quote_border=False,
483
+ add_block_border=False,
484
+ display_title_template="Answer from <strong>{}</strong>",
485
+ )
486
+
487
+ # Assert
488
+ assert "<details open>" in result
489
+ assert "</details>" in result
490
+ assert "<summary>" in result
491
+ assert "</summary>" in result
492
+
493
+
494
+ @pytest.mark.ai
495
+ def test_get_display_template__adds_details_closed__when_details_closed_mode() -> None:
496
+ """
497
+ Purpose: Verify details tags present without open in DETAILS_CLOSED mode.
498
+ Why this matters: Creates expandable section in closed state.
499
+ Setup summary: Set DETAILS_CLOSED mode, assert details tags without open.
500
+ """
501
+ # Arrange
502
+ mode = SubAgentResponseDisplayMode.DETAILS_CLOSED
503
+
504
+ # Act
505
+ result = _get_display_template(
506
+ mode=mode,
507
+ add_quote_border=False,
508
+ add_block_border=False,
509
+ display_title_template="Answer from <strong>{}</strong>",
510
+ )
511
+
512
+ # Assert
513
+ assert "<details>" in result
514
+ assert "<details open>" not in result
515
+ assert "</details>" in result
516
+ assert "<summary>" in result
517
+ assert "</summary>" in result
518
+
519
+
520
+ @pytest.mark.ai
521
+ def test_get_display_template__adds_line_break_after_name__in_plain_mode() -> None:
522
+ """
523
+ Purpose: Verify line break added after display name in PLAIN mode.
524
+ Why this matters: Separates display name from content visually.
525
+ Setup summary: Set PLAIN mode, assert br tag after display_name.
526
+ """
527
+ # Arrange
528
+ mode = SubAgentResponseDisplayMode.PLAIN
529
+
530
+ # Act
531
+ result = _get_display_template(
532
+ mode=mode,
533
+ add_quote_border=False,
534
+ add_block_border=False,
535
+ display_title_template="Answer from <strong>{}</strong>",
536
+ )
537
+
538
+ # Assert
539
+ # The display_name should be wrapped with line break (before=False, after=True)
540
+ assert "<br>" in result
541
+
542
+
543
+ @pytest.mark.ai
544
+ def test_get_display_template__adds_quote_border__when_flag_true() -> None:
545
+ """
546
+ Purpose: Verify quote border styling added when add_quote_border=True.
547
+ Why this matters: Visual indication of quoted content.
548
+ Setup summary: Set add_quote_border=True, assert quote border style.
549
+ """
550
+ # Arrange
551
+ mode = SubAgentResponseDisplayMode.PLAIN
552
+
553
+ # Act
554
+ result = _get_display_template(
555
+ mode=mode,
556
+ add_quote_border=True,
557
+ add_block_border=False,
558
+ display_title_template="Answer from <strong>{}</strong>",
559
+ )
560
+
561
+ # Assert
562
+ assert "margin-left: 20px" in result
563
+ assert "border-left: 2px solid #ccc" in result
564
+
565
+
566
+ @pytest.mark.ai
567
+ def test_get_display_template__adds_block_border__when_flag_true() -> None:
568
+ """
569
+ Purpose: Verify block border styling added when add_block_border=True.
570
+ Why this matters: Visual separation of content blocks.
571
+ Setup summary: Set add_block_border=True, assert block border style.
572
+ """
573
+ # Arrange
574
+ mode = SubAgentResponseDisplayMode.PLAIN
575
+
576
+ # Act
577
+ result = _get_display_template(
578
+ mode=mode,
579
+ add_quote_border=False,
580
+ add_block_border=True,
581
+ display_title_template="Answer from <strong>{}</strong>",
582
+ )
583
+
584
+ # Assert
585
+ assert "overflow-y: auto" in result
586
+ assert "border: 1px solid #ccc" in result
587
+
588
+
589
+ @pytest.mark.ai
590
+ def test_get_display_template__adds_both_borders__when_both_flags_true() -> None:
591
+ """
592
+ Purpose: Verify both border styles added when both flags true.
593
+ Why this matters: Support for combining visual styles.
594
+ Setup summary: Set both border flags true, assert both styles present.
595
+ """
596
+ # Arrange
597
+ mode = SubAgentResponseDisplayMode.PLAIN
598
+
599
+ # Act
600
+ result = _get_display_template(
601
+ mode=mode,
602
+ add_quote_border=True,
603
+ add_block_border=True,
604
+ display_title_template="Answer from <strong>{}</strong>",
605
+ )
606
+
607
+ # Assert
608
+ # Quote border should be inside block border
609
+ assert "overflow-y: auto" in result
610
+ assert "border: 1px solid #ccc" in result
611
+ assert "margin-left: 20px" in result
612
+ assert "border-left: 2px solid #ccc" in result
613
+
614
+
615
+ # Test _get_display_removal_re (regex pattern generation)
616
+
617
+
618
+ @pytest.mark.ai
619
+ def test_get_display_removal_re__returns_pattern__for_plain_mode() -> None:
620
+ """
621
+ Purpose: Verify regex pattern is created for PLAIN display mode.
622
+ Why this matters: Enables removal of displayed content from text.
623
+ Setup summary: Generate pattern for PLAIN mode, assert Pattern type.
624
+ """
625
+ # Arrange
626
+ assistant_id = "test-assistant-123"
627
+ mode = SubAgentResponseDisplayMode.PLAIN
628
+
629
+ # Act
630
+ result = _get_display_removal_re(
631
+ assistant_id=assistant_id,
632
+ mode=mode,
633
+ add_quote_border=False,
634
+ add_block_border=False,
635
+ display_title_template="Answer from <strong>{}</strong>",
636
+ )
637
+
638
+ # Assert
639
+ assert isinstance(result, re.Pattern)
640
+ assert result.flags & re.DOTALL # Should have DOTALL flag
641
+
642
+
643
+ @pytest.mark.ai
644
+ def test_get_display_removal_re__returns_pattern__for_details_modes() -> None:
645
+ """
646
+ Purpose: Verify regex patterns created for both DETAILS modes.
647
+ Why this matters: Ensures removal works for collapsible sections.
648
+ Setup summary: Generate patterns for DETAILS modes, assert Pattern types.
649
+ """
650
+ # Arrange
651
+ assistant_id = "test-assistant-123"
652
+ modes = [
653
+ SubAgentResponseDisplayMode.DETAILS_OPEN,
654
+ SubAgentResponseDisplayMode.DETAILS_CLOSED,
655
+ ]
656
+
657
+ for mode in modes:
658
+ # Act
659
+ result = _get_display_removal_re(
660
+ assistant_id=assistant_id,
661
+ mode=mode,
662
+ add_quote_border=False,
663
+ add_block_border=False,
664
+ display_title_template="Answer from <strong>{}</strong>",
665
+ )
666
+
667
+ # Assert
668
+ assert isinstance(result, re.Pattern), f"Pattern not created for {mode}"
669
+ assert result.flags & re.DOTALL, f"DOTALL flag missing for {mode}"
670
+
671
+
672
+ @pytest.mark.ai
673
+ def test_get_display_removal_re__includes_assistant_id__in_pattern() -> None:
674
+ """
675
+ Purpose: Verify assistant_id is embedded in regex pattern.
676
+ Why this matters: Ensures only specific assistant's content is removed.
677
+ Setup summary: Generate pattern with assistant_id, assert ID in pattern.
678
+ """
679
+ # Arrange
680
+ assistant_id = "unique-assistant-xyz"
681
+ mode = SubAgentResponseDisplayMode.PLAIN
682
+
683
+ # Act
684
+ result = _get_display_removal_re(
685
+ assistant_id=assistant_id,
686
+ mode=mode,
687
+ add_quote_border=False,
688
+ add_block_border=False,
689
+ display_title_template="Answer from <strong>{}</strong>",
690
+ )
691
+
692
+ # Assert
693
+ assert re.escape(assistant_id) in result.pattern
694
+
695
+
696
+ @pytest.mark.ai
697
+ def test_get_display_removal_re__has_capture_groups__for_answer_and_name() -> None:
698
+ """
699
+ Purpose: Verify regex pattern includes capture groups for dynamic content.
700
+ Why this matters: Allows flexible matching of variable content.
701
+ Setup summary: Check pattern contains regex capture groups.
702
+ """
703
+ # Arrange
704
+ assistant_id = "test-assistant"
705
+ mode = SubAgentResponseDisplayMode.PLAIN
706
+
707
+ # Act
708
+ result = _get_display_removal_re(
709
+ assistant_id=assistant_id,
710
+ mode=mode,
711
+ add_quote_border=False,
712
+ add_block_border=False,
713
+ display_title_template="Answer from <strong>{}</strong>",
714
+ )
715
+
716
+ # Assert
717
+ # Pattern should contain (.*?) for capturing groups
718
+ assert "(.*?)" in result.pattern
719
+
720
+
721
+ # Test _build_sub_agent_answer_display
722
+
723
+
724
+ @pytest.mark.ai
725
+ def test_build_sub_agent_answer_display__creates_html__for_plain_mode() -> None:
726
+ """
727
+ Purpose: Verify HTML output is generated for PLAIN display mode.
728
+ Why this matters: Core functionality for displaying agent responses.
729
+ Setup summary: Build display with PLAIN mode, assert HTML structure.
730
+ """
731
+ # Arrange
732
+ display_name = "Test Agent"
733
+ answer = "This is the answer"
734
+ assistant_id = "agent-123"
735
+ config = SubAgentDisplayConfig(
736
+ mode=SubAgentResponseDisplayMode.PLAIN,
737
+ add_quote_border=False,
738
+ add_block_border=False,
739
+ display_title_template="Answer from <strong>{}</strong>",
740
+ )
741
+
742
+ # Act
743
+ result = get_sub_agent_answer_display(
744
+ display_name=display_name,
745
+ display_config=config,
746
+ answer=answer,
747
+ assistant_id=assistant_id,
748
+ )
749
+
750
+ # Assert
751
+ assert "Test Agent" in result
752
+ assert "This is the answer" in result
753
+ assert "agent-123" in result
754
+ assert '<div style="display: none;">' in result
755
+ assert "<strong>" in result
756
+
757
+
758
+ @pytest.mark.ai
759
+ def test_build_sub_agent_answer_display__creates_details__for_details_open() -> None:
760
+ """
761
+ Purpose: Verify details HTML with open attribute for DETAILS_OPEN mode.
762
+ Why this matters: Creates expandable sections in open state.
763
+ Setup summary: Build display with DETAILS_OPEN, assert details open tags.
764
+ """
765
+ # Arrange
766
+ display_name = "Test Agent"
767
+ answer = "This is the answer"
768
+ assistant_id = "agent-123"
769
+ config = SubAgentDisplayConfig(
770
+ mode=SubAgentResponseDisplayMode.DETAILS_OPEN,
771
+ add_quote_border=False,
772
+ add_block_border=False,
773
+ display_title_template="Answer from <strong>{}</strong>",
774
+ )
775
+
776
+ # Act
777
+ result = get_sub_agent_answer_display(
778
+ display_name=display_name,
779
+ display_config=config,
780
+ answer=answer,
781
+ assistant_id=assistant_id,
782
+ )
783
+
784
+ # Assert
785
+ assert "<details open>" in result
786
+ assert "</details>" in result
787
+ assert "<summary>" in result
788
+ assert "Test Agent" in result
789
+ assert "This is the answer" in result
790
+
791
+
792
+ @pytest.mark.ai
793
+ def test_build_sub_agent_answer_display__creates_details__for_details_closed() -> None:
794
+ """
795
+ Purpose: Verify details HTML without open for DETAILS_CLOSED mode.
796
+ Why this matters: Creates expandable sections in closed state.
797
+ Setup summary: Build display with DETAILS_CLOSED, assert details tags.
798
+ """
799
+ # Arrange
800
+ display_name = "Test Agent"
801
+ answer = "This is the answer"
802
+ assistant_id = "agent-123"
803
+ config = SubAgentDisplayConfig(
804
+ mode=SubAgentResponseDisplayMode.DETAILS_CLOSED,
805
+ add_quote_border=False,
806
+ add_block_border=False,
807
+ display_title_template="Answer from <strong>{}</strong>",
808
+ )
809
+
810
+ # Act
811
+ result = get_sub_agent_answer_display(
812
+ display_name=display_name,
813
+ display_config=config,
814
+ answer=answer,
815
+ assistant_id=assistant_id,
816
+ )
817
+
818
+ # Assert
819
+ assert "<details>" in result
820
+ assert "<details open>" not in result
821
+ assert "</details>" in result
822
+ assert "<summary>" in result
823
+ assert "Test Agent" in result
824
+
825
+
826
+ @pytest.mark.ai
827
+ def test_build_sub_agent_answer_display__returns_empty__for_hidden_mode() -> None:
828
+ """
829
+ Purpose: Verify empty string returned for HIDDEN display mode.
830
+ Why this matters: Hidden content should not generate any HTML.
831
+ Setup summary: Build display with HIDDEN mode, assert empty string.
832
+ """
833
+ # Arrange
834
+ display_name = "Test Agent"
835
+ answer = "This is the answer"
836
+ assistant_id = "agent-123"
837
+ config = SubAgentDisplayConfig(
838
+ mode=SubAgentResponseDisplayMode.HIDDEN,
839
+ add_quote_border=False,
840
+ add_block_border=False,
841
+ display_title_template="Answer from <strong>{}</strong>",
842
+ )
843
+
844
+ # Act
845
+ result = get_sub_agent_answer_display(
846
+ display_name=display_name,
847
+ display_config=config,
848
+ answer=answer,
849
+ assistant_id=assistant_id,
850
+ )
851
+
852
+ # Assert
853
+ assert result == ""
854
+
855
+
856
+ # Test _remove_sub_agent_answer_from_text (regex removal logic)
857
+
858
+
859
+ @pytest.mark.ai
860
+ def test_remove_sub_agent_answer__removes_plain_display__from_text() -> None:
861
+ """
862
+ Purpose: Verify PLAIN mode display content is removed from text via regex.
863
+ Why this matters: Core removal functionality for cleaning history.
864
+ Setup summary: Build display, embed in text, remove via regex, assert removal.
865
+ """
866
+ # Arrange
867
+ assistant_id = "agent-123"
868
+ display_name = "Test Agent"
869
+ answer = "This is the answer"
870
+ config = SubAgentDisplayConfig(
871
+ mode=SubAgentResponseDisplayMode.PLAIN,
872
+ add_quote_border=False,
873
+ add_block_border=False,
874
+ display_title_template="Answer from <strong>{}</strong>",
875
+ )
876
+
877
+ # Build the display
878
+ display = get_sub_agent_answer_display(
879
+ display_name=display_name,
880
+ display_config=config,
881
+ answer=answer,
882
+ assistant_id=assistant_id,
883
+ )
884
+
885
+ text_with_display = f"Before content\n{display}\nAfter content"
886
+
887
+ # Act
888
+ result = remove_sub_agent_answer_from_text(
889
+ display_config=config,
890
+ text=text_with_display,
891
+ assistant_id=assistant_id,
892
+ )
893
+
894
+ # Assert
895
+ assert "This is the answer" not in result
896
+ assert "Test Agent" not in result
897
+ assert "Before content" in result
898
+ assert "After content" in result
899
+
900
+
901
+ @pytest.mark.ai
902
+ def test_remove_sub_agent_answer__removes_details_open__from_text() -> None:
903
+ """
904
+ Purpose: Verify DETAILS_OPEN mode display is removed via regex.
905
+ Why this matters: Ensures removal works for collapsible open sections.
906
+ Setup summary: Build details open display, embed and remove, assert removal.
907
+ """
908
+ # Arrange
909
+ assistant_id = "agent-456"
910
+ display_name = "Research Agent"
911
+ answer = "Research findings here"
912
+ config = SubAgentDisplayConfig(
913
+ mode=SubAgentResponseDisplayMode.DETAILS_OPEN,
914
+ add_quote_border=False,
915
+ add_block_border=False,
916
+ display_title_template="Answer from <strong>{}</strong>",
917
+ )
918
+
919
+ # Build the display
920
+ display = get_sub_agent_answer_display(
921
+ display_name=display_name,
922
+ display_config=config,
923
+ answer=answer,
924
+ assistant_id=assistant_id,
925
+ )
926
+
927
+ text_with_display = f"Start\n{display}\nEnd"
928
+
929
+ # Act
930
+ result = remove_sub_agent_answer_from_text(
931
+ display_config=config,
932
+ text=text_with_display,
933
+ assistant_id=assistant_id,
934
+ )
935
+
936
+ # Assert
937
+ assert "Research findings here" not in result
938
+ assert "Research Agent" not in result
939
+ assert "<details open>" not in result
940
+ assert "Start" in result
941
+ assert "End" in result
942
+
943
+
944
+ @pytest.mark.ai
945
+ def test_remove_sub_agent_answer__removes_details_closed__from_text() -> None:
946
+ """
947
+ Purpose: Verify DETAILS_CLOSED mode display is removed via regex.
948
+ Why this matters: Ensures removal works for collapsible closed sections.
949
+ Setup summary: Build details closed display, embed and remove, assert removal.
950
+ """
951
+ # Arrange
952
+ assistant_id = "agent-789"
953
+ display_name = "Analysis Agent"
954
+ answer = "Analysis results"
955
+ config = SubAgentDisplayConfig(
956
+ mode=SubAgentResponseDisplayMode.DETAILS_CLOSED,
957
+ add_quote_border=False,
958
+ add_block_border=False,
959
+ display_title_template="Answer from <strong>{}</strong>",
960
+ )
961
+
962
+ # Build the display
963
+ display = get_sub_agent_answer_display(
964
+ display_name=display_name,
965
+ display_config=config,
966
+ answer=answer,
967
+ assistant_id=assistant_id,
968
+ )
969
+
970
+ text_with_display = f"Beginning\n{display}\nEnding"
971
+
972
+ # Act
973
+ result = remove_sub_agent_answer_from_text(
974
+ display_config=config,
975
+ text=text_with_display,
976
+ assistant_id=assistant_id,
977
+ )
978
+
979
+ # Assert
980
+ assert "Analysis results" not in result
981
+ assert "Analysis Agent" not in result
982
+ assert "<details>" not in result
983
+ assert "Beginning" in result
984
+ assert "Ending" in result
985
+
986
+
987
+ @pytest.mark.ai
988
+ def test_remove_sub_agent_answer__removes_with_quote_border__from_text() -> None:
989
+ """
990
+ Purpose: Verify removal works when quote border styling is present.
991
+ Why this matters: Regex must handle additional div wrapper.
992
+ Setup summary: Build display with quote border, remove, assert successful removal.
993
+ """
994
+ # Arrange
995
+ assistant_id = "agent-quote"
996
+ display_name = "Quote Agent"
997
+ answer = "Quoted answer"
998
+ config = SubAgentDisplayConfig(
999
+ mode=SubAgentResponseDisplayMode.PLAIN,
1000
+ add_quote_border=True,
1001
+ add_block_border=False,
1002
+ display_title_template="Answer from <strong>{}</strong>",
1003
+ )
1004
+
1005
+ # Build with quote border
1006
+ display = get_sub_agent_answer_display(
1007
+ display_name=display_name,
1008
+ display_config=config,
1009
+ answer=answer,
1010
+ assistant_id=assistant_id,
1011
+ )
1012
+
1013
+ text_with_display = f"Before\n{display}\nAfter"
1014
+
1015
+ # Act
1016
+ result = remove_sub_agent_answer_from_text(
1017
+ display_config=config,
1018
+ text=text_with_display,
1019
+ assistant_id=assistant_id,
1020
+ )
1021
+
1022
+ # Assert
1023
+ assert "Quoted answer" not in result
1024
+ assert "Quote Agent" not in result
1025
+ assert "margin-left: 20px" not in result
1026
+ assert "Before" in result
1027
+ assert "After" in result
1028
+
1029
+
1030
+ @pytest.mark.ai
1031
+ def test_remove_sub_agent_answer__removes_with_block_border__from_text() -> None:
1032
+ """
1033
+ Purpose: Verify removal works when block border styling is present.
1034
+ Why this matters: Regex must handle block border div wrapper.
1035
+ Setup summary: Build display with block border, remove, assert successful removal.
1036
+ """
1037
+ # Arrange
1038
+ assistant_id = "agent-block"
1039
+ display_name = "Block Agent"
1040
+ answer = "Block answer"
1041
+ config = SubAgentDisplayConfig(
1042
+ mode=SubAgentResponseDisplayMode.PLAIN,
1043
+ add_quote_border=False,
1044
+ add_block_border=True,
1045
+ display_title_template="Answer from <strong>{}</strong>",
1046
+ )
1047
+
1048
+ # Build with block border
1049
+ display = get_sub_agent_answer_display(
1050
+ display_name=display_name,
1051
+ display_config=config,
1052
+ answer=answer,
1053
+ assistant_id=assistant_id,
1054
+ )
1055
+
1056
+ text_with_display = f"Start\n{display}\nFinish"
1057
+
1058
+ # Act
1059
+ result = remove_sub_agent_answer_from_text(
1060
+ display_config=config,
1061
+ text=text_with_display,
1062
+ assistant_id=assistant_id,
1063
+ )
1064
+
1065
+ # Assert
1066
+ assert "Block answer" not in result
1067
+ assert "Block Agent" not in result
1068
+ assert "overflow-y: auto" not in result
1069
+ assert "Start" in result
1070
+ assert "Finish" in result
1071
+
1072
+
1073
+ @pytest.mark.ai
1074
+ def test_remove_sub_agent_answer__removes_with_both_borders__from_text() -> None:
1075
+ """
1076
+ Purpose: Verify removal works with both quote and block borders.
1077
+ Why this matters: Regex must handle nested div wrappers.
1078
+ Setup summary: Build display with both borders, remove, assert successful removal.
1079
+ """
1080
+ # Arrange
1081
+ assistant_id = "agent-both"
1082
+ display_name = "Both Borders Agent"
1083
+ answer = "Answer with borders"
1084
+ config = SubAgentDisplayConfig(
1085
+ mode=SubAgentResponseDisplayMode.DETAILS_OPEN,
1086
+ add_quote_border=True,
1087
+ add_block_border=True,
1088
+ display_title_template="Answer from <strong>{}</strong>",
1089
+ )
1090
+
1091
+ # Build with both borders
1092
+ display = get_sub_agent_answer_display(
1093
+ display_name=display_name,
1094
+ display_config=config,
1095
+ answer=answer,
1096
+ assistant_id=assistant_id,
1097
+ )
1098
+
1099
+ text_with_display = f"Prefix\n{display}\nSuffix"
1100
+
1101
+ # Act
1102
+ result = remove_sub_agent_answer_from_text(
1103
+ display_config=config,
1104
+ text=text_with_display,
1105
+ assistant_id=assistant_id,
1106
+ )
1107
+
1108
+ # Assert
1109
+ assert "Answer with borders" not in result
1110
+ assert "Both Borders Agent" not in result
1111
+ assert "<details open>" not in result
1112
+ assert "Prefix" in result
1113
+ assert "Suffix" in result
1114
+
1115
+
1116
+ @pytest.mark.ai
1117
+ def test_remove_sub_agent_answer__preserves_other_content__with_multiple_displays() -> (
1118
+ None
1119
+ ):
1120
+ """
1121
+ Purpose: Verify removal only affects specified assistant_id.
1122
+ Why this matters: Must not remove content from different assistants.
1123
+ Setup summary: Embed multiple assistants, remove one, assert selective removal.
1124
+ """
1125
+ # Arrange
1126
+ assistant_id_1 = "agent-1"
1127
+ assistant_id_2 = "agent-2"
1128
+ config = SubAgentDisplayConfig(
1129
+ mode=SubAgentResponseDisplayMode.PLAIN,
1130
+ add_quote_border=False,
1131
+ add_block_border=False,
1132
+ display_title_template="Answer from <strong>{}</strong>",
1133
+ )
1134
+
1135
+ # Build displays for two different assistants
1136
+ display_1 = get_sub_agent_answer_display(
1137
+ display_name="Agent 1",
1138
+ display_config=config,
1139
+ answer="Answer from agent 1",
1140
+ assistant_id=assistant_id_1,
1141
+ )
1142
+
1143
+ display_2 = get_sub_agent_answer_display(
1144
+ display_name="Agent 2",
1145
+ display_config=config,
1146
+ answer="Answer from agent 2",
1147
+ assistant_id=assistant_id_2,
1148
+ )
1149
+
1150
+ text_with_displays = f"Start\n{display_1}\nMiddle\n{display_2}\nEnd"
1151
+
1152
+ # Act - Remove only agent-1's display
1153
+ result = remove_sub_agent_answer_from_text(
1154
+ display_config=config,
1155
+ text=text_with_displays,
1156
+ assistant_id=assistant_id_1,
1157
+ )
1158
+
1159
+ # Assert
1160
+ assert "Answer from agent 1" not in result # Removed
1161
+ assert "Agent 1" not in result # Removed
1162
+ assert "Answer from agent 2" in result # Preserved
1163
+ assert "Agent 2" in result # Preserved
1164
+ assert "Start" in result
1165
+ assert "Middle" in result
1166
+ assert "End" in result
1167
+
1168
+
1169
+ @pytest.mark.ai
1170
+ def test_remove_sub_agent_answer__handles_multiline_answer__with_dotall_flag() -> None:
1171
+ """
1172
+ Purpose: Verify removal works for multiline answers using DOTALL regex flag.
1173
+ Why this matters: Answers can span multiple lines with newlines.
1174
+ Setup summary: Build display with multiline answer, remove, assert removal.
1175
+ """
1176
+ # Arrange
1177
+ assistant_id = "agent-multiline"
1178
+ display_name = "Multiline Agent"
1179
+ answer = "Line 1\nLine 2\nLine 3\nWith many\nnewlines"
1180
+ config = SubAgentDisplayConfig(
1181
+ mode=SubAgentResponseDisplayMode.PLAIN,
1182
+ add_quote_border=False,
1183
+ add_block_border=False,
1184
+ display_title_template="Answer from <strong>{}</strong>",
1185
+ )
1186
+
1187
+ # Build the display
1188
+ display = get_sub_agent_answer_display(
1189
+ display_name=display_name,
1190
+ display_config=config,
1191
+ answer=answer,
1192
+ assistant_id=assistant_id,
1193
+ )
1194
+
1195
+ text_with_display = f"Before\n{display}\nAfter"
1196
+
1197
+ # Act
1198
+ result = remove_sub_agent_answer_from_text(
1199
+ display_config=config,
1200
+ text=text_with_display,
1201
+ assistant_id=assistant_id,
1202
+ )
1203
+
1204
+ # Assert
1205
+ assert "Line 1" not in result
1206
+ assert "Line 2" not in result
1207
+ assert "Line 3" not in result
1208
+ assert "With many" not in result
1209
+ assert "Multiline Agent" not in result
1210
+ assert "Before" in result
1211
+ assert "After" in result
1212
+
1213
+
1214
+ @pytest.mark.ai
1215
+ def test_remove_sub_agent_answer__handles_special_regex_chars__in_answer() -> None:
1216
+ """
1217
+ Purpose: Verify removal works when answer contains regex special characters.
1218
+ Why this matters: Template uses (.*?) which should match any content safely.
1219
+ Setup summary: Build display with regex chars in answer, remove, assert removal.
1220
+ """
1221
+ # Arrange
1222
+ assistant_id = "agent-special"
1223
+ display_name = "Special Chars"
1224
+ answer = "Answer with $pecial ch@rs: .* + ? [ ] { } ( ) | \\"
1225
+ config = SubAgentDisplayConfig(
1226
+ mode=SubAgentResponseDisplayMode.PLAIN,
1227
+ add_quote_border=False,
1228
+ add_block_border=False,
1229
+ display_title_template="Answer from <strong>{}</strong>",
1230
+ )
1231
+
1232
+ # Build the display
1233
+ display = get_sub_agent_answer_display(
1234
+ display_name=display_name,
1235
+ display_config=config,
1236
+ answer=answer,
1237
+ assistant_id=assistant_id,
1238
+ )
1239
+
1240
+ text_with_display = f"Start\n{display}\nEnd"
1241
+
1242
+ # Act
1243
+ result = remove_sub_agent_answer_from_text(
1244
+ display_config=config,
1245
+ text=text_with_display,
1246
+ assistant_id=assistant_id,
1247
+ )
1248
+
1249
+ # Assert
1250
+ assert "$pecial ch@rs" not in result
1251
+ assert "Special Chars" not in result
1252
+ assert "Start" in result
1253
+ assert "End" in result
1254
+
1255
+
1256
+ @pytest.mark.ai
1257
+ def test_remove_sub_agent_answer__handles_empty_answer__successfully() -> None:
1258
+ """
1259
+ Purpose: Verify removal works when answer is empty string.
1260
+ Why this matters: Edge case handling for empty content.
1261
+ Setup summary: Build display with empty answer, remove, assert removal.
1262
+ """
1263
+ # Arrange
1264
+ assistant_id = "agent-empty"
1265
+ display_name = "Empty Answer Agent"
1266
+ answer = ""
1267
+ config = SubAgentDisplayConfig(
1268
+ mode=SubAgentResponseDisplayMode.PLAIN,
1269
+ add_quote_border=False,
1270
+ add_block_border=False,
1271
+ display_title_template="Answer from <strong>{}</strong>",
1272
+ )
1273
+
1274
+ # Build the display
1275
+ display = get_sub_agent_answer_display(
1276
+ display_name=display_name,
1277
+ display_config=config,
1278
+ answer=answer,
1279
+ assistant_id=assistant_id,
1280
+ )
1281
+
1282
+ text_with_display = f"Beginning\n{display}\nEnding"
1283
+
1284
+ # Act
1285
+ result = remove_sub_agent_answer_from_text(
1286
+ display_config=config,
1287
+ text=text_with_display,
1288
+ assistant_id=assistant_id,
1289
+ )
1290
+
1291
+ # Assert
1292
+ assert "Empty Answer Agent" not in result
1293
+ assert "Beginning" in result
1294
+ assert "Ending" in result
1295
+
1296
+
1297
+ @pytest.mark.ai
1298
+ def test_remove_sub_agent_answer__no_op_when_assistant_not_found() -> None:
1299
+ """
1300
+ Purpose: Verify text unchanged when assistant_id has no matching display.
1301
+ Why this matters: Should not modify text when target not present.
1302
+ Setup summary: Build display for one assistant, try removing different one.
1303
+ """
1304
+ # Arrange
1305
+ assistant_id_present = "agent-present"
1306
+ assistant_id_absent = "agent-absent"
1307
+ config = SubAgentDisplayConfig(
1308
+ mode=SubAgentResponseDisplayMode.PLAIN,
1309
+ add_quote_border=False,
1310
+ add_block_border=False,
1311
+ display_title_template="Answer from <strong>{}</strong>",
1312
+ )
1313
+
1314
+ # Build display for present assistant
1315
+ display = get_sub_agent_answer_display(
1316
+ display_name="Present Agent",
1317
+ display_config=config,
1318
+ answer="Present answer",
1319
+ assistant_id=assistant_id_present,
1320
+ )
1321
+
1322
+ text_with_display = f"Start\n{display}\nEnd"
1323
+ original_text = text_with_display
1324
+
1325
+ # Act - Try to remove absent assistant
1326
+ result = remove_sub_agent_answer_from_text(
1327
+ display_config=config,
1328
+ text=text_with_display,
1329
+ assistant_id=assistant_id_absent,
1330
+ )
1331
+
1332
+ # Assert
1333
+ assert result == original_text
1334
+ assert "Present answer" in result
1335
+ assert "Present Agent" in result