unique_toolkit 1.8.1__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 (105) hide show
  1. unique_toolkit/__init__.py +20 -0
  2. unique_toolkit/_common/api_calling/human_verification_manager.py +121 -28
  3. unique_toolkit/_common/chunk_relevancy_sorter/config.py +3 -3
  4. unique_toolkit/_common/chunk_relevancy_sorter/tests/test_service.py +2 -5
  5. unique_toolkit/_common/default_language_model.py +9 -3
  6. unique_toolkit/_common/docx_generator/__init__.py +7 -0
  7. unique_toolkit/_common/docx_generator/config.py +12 -0
  8. unique_toolkit/_common/docx_generator/schemas.py +80 -0
  9. unique_toolkit/_common/docx_generator/service.py +252 -0
  10. unique_toolkit/_common/docx_generator/template/Doc Template.docx +0 -0
  11. unique_toolkit/_common/endpoint_builder.py +138 -117
  12. unique_toolkit/_common/endpoint_requestor.py +240 -14
  13. unique_toolkit/_common/exception.py +20 -0
  14. unique_toolkit/_common/feature_flags/schema.py +1 -5
  15. unique_toolkit/_common/referencing.py +53 -0
  16. unique_toolkit/_common/string_utilities.py +52 -1
  17. unique_toolkit/_common/tests/test_referencing.py +521 -0
  18. unique_toolkit/_common/tests/test_string_utilities.py +506 -0
  19. unique_toolkit/_common/utils/files.py +43 -0
  20. unique_toolkit/agentic/debug_info_manager/debug_info_manager.py +16 -6
  21. unique_toolkit/agentic/debug_info_manager/test/test_debug_info_manager.py +278 -0
  22. unique_toolkit/agentic/evaluation/config.py +3 -2
  23. unique_toolkit/agentic/evaluation/context_relevancy/service.py +2 -2
  24. unique_toolkit/agentic/evaluation/evaluation_manager.py +9 -5
  25. unique_toolkit/agentic/evaluation/hallucination/constants.py +1 -1
  26. unique_toolkit/agentic/evaluation/hallucination/hallucination_evaluation.py +26 -3
  27. unique_toolkit/agentic/history_manager/history_manager.py +14 -11
  28. unique_toolkit/agentic/history_manager/loop_token_reducer.py +3 -4
  29. unique_toolkit/agentic/history_manager/utils.py +10 -87
  30. unique_toolkit/agentic/postprocessor/postprocessor_manager.py +107 -16
  31. unique_toolkit/agentic/reference_manager/reference_manager.py +1 -1
  32. unique_toolkit/agentic/responses_api/__init__.py +19 -0
  33. unique_toolkit/agentic/responses_api/postprocessors/code_display.py +63 -0
  34. unique_toolkit/agentic/responses_api/postprocessors/generated_files.py +145 -0
  35. unique_toolkit/agentic/responses_api/stream_handler.py +15 -0
  36. unique_toolkit/agentic/tools/a2a/__init__.py +18 -2
  37. unique_toolkit/agentic/tools/a2a/evaluation/__init__.py +2 -0
  38. unique_toolkit/agentic/tools/a2a/evaluation/_utils.py +3 -3
  39. unique_toolkit/agentic/tools/a2a/evaluation/config.py +1 -1
  40. unique_toolkit/agentic/tools/a2a/evaluation/evaluator.py +143 -91
  41. unique_toolkit/agentic/tools/a2a/manager.py +7 -1
  42. unique_toolkit/agentic/tools/a2a/postprocessing/__init__.py +11 -3
  43. unique_toolkit/agentic/tools/a2a/postprocessing/_display_utils.py +185 -0
  44. unique_toolkit/agentic/tools/a2a/postprocessing/_ref_utils.py +73 -0
  45. unique_toolkit/agentic/tools/a2a/postprocessing/config.py +21 -0
  46. unique_toolkit/agentic/tools/a2a/postprocessing/display.py +180 -0
  47. unique_toolkit/agentic/tools/a2a/postprocessing/references.py +101 -0
  48. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display_utils.py +1335 -0
  49. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_ref_utils.py +603 -0
  50. unique_toolkit/agentic/tools/a2a/prompts.py +46 -0
  51. unique_toolkit/agentic/tools/a2a/response_watcher/__init__.py +6 -0
  52. unique_toolkit/agentic/tools/a2a/response_watcher/service.py +91 -0
  53. unique_toolkit/agentic/tools/a2a/tool/config.py +15 -5
  54. unique_toolkit/agentic/tools/a2a/tool/service.py +69 -36
  55. unique_toolkit/agentic/tools/config.py +16 -2
  56. unique_toolkit/agentic/tools/factory.py +4 -0
  57. unique_toolkit/agentic/tools/mcp/tool_wrapper.py +7 -35
  58. unique_toolkit/agentic/tools/openai_builtin/__init__.py +11 -0
  59. unique_toolkit/agentic/tools/openai_builtin/base.py +30 -0
  60. unique_toolkit/agentic/tools/openai_builtin/code_interpreter/__init__.py +8 -0
  61. unique_toolkit/agentic/tools/openai_builtin/code_interpreter/config.py +57 -0
  62. unique_toolkit/agentic/tools/openai_builtin/code_interpreter/service.py +230 -0
  63. unique_toolkit/agentic/tools/openai_builtin/manager.py +62 -0
  64. unique_toolkit/agentic/tools/test/test_mcp_manager.py +95 -7
  65. unique_toolkit/agentic/tools/test/test_tool_progress_reporter.py +240 -0
  66. unique_toolkit/agentic/tools/tool.py +0 -11
  67. unique_toolkit/agentic/tools/tool_manager.py +337 -122
  68. unique_toolkit/agentic/tools/tool_progress_reporter.py +81 -15
  69. unique_toolkit/agentic/tools/utils/__init__.py +18 -0
  70. unique_toolkit/agentic/tools/utils/execution/execution.py +8 -4
  71. unique_toolkit/agentic/tools/utils/source_handling/schema.py +1 -1
  72. unique_toolkit/chat/__init__.py +8 -1
  73. unique_toolkit/chat/deprecated/service.py +232 -0
  74. unique_toolkit/chat/functions.py +54 -40
  75. unique_toolkit/chat/rendering.py +34 -0
  76. unique_toolkit/chat/responses_api.py +461 -0
  77. unique_toolkit/chat/schemas.py +1 -1
  78. unique_toolkit/chat/service.py +96 -1569
  79. unique_toolkit/content/functions.py +116 -1
  80. unique_toolkit/content/schemas.py +59 -0
  81. unique_toolkit/content/service.py +5 -37
  82. unique_toolkit/content/smart_rules.py +301 -0
  83. unique_toolkit/framework_utilities/langchain/client.py +27 -3
  84. unique_toolkit/framework_utilities/openai/client.py +12 -1
  85. unique_toolkit/framework_utilities/openai/message_builder.py +85 -1
  86. unique_toolkit/language_model/default_language_model.py +3 -0
  87. unique_toolkit/language_model/functions.py +25 -9
  88. unique_toolkit/language_model/infos.py +72 -4
  89. unique_toolkit/language_model/schemas.py +246 -40
  90. unique_toolkit/protocols/support.py +91 -9
  91. unique_toolkit/services/__init__.py +7 -0
  92. unique_toolkit/services/chat_service.py +1630 -0
  93. unique_toolkit/services/knowledge_base.py +861 -0
  94. unique_toolkit/smart_rules/compile.py +56 -301
  95. unique_toolkit/test_utilities/events.py +197 -0
  96. {unique_toolkit-1.8.1.dist-info → unique_toolkit-1.23.0.dist-info}/METADATA +173 -3
  97. {unique_toolkit-1.8.1.dist-info → unique_toolkit-1.23.0.dist-info}/RECORD +99 -67
  98. unique_toolkit/agentic/tools/a2a/postprocessing/_display.py +0 -122
  99. unique_toolkit/agentic/tools/a2a/postprocessing/_utils.py +0 -19
  100. unique_toolkit/agentic/tools/a2a/postprocessing/postprocessor.py +0 -230
  101. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_consolidate_references.py +0 -665
  102. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display.py +0 -391
  103. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_postprocessor_reference_functions.py +0 -256
  104. {unique_toolkit-1.8.1.dist-info → unique_toolkit-1.23.0.dist-info}/LICENSE +0 -0
  105. {unique_toolkit-1.8.1.dist-info → unique_toolkit-1.23.0.dist-info}/WHEEL +0 -0
@@ -1,391 +0,0 @@
1
- import re
2
-
3
- import pytest
4
-
5
- from unique_toolkit.agentic.tools.a2a.postprocessing._display import (
6
- _build_sub_agent_answer_display,
7
- _DetailsResponseDisplayHandler,
8
- _remove_sub_agent_answer_from_text,
9
- )
10
- from unique_toolkit.agentic.tools.a2a.postprocessing.config import (
11
- SubAgentResponseDisplayMode,
12
- )
13
-
14
-
15
- class TestDetailsResponseDisplayHandler:
16
- """Test suite for DetailsResponseDisplayHandler class."""
17
-
18
- @pytest.fixture
19
- def open_handler(self) -> _DetailsResponseDisplayHandler:
20
- """Create a handler with open mode."""
21
- return _DetailsResponseDisplayHandler(mode="open")
22
-
23
- @pytest.fixture
24
- def closed_handler(self) -> _DetailsResponseDisplayHandler:
25
- """Create a handler with closed mode."""
26
- return _DetailsResponseDisplayHandler(mode="closed")
27
-
28
- @pytest.fixture
29
- def sample_data(self):
30
- """Sample data for testing."""
31
- return {
32
- "display_name": "Test Assistant",
33
- "assistant_id": "test_assistant_123",
34
- "answer": "This is a test answer with multiple lines.\nSecond line here.",
35
- }
36
-
37
- def test_build_response_display_open_mode(self, open_handler, sample_data):
38
- """Test building response display in open mode."""
39
- result = open_handler.build_response_display(
40
- display_name=sample_data["display_name"],
41
- assistant_id=sample_data["assistant_id"],
42
- answer=sample_data["answer"],
43
- )
44
-
45
- assert "<details open>" in result
46
- assert (
47
- f'<div style="display: none;">{sample_data["assistant_id"]}</div>' in result
48
- )
49
- assert f"<summary>{sample_data['display_name']}</summary>" in result
50
- assert sample_data["answer"] in result
51
- assert "</details>" in result
52
-
53
- def test_build_response_display_closed_mode(self, closed_handler, sample_data):
54
- """Test building response display in closed mode."""
55
- result = closed_handler.build_response_display(
56
- display_name=sample_data["display_name"],
57
- assistant_id=sample_data["assistant_id"],
58
- answer=sample_data["answer"],
59
- )
60
-
61
- assert "<details>" in result
62
- assert "<details open>" not in result
63
- assert (
64
- f'<div style="display: none;">{sample_data["assistant_id"]}</div>' in result
65
- )
66
- assert f"<summary>{sample_data['display_name']}</summary>" in result
67
- assert sample_data["answer"] in result
68
- assert "</details>" in result
69
-
70
- def test_build_response_display_with_special_characters(self, open_handler):
71
- """Test building response display with special characters in content."""
72
- result = open_handler.build_response_display(
73
- display_name="Test & Co.",
74
- assistant_id="test<>123",
75
- answer="Answer with <tags> & symbols",
76
- )
77
-
78
- assert "Test & Co." in result
79
- assert "test<>123" in result
80
- assert "Answer with <tags> & symbols" in result
81
-
82
- def test_remove_response_display_open_mode(self, open_handler, sample_data):
83
- """Test removing response display from text in open mode."""
84
- # First build the display
85
- display_html = open_handler.build_response_display(
86
- display_name=sample_data["display_name"],
87
- assistant_id=sample_data["assistant_id"],
88
- answer=sample_data["answer"],
89
- )
90
-
91
- # Create text with the display embedded
92
- text_with_display = f"Some text before\n{display_html}\nSome text after"
93
-
94
- # Remove the display
95
- result = open_handler.remove_response_display(
96
- assistant_id=sample_data["assistant_id"], text=text_with_display
97
- )
98
-
99
- assert "Some text before" in result
100
- assert "Some text after" in result
101
- assert sample_data["display_name"] not in result
102
- assert sample_data["answer"] not in result
103
-
104
- def test_remove_response_display_closed_mode(self, closed_handler, sample_data):
105
- """Test removing response display from text in closed mode."""
106
- # First build the display
107
- display_html = closed_handler.build_response_display(
108
- display_name=sample_data["display_name"],
109
- assistant_id=sample_data["assistant_id"],
110
- answer=sample_data["answer"],
111
- )
112
-
113
- # Create text with the display embedded
114
- text_with_display = f"Some text before\n{display_html}\nSome text after"
115
-
116
- # Remove the display
117
- result = closed_handler.remove_response_display(
118
- assistant_id=sample_data["assistant_id"], text=text_with_display
119
- )
120
-
121
- assert "Some text before" in result
122
- assert "Some text after" in result
123
- assert sample_data["display_name"] not in result
124
- assert sample_data["answer"] not in result
125
-
126
- def test_remove_response_display_multiple_instances(self, open_handler):
127
- """Test removing multiple instances of response display."""
128
- assistant_id = "test_123"
129
-
130
- display1 = open_handler.build_response_display(
131
- display_name="First", assistant_id=assistant_id, answer="First answer"
132
- )
133
-
134
- display2 = open_handler.build_response_display(
135
- display_name="Second", assistant_id=assistant_id, answer="Second answer"
136
- )
137
-
138
- text_with_displays = f"Start\n{display1}\nMiddle\n{display2}\nEnd"
139
-
140
- result = open_handler.remove_response_display(
141
- assistant_id=assistant_id, text=text_with_displays
142
- )
143
-
144
- assert "Start" in result
145
- assert "Middle" in result
146
- assert "End" in result
147
- assert "First answer" not in result
148
- assert "Second answer" not in result
149
-
150
- def test_remove_response_display_no_match(self, open_handler):
151
- """Test removing response display when no match exists."""
152
- text = "This is some text without any displays"
153
- result = open_handler.remove_response_display(
154
- assistant_id="nonexistent", text=text
155
- )
156
- assert result == text
157
-
158
- def test_remove_response_display_with_regex_special_chars(self, open_handler):
159
- """Test removing response display with regex special characters in assistant_id."""
160
- assistant_id = "test.+*?[]{}()^$|"
161
-
162
- display_html = open_handler.build_response_display(
163
- display_name="Test", assistant_id=assistant_id, answer="Test answer"
164
- )
165
-
166
- text_with_display = f"Before\n{display_html}\nAfter"
167
-
168
- result = open_handler.remove_response_display(
169
- assistant_id=assistant_id, text=text_with_display
170
- )
171
-
172
- assert "Before" in result
173
- assert "After" in result
174
- assert "Test answer" not in result
175
-
176
- def test_get_detect_re_pattern_validity(self, open_handler, closed_handler):
177
- """Test that the regex patterns are valid and compilable."""
178
- assistant_id = "test_123"
179
-
180
- open_pattern = open_handler._get_detect_re(assistant_id)
181
- closed_pattern = closed_handler._get_detect_re(assistant_id)
182
-
183
- # Should not raise exceptions
184
- re.compile(open_pattern)
185
- re.compile(closed_pattern)
186
-
187
- assert "(?s)" in open_pattern # multiline flag
188
- assert "(?s)" in closed_pattern
189
- assert "details open" in open_pattern
190
- assert "details>" in closed_pattern
191
- assert "details open" not in closed_pattern
192
-
193
-
194
- class TestDisplayFunctions:
195
- """Test suite for module-level display functions."""
196
-
197
- @pytest.fixture
198
- def sample_data(self):
199
- """Sample data for testing."""
200
- return {
201
- "display_name": "Test Assistant",
202
- "assistant_id": "test_assistant_123",
203
- "answer": "This is a test answer.",
204
- }
205
-
206
- @pytest.mark.parametrize(
207
- "display_mode,expected_content,not_expected_content",
208
- [
209
- (SubAgentResponseDisplayMode.DETAILS_OPEN, "<details open>", None),
210
- (SubAgentResponseDisplayMode.DETAILS_CLOSED, "<details>", "<details open>"),
211
- (SubAgentResponseDisplayMode.HIDDEN, "", None),
212
- ],
213
- )
214
- def test_build_sub_agent_answer_display(
215
- self, sample_data, display_mode, expected_content, not_expected_content
216
- ):
217
- """Test building sub-agent answer display with different modes."""
218
- result = _build_sub_agent_answer_display(
219
- display_name=sample_data["display_name"],
220
- display_mode=display_mode,
221
- answer=sample_data["answer"],
222
- assistant_id=sample_data["assistant_id"],
223
- )
224
-
225
- if display_mode == SubAgentResponseDisplayMode.HIDDEN:
226
- assert result == ""
227
- else:
228
- assert expected_content in result
229
- assert sample_data["display_name"] in result
230
- assert sample_data["answer"] in result
231
- assert sample_data["assistant_id"] in result
232
-
233
- if not_expected_content:
234
- assert not_expected_content not in result
235
-
236
- @pytest.mark.parametrize(
237
- "display_mode",
238
- [
239
- SubAgentResponseDisplayMode.DETAILS_OPEN,
240
- SubAgentResponseDisplayMode.DETAILS_CLOSED,
241
- ],
242
- )
243
- def test_remove_sub_agent_answer_from_text_details_modes(
244
- self, sample_data, display_mode
245
- ):
246
- """Test removing sub-agent answer from text with DETAILS_OPEN and DETAILS_CLOSED modes."""
247
- # First build the display
248
- display_html = _build_sub_agent_answer_display(
249
- display_name=sample_data["display_name"],
250
- display_mode=display_mode,
251
- answer=sample_data["answer"],
252
- assistant_id=sample_data["assistant_id"],
253
- )
254
-
255
- text_with_display = f"Before\n{display_html}\nAfter"
256
-
257
- result = _remove_sub_agent_answer_from_text(
258
- display_mode=display_mode,
259
- text=text_with_display,
260
- assistant_id=sample_data["assistant_id"],
261
- )
262
-
263
- assert "Before" in result
264
- assert "After" in result
265
- assert sample_data["answer"] not in result
266
-
267
- def test_remove_sub_agent_answer_from_text_hidden_mode(self, sample_data):
268
- """Test removing sub-agent answer from text with HIDDEN mode."""
269
- text = "Some text here"
270
- result = _remove_sub_agent_answer_from_text(
271
- display_mode=SubAgentResponseDisplayMode.HIDDEN,
272
- text=text,
273
- assistant_id=sample_data["assistant_id"],
274
- )
275
-
276
- assert result == text
277
-
278
- def test_roundtrip_build_and_remove(self, sample_data):
279
- """Test that building and then removing display results in clean text."""
280
- original_text = "This is the original text."
281
-
282
- # Build display
283
- display_html = _build_sub_agent_answer_display(
284
- display_name=sample_data["display_name"],
285
- display_mode=SubAgentResponseDisplayMode.DETAILS_OPEN,
286
- answer=sample_data["answer"],
287
- assistant_id=sample_data["assistant_id"],
288
- )
289
-
290
- # Insert into text
291
- text_with_display = f"{original_text}\n{display_html}"
292
-
293
- # Remove display
294
- result = _remove_sub_agent_answer_from_text(
295
- display_mode=SubAgentResponseDisplayMode.DETAILS_OPEN,
296
- text=text_with_display,
297
- assistant_id=sample_data["assistant_id"],
298
- )
299
-
300
- # Should be back to original (with some whitespace differences)
301
- assert original_text in result.strip()
302
- assert sample_data["answer"] not in result
303
-
304
-
305
- class TestEdgeCases:
306
- """Test suite for edge cases and error conditions."""
307
-
308
- def test_empty_strings(self):
309
- """Test handling of empty strings."""
310
- handler = _DetailsResponseDisplayHandler(mode="open")
311
-
312
- result = handler.build_response_display(
313
- display_name="", assistant_id="test", answer=""
314
- )
315
-
316
- assert "<details open>" in result
317
- assert "<summary></summary>" in result
318
-
319
- def test_multiline_content(self):
320
- """Test handling of multiline content."""
321
- handler = _DetailsResponseDisplayHandler(mode="open")
322
-
323
- multiline_answer = """Line 1
324
- Line 2
325
- Line 3 with spaces
326
-
327
- Line 5 after blank line"""
328
-
329
- result = handler.build_response_display(
330
- display_name="Multi-line Test",
331
- assistant_id="test_ml",
332
- answer=multiline_answer,
333
- )
334
-
335
- assert multiline_answer in result
336
-
337
- # Test removal
338
- text_with_display = f"Before\n{result}\nAfter"
339
- clean_result = handler.remove_response_display(
340
- assistant_id="test_ml", text=text_with_display
341
- )
342
-
343
- assert "Before" in clean_result
344
- assert "After" in clean_result
345
- assert multiline_answer not in clean_result
346
-
347
- def test_html_content_in_answer(self):
348
- """Test handling of HTML content within the answer."""
349
- handler = _DetailsResponseDisplayHandler(mode="open")
350
-
351
- html_answer = "<p>This is <strong>bold</strong> text with <em>emphasis</em></p>"
352
-
353
- result = handler.build_response_display(
354
- display_name="HTML Test", assistant_id="test_html", answer=html_answer
355
- )
356
-
357
- assert html_answer in result
358
-
359
- # Test removal
360
- text_with_display = f"Before\n{result}\nAfter"
361
- clean_result = handler.remove_response_display(
362
- assistant_id="test_html", text=text_with_display
363
- )
364
-
365
- assert "Before" in clean_result
366
- assert "After" in clean_result
367
- assert html_answer not in clean_result
368
-
369
- def test_unicode_content(self):
370
- """Test handling of Unicode content."""
371
- handler = _DetailsResponseDisplayHandler(mode="open")
372
-
373
- unicode_content = "Testing Unicode: 你好 🌟 café naïve résumé"
374
-
375
- result = handler.build_response_display(
376
- display_name="Unicode Test",
377
- assistant_id="test_unicode",
378
- answer=unicode_content,
379
- )
380
-
381
- assert unicode_content in result
382
-
383
- # Test removal
384
- text_with_display = f"Before\n{result}\nAfter"
385
- clean_result = handler.remove_response_display(
386
- assistant_id="test_unicode", text=text_with_display
387
- )
388
-
389
- assert "Before" in clean_result
390
- assert "After" in clean_result
391
- assert unicode_content not in clean_result
@@ -1,256 +0,0 @@
1
- import pytest
2
-
3
- from unique_toolkit.agentic.tools.a2a.postprocessing._utils import (
4
- _replace_references_in_text,
5
- _replace_references_in_text_non_overlapping,
6
- )
7
-
8
-
9
- class TestReplaceReferencesInTextNonOverlapping:
10
- """Test cases for _replace_references_in_text_non_overlapping function."""
11
-
12
- def test_single_reference_replacement(self):
13
- """Test replacing a single reference."""
14
- text = "This is a test<sup>1</sup> with one reference."
15
- ref_map = {1: 5}
16
- result = _replace_references_in_text_non_overlapping(text, ref_map)
17
- expected = "This is a test<sup>5</sup> with one reference."
18
- assert result == expected
19
-
20
- def test_multiple_reference_replacements(self):
21
- """Test replacing multiple references."""
22
- text = "First<sup>1</sup> and second<sup>2</sup> and third<sup>3</sup>."
23
- ref_map = {1: 10, 2: 20, 3: 30}
24
- result = _replace_references_in_text_non_overlapping(text, ref_map)
25
- expected = "First<sup>10</sup> and second<sup>20</sup> and third<sup>30</sup>."
26
- assert result == expected
27
-
28
- def test_no_references_in_text(self):
29
- """Test with text that has no references."""
30
- text = "This text has no references at all."
31
- ref_map = {1: 5, 2: 10}
32
- result = _replace_references_in_text_non_overlapping(text, ref_map)
33
- assert result == text
34
-
35
- @pytest.mark.parametrize(
36
- "text,ref_map,expected",
37
- [
38
- (
39
- "This text has<sup>1</sup> references but empty map.",
40
- {},
41
- "This text has<sup>1</sup> references but empty map.",
42
- ),
43
- ("", {1: 5}, ""),
44
- ],
45
- )
46
- def test_empty_inputs(self, text, ref_map, expected):
47
- """Test with empty reference map or empty text."""
48
- result = _replace_references_in_text_non_overlapping(text, ref_map)
49
- assert result == expected
50
-
51
- def test_reference_not_in_map(self):
52
- """Test with references in text that are not in the map."""
53
- text = "Reference<sup>1</sup> and<sup>2</sup> and<sup>3</sup>."
54
- ref_map = {1: 10} # Only maps reference 1
55
- result = _replace_references_in_text_non_overlapping(text, ref_map)
56
- expected = "Reference<sup>10</sup> and<sup>2</sup> and<sup>3</sup>."
57
- assert result == expected
58
-
59
- def test_duplicate_references_in_text(self):
60
- """Test with duplicate references in text."""
61
- text = "First<sup>1</sup> and second<sup>1</sup> occurrence."
62
- ref_map = {1: 99}
63
- result = _replace_references_in_text_non_overlapping(text, ref_map)
64
- expected = "First<sup>99</sup> and second<sup>99</sup> occurrence."
65
- assert result == expected
66
-
67
- def test_adjacent_references(self):
68
- """Test with adjacent references."""
69
- text = "Adjacent<sup>1</sup><sup>2</sup> references."
70
- ref_map = {1: 10, 2: 20}
71
- result = _replace_references_in_text_non_overlapping(text, ref_map)
72
- expected = "Adjacent<sup>10</sup><sup>20</sup> references."
73
- assert result == expected
74
-
75
- def test_references_with_multi_digit_numbers(self):
76
- """Test with multi-digit reference numbers."""
77
- text = "Reference<sup>123</sup> and<sup>456</sup>."
78
- ref_map = {123: 789, 456: 101112}
79
- result = _replace_references_in_text_non_overlapping(text, ref_map)
80
- expected = "Reference<sup>789</sup> and<sup>101112</sup>."
81
- assert result == expected
82
-
83
- def test_references_at_text_boundaries(self):
84
- """Test with references at the beginning and end of text."""
85
- text = "<sup>1</sup>Start and end<sup>2</sup>"
86
- ref_map = {1: 100, 2: 200}
87
- result = _replace_references_in_text_non_overlapping(text, ref_map)
88
- expected = "<sup>100</sup>Start and end<sup>200</sup>"
89
- assert result == expected
90
-
91
- def test_malformed_references_ignored(self):
92
- """Test that malformed references are ignored."""
93
- text = "Good<sup>1</sup> and bad<sup>abc</sup> and<sup></sup>."
94
- ref_map = {1: 10}
95
- result = _replace_references_in_text_non_overlapping(text, ref_map)
96
- expected = "Good<sup>10</sup> and bad<sup>abc</sup> and<sup></sup>."
97
- assert result == expected
98
-
99
- @pytest.mark.parametrize(
100
- "text,ref_map,expected",
101
- [
102
- (
103
- "Zero reference<sup>0</sup> here.",
104
- {0: 100},
105
- "Zero reference<sup>100</sup> here.",
106
- ),
107
- (
108
- "Negative<sup>-1</sup> reference.",
109
- {-1: 5},
110
- "Negative<sup>5</sup> reference.",
111
- ),
112
- ],
113
- )
114
- def test_special_reference_numbers(self, text, ref_map, expected):
115
- """Test with zero and negative reference numbers."""
116
- result = _replace_references_in_text_non_overlapping(text, ref_map)
117
- assert result == expected
118
-
119
-
120
- class TestReplaceReferencesInText:
121
- """Test cases for _replace_references_in_text function."""
122
-
123
- def test_non_overlapping_simple_case(self):
124
- """Test simple non-overlapping case."""
125
- text = "Reference<sup>1</sup> and<sup>2</sup>."
126
- ref_map = {1: 10, 2: 20}
127
- result = _replace_references_in_text(text, ref_map)
128
- expected = "Reference<sup>10</sup> and<sup>20</sup>."
129
- assert result == expected
130
-
131
- def test_overlapping_references_case1(self):
132
- """Test overlapping case where new reference numbers conflict with existing ones."""
133
- text = "First<sup>1</sup> and second<sup>2</sup>."
134
- ref_map = {1: 2, 2: 1} # Swap references
135
- result = _replace_references_in_text(text, ref_map)
136
- expected = "First<sup>2</sup> and second<sup>1</sup>."
137
- assert result == expected
138
-
139
- def test_overlapping_references_case2(self):
140
- """Test overlapping case with chain of replacements."""
141
- text = "Refs<sup>1</sup><sup>2</sup><sup>3</sup>."
142
- ref_map = {1: 2, 2: 3, 3: 1} # Circular replacement
143
- result = _replace_references_in_text(text, ref_map)
144
- expected = "Refs<sup>2</sup><sup>3</sup><sup>1</sup>."
145
- assert result == expected
146
-
147
- def test_overlapping_with_higher_numbers(self):
148
- """Test overlapping where replacement numbers are higher than originals."""
149
- text = "Test<sup>1</sup> and<sup>2</sup>."
150
- ref_map = {1: 3, 2: 1} # 2 -> 1, but 1 -> 3
151
- result = _replace_references_in_text(text, ref_map)
152
- expected = "Test<sup>3</sup> and<sup>1</sup>."
153
- assert result == expected
154
-
155
- def test_complex_overlapping_scenario(self):
156
- """Test complex overlapping scenario with multiple conflicts."""
157
- text = "A<sup>1</sup>B<sup>2</sup>C<sup>3</sup>D<sup>4</sup>."
158
- ref_map = {1: 4, 2: 1, 3: 2, 4: 3} # Full rotation
159
- result = _replace_references_in_text(text, ref_map)
160
- expected = "A<sup>4</sup>B<sup>1</sup>C<sup>2</sup>D<sup>3</sup>."
161
- assert result == expected
162
-
163
- @pytest.mark.parametrize(
164
- "text,ref_map,expected",
165
- [
166
- (
167
- "Text with<sup>1</sup> references.",
168
- {},
169
- "Text with<sup>1</sup> references.",
170
- ),
171
- ("", {1: 2}, ""),
172
- (
173
- "This text has no references.",
174
- {1: 10, 2: 20},
175
- "This text has no references.",
176
- ),
177
- ],
178
- )
179
- def test_edge_cases(self, text, ref_map, expected):
180
- """Test edge cases: empty reference map, empty text, and text with no references."""
181
- result = _replace_references_in_text(text, ref_map)
182
- assert result == expected
183
-
184
- def test_single_reference_no_overlap(self):
185
- """Test single reference with no overlap potential."""
186
- text = "Single<sup>5</sup> reference."
187
- ref_map = {5: 100}
188
- result = _replace_references_in_text(text, ref_map)
189
- expected = "Single<sup>100</sup> reference."
190
- assert result == expected
191
-
192
- def test_partial_overlap(self):
193
- """Test case where only some references have overlapping numbers."""
194
- text = "Mix<sup>1</sup><sup>2</sup><sup>10</sup>."
195
- ref_map = {1: 2, 2: 20, 10: 100} # Only 1->2 creates potential overlap
196
- result = _replace_references_in_text(text, ref_map)
197
- expected = "Mix<sup>2</sup><sup>20</sup><sup>100</sup>."
198
- assert result == expected
199
-
200
- def test_self_mapping(self):
201
- """Test case where a reference maps to itself."""
202
- text = "Self<sup>1</sup> and other<sup>2</sup>."
203
- ref_map = {1: 1, 2: 10} # 1 maps to itself
204
- result = _replace_references_in_text(text, ref_map)
205
- expected = "Self<sup>1</sup> and other<sup>10</sup>."
206
- assert result == expected
207
-
208
- def test_duplicate_references_with_overlap(self):
209
- """Test duplicate references in text with overlapping mappings."""
210
- text = "Dup<sup>1</sup> and dup<sup>1</sup> and<sup>2</sup>."
211
- ref_map = {1: 2, 2: 1} # Swap
212
- result = _replace_references_in_text(text, ref_map)
213
- expected = "Dup<sup>2</sup> and dup<sup>2</sup> and<sup>1</sup>."
214
- assert result == expected
215
-
216
- def test_large_reference_numbers(self):
217
- """Test with large reference numbers."""
218
- text = "Large<sup>999</sup> and<sup>1000</sup>."
219
- ref_map = {999: 1000, 1000: 999} # Swap large numbers
220
- result = _replace_references_in_text(text, ref_map)
221
- expected = "Large<sup>1000</sup> and<sup>999</sup>."
222
- assert result == expected
223
-
224
- def test_zero_and_negative_with_overlap(self):
225
- """Test zero and negative numbers with potential overlap."""
226
- text = "Zero<sup>0</sup> and neg<sup>-1</sup> and pos<sup>1</sup>."
227
- ref_map = {0: 1, -1: 0, 1: -1} # Circular with zero and negative
228
- result = _replace_references_in_text(text, ref_map)
229
- expected = "Zero<sup>1</sup> and neg<sup>0</sup> and pos<sup>-1</sup>."
230
- assert result == expected
231
-
232
- def test_max_ref_calculation_edge_case(self):
233
- """Test edge case for max_ref calculation with empty map."""
234
- text = "Some<sup>1</sup> text."
235
- ref_map = {}
236
- result = _replace_references_in_text(text, ref_map)
237
- assert result == text
238
-
239
- def test_phase_separation_correctness(self):
240
- """Test that the two-phase approach correctly handles complex overlaps."""
241
- # This test ensures the intermediate unique references don't interfere
242
- text = "Test<sup>1</sup><sup>2</sup><sup>3</sup><sup>4</sup><sup>5</sup>."
243
- ref_map = {1: 5, 2: 4, 3: 3, 4: 2, 5: 1} # Reverse order
244
- result = _replace_references_in_text(text, ref_map)
245
- expected = "Test<sup>5</sup><sup>4</sup><sup>3</sup><sup>2</sup><sup>1</sup>."
246
- assert result == expected
247
-
248
- def test_intermediate_collision_avoidance(self):
249
- """Test that intermediate unique references don't collide with existing text."""
250
- # Create a scenario where intermediate refs might collide
251
- text = "Refs<sup>1</sup><sup>2</sup> and existing<sup>6</sup><sup>7</sup>."
252
- ref_map = {1: 2, 2: 1} # Simple swap
253
- result = _replace_references_in_text(text, ref_map)
254
- expected = "Refs<sup>2</sup><sup>1</sup> and existing<sup>6</sup><sup>7</sup>."
255
- assert result == expected
256
- # The existing <sup>6</sup> and <sup>7</sup> should remain unchanged