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
@@ -0,0 +1,603 @@
1
+ """
2
+ Unit tests for reference utility functions in _ref_utils.py.
3
+ """
4
+
5
+ import pytest
6
+
7
+ from unique_toolkit.agentic.tools.a2a.postprocessing._ref_utils import (
8
+ _add_source_ids,
9
+ add_content_refs,
10
+ add_content_refs_and_replace_in_text,
11
+ )
12
+ from unique_toolkit.content import ContentReference
13
+
14
+ # Fixtures
15
+
16
+
17
+ @pytest.fixture
18
+ def base_content_ref() -> ContentReference:
19
+ """Base ContentReference fixture for testing."""
20
+ return ContentReference(
21
+ name="Test Doc",
22
+ url="https://example.com/doc1",
23
+ sequence_number=1,
24
+ source_id="doc-123",
25
+ source="test-source",
26
+ )
27
+
28
+
29
+ @pytest.fixture
30
+ def content_refs_list() -> list[ContentReference]:
31
+ """List of ContentReference fixtures."""
32
+ return [
33
+ ContentReference(
34
+ name="Doc 1",
35
+ url="https://example.com/doc1",
36
+ sequence_number=1,
37
+ source_id="doc-1",
38
+ source="test-source",
39
+ ),
40
+ ContentReference(
41
+ name="Doc 2",
42
+ url="https://example.com/doc2",
43
+ sequence_number=2,
44
+ source_id="doc-2",
45
+ source="test-source",
46
+ ),
47
+ ]
48
+
49
+
50
+ # Tests for _add_source_ids
51
+
52
+
53
+ @pytest.mark.ai
54
+ def test_add_source_ids__returns_empty_dict__when_no_new_refs() -> None:
55
+ """
56
+ Purpose: Verify _add_source_ids returns empty dict when no new references provided.
57
+ Why this matters: Ensures function handles empty input gracefully.
58
+ Setup summary: Empty new_refs iterable, assert empty result dict.
59
+ """
60
+ # Arrange
61
+ existing_refs = {"doc-1": 1, "doc-2": 2}
62
+ new_refs: list[str] = []
63
+
64
+ # Act
65
+ result = _add_source_ids(existing_refs, new_refs)
66
+
67
+ # Assert
68
+ assert result == {}
69
+ assert isinstance(result, dict)
70
+
71
+
72
+ @pytest.mark.ai
73
+ def test_add_source_ids__assigns_sequential_numbers__for_new_source_ids() -> None:
74
+ """
75
+ Purpose: Verify _add_source_ids assigns sequential numbers starting after max existing.
76
+ Why this matters: Core functionality for maintaining reference number uniqueness.
77
+ Setup summary: Existing refs with max=2, add two new IDs, verify they get 3 and 4.
78
+ """
79
+ # Arrange
80
+ existing_refs = {"doc-1": 1, "doc-2": 2}
81
+ new_refs = ["doc-3", "doc-4"]
82
+
83
+ # Act
84
+ result = _add_source_ids(existing_refs, new_refs)
85
+
86
+ # Assert
87
+ assert result == {"doc-3": 3, "doc-4": 4}
88
+
89
+
90
+ @pytest.mark.ai
91
+ def test_add_source_ids__skips_existing_source_ids__does_not_duplicate() -> None:
92
+ """
93
+ Purpose: Verify _add_source_ids skips source IDs that already exist.
94
+ Why this matters: Prevents duplicate references and maintains reference integrity.
95
+ Setup summary: New refs include existing ID, verify it's not in result.
96
+ """
97
+ # Arrange
98
+ existing_refs = {"doc-1": 1, "doc-2": 2}
99
+ new_refs = ["doc-1", "doc-3"]
100
+
101
+ # Act
102
+ result = _add_source_ids(existing_refs, new_refs)
103
+
104
+ # Assert
105
+ assert result == {"doc-3": 3}
106
+ assert "doc-1" not in result
107
+
108
+
109
+ @pytest.mark.ai
110
+ def test_add_source_ids__handles_empty_existing_refs__starts_at_one() -> None:
111
+ """
112
+ Purpose: Verify _add_source_ids starts numbering at 1 when no existing refs.
113
+ Why this matters: Ensures correct initialization for new reference collections.
114
+ Setup summary: Empty existing_refs, add new refs, verify numbering starts at 1.
115
+ """
116
+ # Arrange
117
+ existing_refs: dict[str, int] = {}
118
+ new_refs = ["doc-1", "doc-2"]
119
+
120
+ # Act
121
+ result = _add_source_ids(existing_refs, new_refs)
122
+
123
+ # Assert
124
+ assert result == {"doc-1": 1, "doc-2": 2}
125
+
126
+
127
+ @pytest.mark.ai
128
+ def test_add_source_ids__handles_duplicate_in_new_refs__assigns_once() -> None:
129
+ """
130
+ Purpose: Verify _add_source_ids handles duplicates within new_refs correctly.
131
+ Why this matters: Prevents multiple assignments for same source ID in a batch.
132
+ Setup summary: New refs with duplicates, verify only one sequence number assigned.
133
+ """
134
+ # Arrange
135
+ existing_refs: dict[str, int] = {}
136
+ new_refs = ["doc-1", "doc-2", "doc-1", "doc-3"]
137
+
138
+ # Act
139
+ result = _add_source_ids(existing_refs, new_refs)
140
+
141
+ # Assert
142
+ assert result == {"doc-1": 1, "doc-2": 2, "doc-3": 3}
143
+
144
+
145
+ # Tests for add_content_refs
146
+
147
+
148
+ @pytest.mark.ai
149
+ def test_add_content_refs__returns_original_list__when_no_new_refs(
150
+ content_refs_list: list[ContentReference],
151
+ ) -> None:
152
+ """
153
+ Purpose: Verify add_content_refs returns original list unchanged when no new refs.
154
+ Why this matters: Handles empty additions efficiently without modification.
155
+ Setup summary: Existing refs, empty new refs list, assert original returned.
156
+ """
157
+ # Arrange
158
+ new_refs: list[ContentReference] = []
159
+
160
+ # Act
161
+ result = add_content_refs(content_refs_list, new_refs)
162
+
163
+ # Assert
164
+ assert result == content_refs_list
165
+ assert len(result) == 2
166
+
167
+
168
+ @pytest.mark.ai
169
+ def test_add_content_refs__appends_new_refs__with_updated_sequence_numbers(
170
+ content_refs_list: list[ContentReference],
171
+ ) -> None:
172
+ """
173
+ Purpose: Verify add_content_refs appends new refs with correct sequence numbers.
174
+ Why this matters: Core functionality for extending reference lists.
175
+ Setup summary: Two existing refs (seq 1,2), add one new, verify seq 3 assigned.
176
+ """
177
+ # Arrange
178
+ new_ref = ContentReference(
179
+ name="Doc 3",
180
+ url="https://example.com/doc3",
181
+ sequence_number=1, # Original number, should be updated
182
+ source_id="doc-3",
183
+ source="test-source",
184
+ )
185
+ new_refs = [new_ref]
186
+
187
+ # Act
188
+ result = add_content_refs(content_refs_list, new_refs)
189
+
190
+ # Assert
191
+ assert len(result) == 3
192
+ assert result[2].source_id == "doc-3"
193
+ assert result[2].sequence_number == 3
194
+
195
+
196
+ @pytest.mark.ai
197
+ def test_add_content_refs__skips_duplicate_source_ids__no_duplication(
198
+ content_refs_list: list[ContentReference],
199
+ ) -> None:
200
+ """
201
+ Purpose: Verify add_content_refs skips refs with existing source_ids.
202
+ Why this matters: Prevents duplicate references in the final list.
203
+ Setup summary: New ref with existing source_id, verify not added again.
204
+ """
205
+ # Arrange
206
+ duplicate_ref = ContentReference(
207
+ name="Doc 1 Duplicate",
208
+ url="https://example.com/doc1-dup",
209
+ sequence_number=99,
210
+ source_id="doc-1", # Already exists
211
+ source="test-source",
212
+ )
213
+ new_refs = [duplicate_ref]
214
+
215
+ # Act
216
+ result = add_content_refs(content_refs_list, new_refs)
217
+
218
+ # Assert
219
+ assert len(result) == 2 # No new item added
220
+ assert all(ref.source_id != "doc-1" or ref.name == "Doc 1" for ref in result)
221
+
222
+
223
+ @pytest.mark.ai
224
+ def test_add_content_refs__handles_multiple_new_refs__sequential_numbering() -> None:
225
+ """
226
+ Purpose: Verify add_content_refs handles multiple new refs with sequential numbering.
227
+ Why this matters: Ensures batch additions maintain sequence integrity.
228
+ Setup summary: Add three new refs, verify they get sequential numbers 1, 2, 3.
229
+ """
230
+ # Arrange
231
+ message_refs: list[ContentReference] = []
232
+ new_refs = [
233
+ ContentReference(
234
+ name="Doc A",
235
+ url="https://example.com/a",
236
+ sequence_number=10,
237
+ source_id="doc-a",
238
+ source="test",
239
+ ),
240
+ ContentReference(
241
+ name="Doc B",
242
+ url="https://example.com/b",
243
+ sequence_number=20,
244
+ source_id="doc-b",
245
+ source="test",
246
+ ),
247
+ ContentReference(
248
+ name="Doc C",
249
+ url="https://example.com/c",
250
+ sequence_number=30,
251
+ source_id="doc-c",
252
+ source="test",
253
+ ),
254
+ ]
255
+
256
+ # Act
257
+ result = add_content_refs(message_refs, new_refs)
258
+
259
+ # Assert
260
+ assert len(result) == 3
261
+ assert result[0].sequence_number == 1
262
+ assert result[1].sequence_number == 2
263
+ assert result[2].sequence_number == 3
264
+
265
+
266
+ @pytest.mark.ai
267
+ def test_add_content_refs__preserves_original_ref_properties__except_sequence_num(
268
+ content_refs_list: list[ContentReference],
269
+ ) -> None:
270
+ """
271
+ Purpose: Verify add_content_refs preserves all properties except sequence_number.
272
+ Why this matters: Ensures reference data integrity during addition.
273
+ Setup summary: Add ref with specific properties, verify all preserved with new seq num.
274
+ """
275
+ # Arrange
276
+ new_ref = ContentReference(
277
+ name="Special Doc",
278
+ url="https://example.com/special",
279
+ sequence_number=999,
280
+ source_id="doc-special",
281
+ source="special-source",
282
+ id="custom-id",
283
+ message_id="msg-123",
284
+ original_index=[1, 2, 3],
285
+ )
286
+ new_refs = [new_ref]
287
+
288
+ # Act
289
+ result = add_content_refs(content_refs_list, new_refs)
290
+
291
+ # Assert
292
+ added_ref = result[2]
293
+ assert added_ref.name == "Special Doc"
294
+ assert added_ref.url == "https://example.com/special"
295
+ assert added_ref.source_id == "doc-special"
296
+ assert added_ref.source == "special-source"
297
+ assert added_ref.id == "custom-id"
298
+ assert added_ref.message_id == "msg-123"
299
+ assert added_ref.original_index == [1, 2, 3]
300
+ assert added_ref.sequence_number == 3 # Updated
301
+
302
+
303
+ @pytest.mark.ai
304
+ def test_add_content_refs__sorts_new_refs_by_sequence_number__before_processing() -> (
305
+ None
306
+ ):
307
+ """
308
+ Purpose: Verify add_content_refs processes new refs in sequence_number order.
309
+ Why this matters: Ensures predictable ordering when multiple refs added.
310
+ Setup summary: Add refs in unsorted order, verify they're assigned by original seq order.
311
+ """
312
+ # Arrange
313
+ message_refs: list[ContentReference] = []
314
+ new_refs = [
315
+ ContentReference(
316
+ name="Third",
317
+ url="",
318
+ sequence_number=30,
319
+ source_id="doc-c",
320
+ source="test",
321
+ ),
322
+ ContentReference(
323
+ name="First",
324
+ url="",
325
+ sequence_number=10,
326
+ source_id="doc-a",
327
+ source="test",
328
+ ),
329
+ ContentReference(
330
+ name="Second",
331
+ url="",
332
+ sequence_number=20,
333
+ source_id="doc-b",
334
+ source="test",
335
+ ),
336
+ ]
337
+
338
+ # Act
339
+ result = add_content_refs(message_refs, new_refs)
340
+
341
+ # Assert
342
+ assert len(result) == 3
343
+ # doc-a (seq 10) should be processed first and assigned 1
344
+ assert result[0].source_id == "doc-a"
345
+ assert result[0].sequence_number == 1
346
+
347
+
348
+ # Tests for add_content_refs_and_replace_in_text
349
+
350
+
351
+ @pytest.mark.ai
352
+ def test_add_content_refs_and_replace_in_text__returns_unchanged__when_no_new_refs() -> (
353
+ None
354
+ ):
355
+ """
356
+ Purpose: Verify function returns unchanged text and refs when no new refs provided.
357
+ Why this matters: Handles empty additions efficiently.
358
+ Setup summary: Text and refs with empty new_refs, verify no changes.
359
+ """
360
+ # Arrange
361
+ message_text = "Some text with <sup>1</sup> reference"
362
+ message_refs = [
363
+ ContentReference(
364
+ name="Doc 1",
365
+ url="https://example.com",
366
+ sequence_number=1,
367
+ source_id="doc-1",
368
+ source="test",
369
+ )
370
+ ]
371
+ new_refs: list[ContentReference] = []
372
+
373
+ # Act
374
+ result_text, result_refs = add_content_refs_and_replace_in_text(
375
+ message_text, message_refs, new_refs
376
+ )
377
+
378
+ # Assert
379
+ assert result_text == message_text
380
+ assert result_refs == message_refs
381
+
382
+
383
+ @pytest.mark.ai
384
+ def test_add_content_refs_and_replace_in_text__replaces_ref_numbers__in_text() -> None:
385
+ """
386
+ Purpose: Verify function replaces reference numbers in text with updated numbers.
387
+ Why this matters: Maintains text-reference synchronization.
388
+ Setup summary: Text with <sup>1</sup>, add new ref that renumbers it, verify replacement.
389
+ """
390
+ # Arrange
391
+ message_text = "Check this reference <sup>1</sup> here"
392
+ message_refs: list[ContentReference] = []
393
+ new_refs = [
394
+ ContentReference(
395
+ name="Doc 1",
396
+ url="https://example.com/doc1",
397
+ sequence_number=1,
398
+ source_id="doc-1",
399
+ source="test",
400
+ )
401
+ ]
402
+
403
+ # Act
404
+ result_text, result_refs = add_content_refs_and_replace_in_text(
405
+ message_text, message_refs, new_refs
406
+ )
407
+
408
+ # Assert
409
+ assert "<sup>1</sup>" in result_text
410
+ assert len(result_refs) == 1
411
+
412
+
413
+ @pytest.mark.ai
414
+ def test_add_content_refs_and_replace_in_text__uses_custom_pattern_functions(
415
+ mocker,
416
+ ) -> None:
417
+ """
418
+ Purpose: Verify function uses custom pattern and replacement functions when provided.
419
+ Why this matters: Allows flexibility for different reference formats.
420
+ Setup summary: Mock pattern functions with regex-escaped patterns, verify they're called correctly.
421
+ """
422
+ # Arrange
423
+ # Use regex-escaped pattern since replace_in_text uses re.sub
424
+ message_text = "[REF-1] is here"
425
+ message_refs: list[ContentReference] = []
426
+ new_refs = [
427
+ ContentReference(
428
+ name="Doc 1",
429
+ url="",
430
+ sequence_number=1,
431
+ source_id="doc-1",
432
+ source="test",
433
+ )
434
+ ]
435
+
436
+ def custom_pattern(num: int) -> str:
437
+ # Return regex-escaped pattern
438
+ return f"\\[REF-{num}\\]"
439
+
440
+ def custom_replacement(num: int) -> str:
441
+ return f"[REF-{num}]"
442
+
443
+ mock_pattern = mocker.Mock(side_effect=custom_pattern)
444
+ mock_replacement = mocker.Mock(side_effect=custom_replacement)
445
+
446
+ # Act
447
+ result_text, result_refs = add_content_refs_and_replace_in_text(
448
+ message_text,
449
+ message_refs,
450
+ new_refs,
451
+ ref_pattern_f=mock_pattern,
452
+ ref_replacement_f=mock_replacement,
453
+ )
454
+
455
+ # Assert
456
+ mock_pattern.assert_called_once_with(1)
457
+ mock_replacement.assert_called_once_with(1)
458
+ assert len(result_refs) == 1
459
+ assert result_text == "[REF-1] is here"
460
+
461
+
462
+ @pytest.mark.ai
463
+ def test_add_content_refs_and_replace_in_text__handles_multiple_refs_in_text() -> None:
464
+ """
465
+ Purpose: Verify function correctly handles multiple reference replacements in text.
466
+ Why this matters: Ensures batch text updates work correctly.
467
+ Setup summary: Text with multiple refs, add new refs, verify all updated.
468
+ """
469
+ # Arrange
470
+ message_text = "First <sup>1</sup> and second <sup>2</sup>"
471
+ message_refs: list[ContentReference] = []
472
+ new_refs = [
473
+ ContentReference(
474
+ name="Doc 1",
475
+ url="",
476
+ sequence_number=1,
477
+ source_id="doc-1",
478
+ source="test",
479
+ ),
480
+ ContentReference(
481
+ name="Doc 2",
482
+ url="",
483
+ sequence_number=2,
484
+ source_id="doc-2",
485
+ source="test",
486
+ ),
487
+ ]
488
+
489
+ # Act
490
+ result_text, result_refs = add_content_refs_and_replace_in_text(
491
+ message_text, message_refs, new_refs
492
+ )
493
+
494
+ # Assert
495
+ assert "<sup>1</sup>" in result_text
496
+ assert "<sup>2</sup>" in result_text
497
+ assert len(result_refs) == 2
498
+
499
+
500
+ @pytest.mark.ai
501
+ def test_add_content_refs_and_replace_in_text__avoids_duplicate_source_ids() -> None:
502
+ """
503
+ Purpose: Verify function doesn't add refs with duplicate source_ids.
504
+ Why this matters: Maintains reference uniqueness in combined operation.
505
+ Setup summary: Try to add ref with existing source_id, verify not duplicated.
506
+ """
507
+ # Arrange
508
+ message_text = "Text with <sup>1</sup>"
509
+ message_refs = [
510
+ ContentReference(
511
+ name="Doc 1",
512
+ url="",
513
+ sequence_number=1,
514
+ source_id="doc-1",
515
+ source="test",
516
+ )
517
+ ]
518
+ new_refs = [
519
+ ContentReference(
520
+ name="Doc 1 Duplicate",
521
+ url="",
522
+ sequence_number=2,
523
+ source_id="doc-1", # Duplicate
524
+ source="test",
525
+ )
526
+ ]
527
+
528
+ # Act
529
+ result_text, result_refs = add_content_refs_and_replace_in_text(
530
+ message_text, message_refs, new_refs
531
+ )
532
+
533
+ # Assert
534
+ assert len(result_refs) == 1 # No duplicate added
535
+ assert result_text == message_text
536
+
537
+
538
+ @pytest.mark.ai
539
+ def test_add_content_refs_and_replace_in_text__returns_tuple_with_correct_types() -> (
540
+ None
541
+ ):
542
+ """
543
+ Purpose: Verify function returns correctly typed tuple.
544
+ Why this matters: Ensures API contract and type safety.
545
+ Setup summary: Call function, assert return types are str and list.
546
+ """
547
+ # Arrange
548
+ message_text = "Test"
549
+ message_refs: list[ContentReference] = []
550
+ new_refs: list[ContentReference] = []
551
+
552
+ # Act
553
+ result_text, result_refs = add_content_refs_and_replace_in_text(
554
+ message_text, message_refs, new_refs
555
+ )
556
+
557
+ # Assert
558
+ assert isinstance(result_text, str)
559
+ assert isinstance(result_refs, list)
560
+
561
+
562
+ @pytest.mark.ai
563
+ def test_add_content_refs_and_replace_in_text__creates_ref_map_correctly() -> None:
564
+ """
565
+ Purpose: Verify function creates correct mapping for text replacement.
566
+ Why this matters: Ensures reference renumbering logic is correct.
567
+ Setup summary: Add refs that need renumbering, verify text updates accordingly.
568
+ """
569
+ # Arrange
570
+ # Start with existing ref at sequence 1
571
+ message_text = "See <sup>5</sup> for details"
572
+ message_refs = [
573
+ ContentReference(
574
+ name="Existing",
575
+ url="",
576
+ sequence_number=1,
577
+ source_id="doc-existing",
578
+ source="test",
579
+ )
580
+ ]
581
+ # Add new ref with sequence 5 that should become sequence 2
582
+ new_refs = [
583
+ ContentReference(
584
+ name="New Doc",
585
+ url="",
586
+ sequence_number=5,
587
+ source_id="doc-new",
588
+ source="test",
589
+ )
590
+ ]
591
+
592
+ # Act
593
+ result_text, result_refs = add_content_refs_and_replace_in_text(
594
+ message_text, message_refs, new_refs
595
+ )
596
+
597
+ # Assert
598
+ # The new ref's sequence number 5 should be mapped to 2
599
+ assert len(result_refs) == 2
600
+ assert result_refs[1].sequence_number == 2
601
+ # Text should have <sup>5</sup> replaced with <sup>2</sup>
602
+ assert "<sup>2</sup>" in result_text
603
+ assert "<sup>5</sup>" not in result_text
@@ -0,0 +1,46 @@
1
+ REFERENCING_INSTRUCTIONS_FOR_SYSTEM_PROMPT = """
2
+ Whenever you use a fact from a sub-agent response in yours, you MUST always reference it.
3
+
4
+ CRITICAL INSTRUCTION: References must always appear immediately after the fact they support.
5
+ Do NOT collect, group, or move references into a list at the end.
6
+
7
+ Rules:
8
+
9
+ 1. Inline placement: After every fact from SubAgentName, immediately attach its reference(s) inline.
10
+ Example:
11
+ “The stock price of Apple Inc. is $150” <sup><name>SubAgentName 2</name>1</sup>.
12
+
13
+ 2. No separate reference list: Do not place references in footnotes, bibliographies, or at the bottom.
14
+ Wrong:
15
+ “The stock price of Apple Inc. is $150.”
16
+ References: <sup><name>SubAgentName 2</name>1</sup>
17
+ Correct:
18
+ “The stock price of Apple Inc. is $150” <sup><name>SubAgentName 2</name>1</sup>.
19
+
20
+ 3. Exact copy: Copy references character-for-character from SubAgentName’s message.
21
+ Do not alter numbering, labels, or order.
22
+
23
+ 4. Multiple references: If more than one reference supports a single fact, include all of them inline, in the same sentence, in the original order.
24
+ Example:
25
+ “MSFT would be a good investment” <sup><name>SubAgentName 3</name>4</sup><sup><name>SubAgentName 3</name>8</sup>.
26
+ Wrong:
27
+ “MSFT would be a good investment” <sup><name>SubAgentName 3</name>8</sup><sup><name>SubAgentName 3</name>4</sup>. (order changed)
28
+
29
+ 5. Never at the bottom: References must always stay attached inline with the fact.
30
+ Multi-fact Example (Correct):
31
+ “Tesla delivered 400,000 cars in Q2” <sup><name>SubAgentName 4</name>2</sup>.
32
+ “Its revenue for the quarter was $24B” <sup><name>SubAgentName 4</name>5</sup>.
33
+ “The company also expanded its Berlin factory capacity” <sup><name>SubAgentName 4</name>7</sup>.
34
+ Wrong Multi-fact Example:
35
+ “Tesla delivered 400,000 cars in Q2. Its revenue for the quarter was $24B. The company also expanded its Berlin factory capacity.”
36
+ References: <sup><name>SubAgentName 4</name>2</sup><sup><name>SubAgentName 4</name>5</sup><sup><name>SubAgentName 4</name>7</sup>
37
+
38
+ 6. Fact repetition: If you reuse a fact from SubAgentName, you MUST reference it again inline with the correct format.
39
+
40
+ Reminder:
41
+ Inline = directly next to the fact, inside the same sentence or bullet.
42
+ """.strip()
43
+
44
+ REFERENCING_INSTRUCTIONS_FOR_USER_PROMPT = """
45
+ Rememeber to properly reference EACH fact from a sub agent's response with the correct format INLINE.
46
+ """.strip()
@@ -0,0 +1,6 @@
1
+ from unique_toolkit.agentic.tools.a2a.response_watcher.service import (
2
+ SubAgentResponse,
3
+ SubAgentResponseWatcher,
4
+ )
5
+
6
+ __all__ = ["SubAgentResponseWatcher", "SubAgentResponse"]