unique_toolkit 1.35.4__py3-none-any.whl → 1.36.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.
- unique_toolkit/_common/referencing.py +3 -3
- unique_toolkit/agentic/tools/a2a/postprocessing/config.py +1 -1
- unique_toolkit/agentic/tools/a2a/prompts.py +2 -0
- unique_toolkit/agentic/tools/a2a/tool/config.py +22 -4
- unique_toolkit/agentic/tools/a2a/tool/service.py +119 -69
- unique_toolkit/agentic/tools/a2a/tool/test/test_service_utils.py +829 -0
- {unique_toolkit-1.35.4.dist-info → unique_toolkit-1.36.0.dist-info}/METADATA +4 -1
- {unique_toolkit-1.35.4.dist-info → unique_toolkit-1.36.0.dist-info}/RECORD +10 -9
- {unique_toolkit-1.35.4.dist-info → unique_toolkit-1.36.0.dist-info}/LICENSE +0 -0
- {unique_toolkit-1.35.4.dist-info → unique_toolkit-1.36.0.dist-info}/WHEEL +0 -0
|
@@ -15,7 +15,7 @@ def _iter_ref_numbers(text: str) -> Generator[int, None, None]:
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
@functools.cache
|
|
18
|
-
def
|
|
18
|
+
def get_detection_pattern_for_ref(ref_number: int) -> re.Pattern[str]:
|
|
19
19
|
return re.compile(rf"<sup>\s*{ref_number}\s*</sup>")
|
|
20
20
|
|
|
21
21
|
|
|
@@ -35,11 +35,11 @@ def replace_ref_number(text: str, ref_number: int, replacement: int | str) -> st
|
|
|
35
35
|
if isinstance(replacement, int):
|
|
36
36
|
replacement = get_reference_pattern(replacement)
|
|
37
37
|
|
|
38
|
-
return
|
|
38
|
+
return get_detection_pattern_for_ref(ref_number).sub(replacement, text)
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
def remove_ref_number(text: str, ref_number: int) -> str:
|
|
42
|
-
return
|
|
42
|
+
return get_detection_pattern_for_ref(ref_number).sub("", text)
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
def remove_all_refs(text: str) -> str:
|
|
@@ -65,7 +65,7 @@ class SubAgentDisplayConfig(BaseModel):
|
|
|
65
65
|
)
|
|
66
66
|
force_include_references: bool = Field(
|
|
67
67
|
default=False,
|
|
68
|
-
description="If set, the sub agent references will be added to the main agent response references even
|
|
68
|
+
description="If set, the sub agent references will be added to the main agent response references even if not mentioned in the main agent response text.",
|
|
69
69
|
)
|
|
70
70
|
|
|
71
71
|
answer_substrings_config: list[SubAgentAnswerSubstringConfig] = Field(
|
|
@@ -37,6 +37,8 @@ References: <sup><name>SubAgentName 4</name>2</sup><sup><name>SubAgentName 4</na
|
|
|
37
37
|
|
|
38
38
|
6. Fact repetition: If you reuse a fact from SubAgentName, you MUST reference it again inline with the correct format.
|
|
39
39
|
|
|
40
|
+
7. You can ONLY use references if they are present in the subagent response! You must NOT create any references!
|
|
41
|
+
|
|
40
42
|
Reminder:
|
|
41
43
|
Inline = directly next to the fact, inside the same sentence or bullet.
|
|
42
44
|
""".strip()
|
|
@@ -13,6 +13,7 @@ class SubAgentSystemReminderType(StrEnum):
|
|
|
13
13
|
FIXED = "fixed"
|
|
14
14
|
REGEXP = "regexp"
|
|
15
15
|
REFERENCE = "reference"
|
|
16
|
+
NO_REFERENCE = "no_reference"
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
T = TypeVar("T", bound=SubAgentSystemReminderType)
|
|
@@ -31,12 +32,24 @@ The reminder to add to the tool response. The reminder can be a Jinja template a
|
|
|
31
32
|
""".strip()
|
|
32
33
|
|
|
33
34
|
|
|
35
|
+
class NoReferenceSystemReminderConfig(SystemReminderConfig):
|
|
36
|
+
"""A system reminder that is only added if the sub agent response does not contain any references."""
|
|
37
|
+
|
|
38
|
+
type: Literal[SubAgentSystemReminderType.NO_REFERENCE] = (
|
|
39
|
+
SubAgentSystemReminderType.NO_REFERENCE
|
|
40
|
+
)
|
|
41
|
+
reminder: str = Field(
|
|
42
|
+
default="Do NOT create any references from this sub agent in your response! The sub agent response does not contain any references.",
|
|
43
|
+
description=_SYSTEM_REMINDER_FIELD_DESCRIPTION,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
34
47
|
class ReferenceSystemReminderConfig(SystemReminderConfig):
|
|
35
48
|
type: Literal[SubAgentSystemReminderType.REFERENCE] = (
|
|
36
49
|
SubAgentSystemReminderType.REFERENCE
|
|
37
50
|
)
|
|
38
51
|
reminder: str = Field(
|
|
39
|
-
default="Rememeber to properly reference EACH fact from sub agent {{ display_name }}'s response with the correct format INLINE.",
|
|
52
|
+
default="Rememeber to properly reference EACH fact from sub agent {{ display_name }}'s response with the correct format INLINE. You MUST COPY THE REFERENCE AS PRESENT IN THE SUBAGENT RESPONSE.",
|
|
40
53
|
description=_SYSTEM_REMINDER_FIELD_DESCRIPTION,
|
|
41
54
|
)
|
|
42
55
|
|
|
@@ -69,6 +82,13 @@ class RegExpDetectedSystemReminderConfig(SystemReminderConfig):
|
|
|
69
82
|
)
|
|
70
83
|
|
|
71
84
|
|
|
85
|
+
SystemReminderConfigType = (
|
|
86
|
+
FixedSystemReminderConfig
|
|
87
|
+
| RegExpDetectedSystemReminderConfig
|
|
88
|
+
| ReferenceSystemReminderConfig
|
|
89
|
+
| NoReferenceSystemReminderConfig
|
|
90
|
+
)
|
|
91
|
+
|
|
72
92
|
DEFAULT_PARAM_DESCRIPTION_SUB_AGENT_USER_MESSAGE = """
|
|
73
93
|
This is the message that will be sent to the sub-agent.
|
|
74
94
|
""".strip()
|
|
@@ -147,9 +167,7 @@ class SubAgentToolConfig(BaseToolConfig):
|
|
|
147
167
|
|
|
148
168
|
system_reminders_config: list[
|
|
149
169
|
Annotated[
|
|
150
|
-
|
|
151
|
-
| RegExpDetectedSystemReminderConfig
|
|
152
|
-
| ReferenceSystemReminderConfig,
|
|
170
|
+
SystemReminderConfigType,
|
|
153
171
|
Field(discriminator="type"),
|
|
154
172
|
]
|
|
155
173
|
] = Field(
|
|
@@ -12,7 +12,7 @@ from unique_sdk.utils.chat_in_space import send_message_and_wait_for_completion
|
|
|
12
12
|
|
|
13
13
|
from unique_toolkit._common.referencing import (
|
|
14
14
|
get_all_ref_numbers,
|
|
15
|
-
|
|
15
|
+
get_detection_pattern_for_ref,
|
|
16
16
|
replace_ref_number,
|
|
17
17
|
)
|
|
18
18
|
from unique_toolkit._common.utils.jinja.render import render_template
|
|
@@ -29,6 +29,7 @@ from unique_toolkit.agentic.tools.a2a.tool.config import (
|
|
|
29
29
|
RegExpDetectedSystemReminderConfig,
|
|
30
30
|
SubAgentSystemReminderType,
|
|
31
31
|
SubAgentToolConfig,
|
|
32
|
+
SystemReminderConfigType,
|
|
32
33
|
)
|
|
33
34
|
from unique_toolkit.agentic.tools.factory import ToolFactory
|
|
34
35
|
from unique_toolkit.agentic.tools.schemas import ToolCallResponse
|
|
@@ -201,15 +202,39 @@ class SubAgentTool(Tool[SubAgentToolConfig]):
|
|
|
201
202
|
if response["text"] is None:
|
|
202
203
|
raise ValueError("No response returned from sub agent")
|
|
203
204
|
|
|
205
|
+
has_refs = False
|
|
206
|
+
content = ""
|
|
207
|
+
content_chunks = None
|
|
204
208
|
if self.config.returns_content_chunks:
|
|
205
|
-
content = ""
|
|
206
209
|
content_chunks = _ContentChunkList.validate_json(response["text"])
|
|
207
210
|
else:
|
|
208
|
-
|
|
211
|
+
has_refs = self.config.use_sub_agent_references and _response_has_refs(
|
|
212
|
+
response
|
|
213
|
+
)
|
|
214
|
+
content = response["text"]
|
|
215
|
+
if has_refs:
|
|
216
|
+
refs = response["references"]
|
|
217
|
+
assert refs is not None # Checked in _response_has_refs
|
|
218
|
+
content = _prepare_sub_agent_response_refs(
|
|
219
|
+
response=content,
|
|
220
|
+
name=self.name,
|
|
221
|
+
sequence_number=sequence_number,
|
|
222
|
+
refs=refs,
|
|
223
|
+
)
|
|
224
|
+
content = _remove_extra_refs(content, refs=refs)
|
|
225
|
+
else:
|
|
226
|
+
content = _remove_extra_refs(content, refs=[])
|
|
227
|
+
|
|
228
|
+
system_reminders = []
|
|
229
|
+
if not self.config.returns_content_chunks:
|
|
230
|
+
system_reminders = _get_sub_agent_system_reminders(
|
|
209
231
|
response=response["text"],
|
|
232
|
+
configs=self.config.system_reminders_config,
|
|
233
|
+
name=self.name,
|
|
234
|
+
display_name=self.display_name(),
|
|
210
235
|
sequence_number=sequence_number,
|
|
236
|
+
has_refs=has_refs,
|
|
211
237
|
)
|
|
212
|
-
content_chunks = None
|
|
213
238
|
|
|
214
239
|
await self._notify_progress(
|
|
215
240
|
tool_call=tool_call,
|
|
@@ -223,58 +248,11 @@ class SubAgentTool(Tool[SubAgentToolConfig]):
|
|
|
223
248
|
content=_format_response(
|
|
224
249
|
tool_name=self.name,
|
|
225
250
|
text=content,
|
|
226
|
-
system_reminders=
|
|
251
|
+
system_reminders=system_reminders,
|
|
227
252
|
),
|
|
228
253
|
content_chunks=content_chunks,
|
|
229
254
|
)
|
|
230
255
|
|
|
231
|
-
def _get_system_reminders(self, message: unique_sdk.Space.Message) -> list[str]:
|
|
232
|
-
reminders = []
|
|
233
|
-
for reminder_config in self.config.system_reminders_config:
|
|
234
|
-
if reminder_config.type == SubAgentSystemReminderType.FIXED:
|
|
235
|
-
reminders.append(
|
|
236
|
-
render_template(
|
|
237
|
-
reminder_config.reminder,
|
|
238
|
-
display_name=self.display_name(),
|
|
239
|
-
tool_name=self.name,
|
|
240
|
-
)
|
|
241
|
-
)
|
|
242
|
-
elif (
|
|
243
|
-
reminder_config.type == SubAgentSystemReminderType.REFERENCE
|
|
244
|
-
and self.config.use_sub_agent_references
|
|
245
|
-
and message["references"] is not None
|
|
246
|
-
and len(message["references"]) > 0
|
|
247
|
-
):
|
|
248
|
-
reminders.append(
|
|
249
|
-
render_template(
|
|
250
|
-
reminder_config.reminder,
|
|
251
|
-
display_name=self.display_name(),
|
|
252
|
-
tool_name=self.name,
|
|
253
|
-
)
|
|
254
|
-
)
|
|
255
|
-
elif (
|
|
256
|
-
reminder_config.type == SubAgentSystemReminderType.REGEXP
|
|
257
|
-
and message["text"] is not None
|
|
258
|
-
):
|
|
259
|
-
reminder_config = cast(
|
|
260
|
-
RegExpDetectedSystemReminderConfig, reminder_config
|
|
261
|
-
)
|
|
262
|
-
text_matches = [
|
|
263
|
-
match.group(0)
|
|
264
|
-
for match in reminder_config.regexp.finditer(message["text"])
|
|
265
|
-
]
|
|
266
|
-
if len(text_matches) > 0:
|
|
267
|
-
reminders.append(
|
|
268
|
-
render_template(
|
|
269
|
-
reminder_config.reminder,
|
|
270
|
-
display_name=self.display_name(),
|
|
271
|
-
tool_name=self.name,
|
|
272
|
-
text_matches=text_matches,
|
|
273
|
-
)
|
|
274
|
-
)
|
|
275
|
-
|
|
276
|
-
return reminders
|
|
277
|
-
|
|
278
256
|
async def _get_chat_id(self) -> str | None:
|
|
279
257
|
if not self.config.reuse_chat:
|
|
280
258
|
return None
|
|
@@ -290,23 +268,6 @@ class SubAgentTool(Tool[SubAgentToolConfig]):
|
|
|
290
268
|
|
|
291
269
|
return None
|
|
292
270
|
|
|
293
|
-
def _prepare_response_references(self, response: str, sequence_number: int) -> str:
|
|
294
|
-
if not self.config.use_sub_agent_references:
|
|
295
|
-
# Remove all references from the response
|
|
296
|
-
response = remove_all_refs(response)
|
|
297
|
-
return response
|
|
298
|
-
|
|
299
|
-
for ref_number in get_all_ref_numbers(response):
|
|
300
|
-
reference = self.get_sub_agent_reference_format(
|
|
301
|
-
name=self.name,
|
|
302
|
-
sequence_number=sequence_number,
|
|
303
|
-
reference_number=ref_number,
|
|
304
|
-
)
|
|
305
|
-
response = replace_ref_number(
|
|
306
|
-
text=response, ref_number=ref_number, replacement=reference
|
|
307
|
-
)
|
|
308
|
-
return response
|
|
309
|
-
|
|
310
271
|
async def _save_chat_id(self, chat_id: str) -> None:
|
|
311
272
|
if not self.config.reuse_chat:
|
|
312
273
|
return
|
|
@@ -390,4 +351,93 @@ def _format_response(tool_name: str, text: str, system_reminders: list[str]) ->
|
|
|
390
351
|
return json.dumps(response, indent=2)
|
|
391
352
|
|
|
392
353
|
|
|
354
|
+
def _response_has_refs(response: unique_sdk.Space.Message) -> bool:
|
|
355
|
+
if (
|
|
356
|
+
response["text"] is None
|
|
357
|
+
or response["references"] is None
|
|
358
|
+
or len(response["references"]) == 0
|
|
359
|
+
):
|
|
360
|
+
return False
|
|
361
|
+
|
|
362
|
+
for ref in response["references"]:
|
|
363
|
+
if (
|
|
364
|
+
re.search(
|
|
365
|
+
get_detection_pattern_for_ref(ref["sequenceNumber"]), response["text"]
|
|
366
|
+
)
|
|
367
|
+
is not None
|
|
368
|
+
):
|
|
369
|
+
return True
|
|
370
|
+
|
|
371
|
+
return False
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _remove_extra_refs(response: str, refs: list[unique_sdk.Space.Reference]) -> str:
|
|
375
|
+
text_ref_numbers = set(get_all_ref_numbers(response))
|
|
376
|
+
extra_ref_numbers = text_ref_numbers - set(ref["sequenceNumber"] for ref in refs)
|
|
377
|
+
|
|
378
|
+
for ref_num in extra_ref_numbers:
|
|
379
|
+
response = get_detection_pattern_for_ref(ref_num).sub("", response)
|
|
380
|
+
|
|
381
|
+
return response
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _prepare_sub_agent_response_refs(
|
|
385
|
+
response: str,
|
|
386
|
+
name: str,
|
|
387
|
+
sequence_number: int,
|
|
388
|
+
refs: list[unique_sdk.Space.Reference],
|
|
389
|
+
) -> str:
|
|
390
|
+
for ref in refs:
|
|
391
|
+
ref_number = ref["sequenceNumber"]
|
|
392
|
+
reference = SubAgentTool.get_sub_agent_reference_format(
|
|
393
|
+
name=name, sequence_number=sequence_number, reference_number=ref_number
|
|
394
|
+
)
|
|
395
|
+
response = replace_ref_number(
|
|
396
|
+
text=response, ref_number=ref_number, replacement=reference
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
return response
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _get_sub_agent_system_reminders(
|
|
403
|
+
response: str,
|
|
404
|
+
configs: list[SystemReminderConfigType],
|
|
405
|
+
name: str,
|
|
406
|
+
display_name: str,
|
|
407
|
+
sequence_number: int,
|
|
408
|
+
has_refs: bool,
|
|
409
|
+
) -> list[str]:
|
|
410
|
+
reminders = []
|
|
411
|
+
|
|
412
|
+
for reminder_config in configs:
|
|
413
|
+
render_kwargs = {}
|
|
414
|
+
render_kwargs["display_name"] = display_name
|
|
415
|
+
render_kwargs["tool_name"] = name
|
|
416
|
+
template = None
|
|
417
|
+
|
|
418
|
+
if reminder_config.type == SubAgentSystemReminderType.FIXED:
|
|
419
|
+
template = reminder_config.reminder
|
|
420
|
+
elif (
|
|
421
|
+
reminder_config.type == SubAgentSystemReminderType.REFERENCE and has_refs
|
|
422
|
+
) or (
|
|
423
|
+
reminder_config.type == SubAgentSystemReminderType.NO_REFERENCE
|
|
424
|
+
and not has_refs
|
|
425
|
+
):
|
|
426
|
+
render_kwargs["tool_name"] = f"{name} {sequence_number}"
|
|
427
|
+
template = reminder_config.reminder
|
|
428
|
+
elif reminder_config.type == SubAgentSystemReminderType.REGEXP:
|
|
429
|
+
reminder_config = cast(RegExpDetectedSystemReminderConfig, reminder_config)
|
|
430
|
+
text_matches = [
|
|
431
|
+
match.group(0) for match in reminder_config.regexp.finditer(response)
|
|
432
|
+
]
|
|
433
|
+
if len(text_matches) > 0:
|
|
434
|
+
template = reminder_config.reminder
|
|
435
|
+
render_kwargs["text_matches"] = text_matches
|
|
436
|
+
|
|
437
|
+
if template is not None:
|
|
438
|
+
reminders.append(render_template(template, **render_kwargs))
|
|
439
|
+
|
|
440
|
+
return reminders
|
|
441
|
+
|
|
442
|
+
|
|
393
443
|
ToolFactory.register_tool(SubAgentTool, SubAgentToolConfig)
|
|
@@ -0,0 +1,829 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for utility functions in service.py for SubAgentTool.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from unique_toolkit.agentic.tools.a2a.tool.config import (
|
|
12
|
+
FixedSystemReminderConfig,
|
|
13
|
+
NoReferenceSystemReminderConfig,
|
|
14
|
+
ReferenceSystemReminderConfig,
|
|
15
|
+
RegExpDetectedSystemReminderConfig,
|
|
16
|
+
)
|
|
17
|
+
from unique_toolkit.agentic.tools.a2a.tool.service import (
|
|
18
|
+
_format_response,
|
|
19
|
+
_get_sub_agent_system_reminders,
|
|
20
|
+
_prepare_sub_agent_response_refs,
|
|
21
|
+
_remove_extra_refs,
|
|
22
|
+
_response_has_refs,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# Fixtures
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@pytest.fixture
|
|
29
|
+
def base_message_response() -> dict[str, Any]:
|
|
30
|
+
"""Base message response fixture mimicking unique_sdk.Space.Message."""
|
|
31
|
+
return {
|
|
32
|
+
"text": "Some response text",
|
|
33
|
+
"references": None,
|
|
34
|
+
"chatId": "chat-123",
|
|
35
|
+
"assessment": None,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.fixture
|
|
40
|
+
def message_with_refs() -> dict[str, Any]:
|
|
41
|
+
"""Message response with references that appear in text."""
|
|
42
|
+
return {
|
|
43
|
+
"text": "This is the answer <sup>1</sup> and also <sup>2</sup>",
|
|
44
|
+
"references": [
|
|
45
|
+
{"sequenceNumber": 1, "name": "Doc 1", "url": "http://example.com/1"},
|
|
46
|
+
{"sequenceNumber": 2, "name": "Doc 2", "url": "http://example.com/2"},
|
|
47
|
+
],
|
|
48
|
+
"chatId": "chat-123",
|
|
49
|
+
"assessment": None,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@pytest.fixture
|
|
54
|
+
def refs_list() -> list[dict[str, Any]]:
|
|
55
|
+
"""List of reference objects mimicking unique_sdk.Space.Reference."""
|
|
56
|
+
return [
|
|
57
|
+
{"sequenceNumber": 1, "name": "Doc 1", "url": "http://example.com/1"},
|
|
58
|
+
{"sequenceNumber": 2, "name": "Doc 2", "url": "http://example.com/2"},
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# Tests for _format_response
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@pytest.mark.ai
|
|
66
|
+
def test_format_response__returns_text__when_no_system_reminders() -> None:
|
|
67
|
+
"""
|
|
68
|
+
Purpose: Verify _format_response returns plain text when system_reminders is empty.
|
|
69
|
+
Why this matters: Avoids unnecessary JSON wrapping for simple responses.
|
|
70
|
+
Setup summary: Empty system_reminders list, assert text returned unchanged.
|
|
71
|
+
"""
|
|
72
|
+
# Arrange
|
|
73
|
+
tool_name = "TestTool"
|
|
74
|
+
text = "This is the response text"
|
|
75
|
+
system_reminders: list[str] = []
|
|
76
|
+
|
|
77
|
+
# Act
|
|
78
|
+
result = _format_response(tool_name, text, system_reminders)
|
|
79
|
+
|
|
80
|
+
# Assert
|
|
81
|
+
assert result == text
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@pytest.mark.ai
|
|
85
|
+
def test_format_response__returns_json__when_system_reminders_present() -> None:
|
|
86
|
+
"""
|
|
87
|
+
Purpose: Verify _format_response returns JSON with reminders when reminders present.
|
|
88
|
+
Why this matters: Ensures system reminders are properly included for LLM context.
|
|
89
|
+
Setup summary: Provide system reminders, assert JSON structure with reminders key.
|
|
90
|
+
"""
|
|
91
|
+
# Arrange
|
|
92
|
+
tool_name = "TestTool"
|
|
93
|
+
text = "This is the response"
|
|
94
|
+
system_reminders = ["Remember to cite sources", "Be concise"]
|
|
95
|
+
|
|
96
|
+
# Act
|
|
97
|
+
result = _format_response(tool_name, text, system_reminders)
|
|
98
|
+
|
|
99
|
+
# Assert
|
|
100
|
+
parsed = json.loads(result)
|
|
101
|
+
assert "TestTool response" in parsed
|
|
102
|
+
assert parsed["TestTool response"] == text
|
|
103
|
+
assert "SYSTEM_REMINDERS" in parsed
|
|
104
|
+
assert parsed["SYSTEM_REMINDERS"] == system_reminders
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@pytest.mark.ai
|
|
108
|
+
def test_format_response__uses_tool_name_in_key__correctly() -> None:
|
|
109
|
+
"""
|
|
110
|
+
Purpose: Verify the response key includes the tool name.
|
|
111
|
+
Why this matters: Allows LLM to identify which tool the response belongs to.
|
|
112
|
+
Setup summary: Use specific tool name, verify it appears in JSON key.
|
|
113
|
+
"""
|
|
114
|
+
# Arrange
|
|
115
|
+
tool_name = "MySpecialAgent"
|
|
116
|
+
text = "Response content"
|
|
117
|
+
system_reminders = ["Reminder 1"]
|
|
118
|
+
|
|
119
|
+
# Act
|
|
120
|
+
result = _format_response(tool_name, text, system_reminders)
|
|
121
|
+
|
|
122
|
+
# Assert
|
|
123
|
+
parsed = json.loads(result)
|
|
124
|
+
assert "MySpecialAgent response" in parsed
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@pytest.mark.ai
|
|
128
|
+
def test_format_response__preserves_special_characters__in_text() -> None:
|
|
129
|
+
"""
|
|
130
|
+
Purpose: Verify special characters in text are preserved in JSON output.
|
|
131
|
+
Why this matters: Ensures markdown, code, and special chars don't break formatting.
|
|
132
|
+
Setup summary: Text with special JSON chars, verify they're properly escaped.
|
|
133
|
+
"""
|
|
134
|
+
# Arrange
|
|
135
|
+
tool_name = "TestTool"
|
|
136
|
+
text = 'Text with "quotes" and newlines\nand tabs\t'
|
|
137
|
+
system_reminders = ["Reminder"]
|
|
138
|
+
|
|
139
|
+
# Act
|
|
140
|
+
result = _format_response(tool_name, text, system_reminders)
|
|
141
|
+
|
|
142
|
+
# Assert
|
|
143
|
+
parsed = json.loads(result)
|
|
144
|
+
assert parsed["TestTool response"] == text
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# Tests for _response_has_refs
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@pytest.mark.ai
|
|
151
|
+
def test_response_has_refs__returns_false__when_text_is_none() -> None:
|
|
152
|
+
"""
|
|
153
|
+
Purpose: Verify _response_has_refs returns False when text is None.
|
|
154
|
+
Why this matters: Handles edge case of empty/null responses gracefully.
|
|
155
|
+
Setup summary: Response with None text, assert False returned.
|
|
156
|
+
"""
|
|
157
|
+
# Arrange
|
|
158
|
+
response = {"text": None, "references": [{"sequenceNumber": 1}]}
|
|
159
|
+
|
|
160
|
+
# Act
|
|
161
|
+
result = _response_has_refs(response)
|
|
162
|
+
|
|
163
|
+
# Assert
|
|
164
|
+
assert result is False
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@pytest.mark.ai
|
|
168
|
+
def test_response_has_refs__returns_false__when_references_is_none() -> None:
|
|
169
|
+
"""
|
|
170
|
+
Purpose: Verify _response_has_refs returns False when references is None.
|
|
171
|
+
Why this matters: Handles missing references attribute gracefully.
|
|
172
|
+
Setup summary: Response with None references, assert False returned.
|
|
173
|
+
"""
|
|
174
|
+
# Arrange
|
|
175
|
+
response = {"text": "Some text <sup>1</sup>", "references": None}
|
|
176
|
+
|
|
177
|
+
# Act
|
|
178
|
+
result = _response_has_refs(response)
|
|
179
|
+
|
|
180
|
+
# Assert
|
|
181
|
+
assert result is False
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@pytest.mark.ai
|
|
185
|
+
def test_response_has_refs__returns_false__when_references_empty() -> None:
|
|
186
|
+
"""
|
|
187
|
+
Purpose: Verify _response_has_refs returns False when references list is empty.
|
|
188
|
+
Why this matters: Distinguishes between missing and empty reference lists.
|
|
189
|
+
Setup summary: Response with empty references list, assert False returned.
|
|
190
|
+
"""
|
|
191
|
+
# Arrange
|
|
192
|
+
response = {"text": "Some text <sup>1</sup>", "references": []}
|
|
193
|
+
|
|
194
|
+
# Act
|
|
195
|
+
result = _response_has_refs(response)
|
|
196
|
+
|
|
197
|
+
# Assert
|
|
198
|
+
assert result is False
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@pytest.mark.ai
|
|
202
|
+
def test_response_has_refs__returns_true__when_ref_found_in_text(
|
|
203
|
+
message_with_refs: dict[str, Any],
|
|
204
|
+
) -> None:
|
|
205
|
+
"""
|
|
206
|
+
Purpose: Verify _response_has_refs returns True when reference appears in text.
|
|
207
|
+
Why this matters: Core functionality for detecting valid references.
|
|
208
|
+
Setup summary: Response with refs that appear in text, assert True returned.
|
|
209
|
+
"""
|
|
210
|
+
# Act
|
|
211
|
+
result = _response_has_refs(message_with_refs)
|
|
212
|
+
|
|
213
|
+
# Assert
|
|
214
|
+
assert result is True
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@pytest.mark.ai
|
|
218
|
+
def test_response_has_refs__returns_false__when_refs_not_in_text() -> None:
|
|
219
|
+
"""
|
|
220
|
+
Purpose: Verify _response_has_refs returns False when references don't appear in text.
|
|
221
|
+
Why this matters: Ensures we only process actually-used references.
|
|
222
|
+
Setup summary: Response with refs that don't match text, assert False returned.
|
|
223
|
+
"""
|
|
224
|
+
# Arrange
|
|
225
|
+
response = {
|
|
226
|
+
"text": "Text without any sup tags",
|
|
227
|
+
"references": [{"sequenceNumber": 1}, {"sequenceNumber": 2}],
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
# Act
|
|
231
|
+
result = _response_has_refs(response)
|
|
232
|
+
|
|
233
|
+
# Assert
|
|
234
|
+
assert result is False
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@pytest.mark.ai
|
|
238
|
+
def test_response_has_refs__handles_whitespace_in_refs__correctly() -> None:
|
|
239
|
+
"""
|
|
240
|
+
Purpose: Verify _response_has_refs handles whitespace inside sup tags.
|
|
241
|
+
Why this matters: Reference patterns may have varying whitespace.
|
|
242
|
+
Setup summary: Text with whitespace in sup tags, assert still detected.
|
|
243
|
+
"""
|
|
244
|
+
# Arrange
|
|
245
|
+
response = {
|
|
246
|
+
"text": "Text with <sup> 1 </sup> reference",
|
|
247
|
+
"references": [{"sequenceNumber": 1}],
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
# Act
|
|
251
|
+
result = _response_has_refs(response)
|
|
252
|
+
|
|
253
|
+
# Assert
|
|
254
|
+
assert result is True
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@pytest.mark.ai
|
|
258
|
+
def test_response_has_refs__returns_true__when_at_least_one_ref_matches() -> None:
|
|
259
|
+
"""
|
|
260
|
+
Purpose: Verify returns True if any reference matches, not necessarily all.
|
|
261
|
+
Why this matters: Partial reference usage is still valid.
|
|
262
|
+
Setup summary: Multiple refs where only one appears in text, assert True.
|
|
263
|
+
"""
|
|
264
|
+
# Arrange
|
|
265
|
+
response = {
|
|
266
|
+
"text": "Only first ref <sup>1</sup> is used",
|
|
267
|
+
"references": [
|
|
268
|
+
{"sequenceNumber": 1},
|
|
269
|
+
{"sequenceNumber": 2},
|
|
270
|
+
{"sequenceNumber": 3},
|
|
271
|
+
],
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
# Act
|
|
275
|
+
result = _response_has_refs(response)
|
|
276
|
+
|
|
277
|
+
# Assert
|
|
278
|
+
assert result is True
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
# Tests for _remove_extra_refs
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
@pytest.mark.ai
|
|
285
|
+
def test_remove_extra_refs__returns_unchanged__when_no_refs_in_text() -> None:
|
|
286
|
+
"""
|
|
287
|
+
Purpose: Verify _remove_extra_refs returns text unchanged when no refs present.
|
|
288
|
+
Why this matters: Avoids unnecessary string manipulation.
|
|
289
|
+
Setup summary: Plain text without refs, assert unchanged.
|
|
290
|
+
"""
|
|
291
|
+
# Arrange
|
|
292
|
+
response = "This is plain text without references"
|
|
293
|
+
refs: list[dict[str, Any]] = []
|
|
294
|
+
|
|
295
|
+
# Act
|
|
296
|
+
result = _remove_extra_refs(response, refs)
|
|
297
|
+
|
|
298
|
+
# Assert
|
|
299
|
+
assert result == response
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
@pytest.mark.ai
|
|
303
|
+
def test_remove_extra_refs__removes_orphan_refs__not_in_refs_list() -> None:
|
|
304
|
+
"""
|
|
305
|
+
Purpose: Verify _remove_extra_refs removes refs that aren't in the refs list.
|
|
306
|
+
Why this matters: Cleans up invalid/orphan reference markers.
|
|
307
|
+
Setup summary: Text with refs not in refs list, verify they're removed.
|
|
308
|
+
"""
|
|
309
|
+
# Arrange
|
|
310
|
+
response = "Text with <sup>1</sup> and <sup>2</sup> and <sup>3</sup>"
|
|
311
|
+
refs = [{"sequenceNumber": 1}] # Only ref 1 is valid
|
|
312
|
+
|
|
313
|
+
# Act
|
|
314
|
+
result = _remove_extra_refs(response, refs)
|
|
315
|
+
|
|
316
|
+
# Assert
|
|
317
|
+
assert "<sup>1</sup>" in result
|
|
318
|
+
assert "<sup>2</sup>" not in result
|
|
319
|
+
assert "<sup>3</sup>" not in result
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@pytest.mark.ai
|
|
323
|
+
def test_remove_extra_refs__preserves_valid_refs__in_text(
|
|
324
|
+
refs_list: list[dict[str, Any]],
|
|
325
|
+
) -> None:
|
|
326
|
+
"""
|
|
327
|
+
Purpose: Verify _remove_extra_refs preserves refs that are in the refs list.
|
|
328
|
+
Why this matters: Ensures valid references remain intact.
|
|
329
|
+
Setup summary: Text with valid refs, verify all preserved.
|
|
330
|
+
"""
|
|
331
|
+
# Arrange
|
|
332
|
+
response = "First <sup>1</sup> and second <sup>2</sup>"
|
|
333
|
+
|
|
334
|
+
# Act
|
|
335
|
+
result = _remove_extra_refs(response, refs_list)
|
|
336
|
+
|
|
337
|
+
# Assert
|
|
338
|
+
assert result == response
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
@pytest.mark.ai
|
|
342
|
+
def test_remove_extra_refs__removes_all_refs__when_refs_list_empty() -> None:
|
|
343
|
+
"""
|
|
344
|
+
Purpose: Verify all refs removed when refs list is empty.
|
|
345
|
+
Why this matters: Cleans up all references when none are valid.
|
|
346
|
+
Setup summary: Text with refs, empty refs list, verify all removed.
|
|
347
|
+
"""
|
|
348
|
+
# Arrange
|
|
349
|
+
response = "Has <sup>1</sup> and <sup>2</sup>"
|
|
350
|
+
refs: list[dict[str, Any]] = []
|
|
351
|
+
|
|
352
|
+
# Act
|
|
353
|
+
result = _remove_extra_refs(response, refs)
|
|
354
|
+
|
|
355
|
+
# Assert
|
|
356
|
+
assert "<sup>" not in result
|
|
357
|
+
assert result == "Has and "
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
@pytest.mark.ai
|
|
361
|
+
def test_remove_extra_refs__handles_multiple_occurrences__of_same_ref() -> None:
|
|
362
|
+
"""
|
|
363
|
+
Purpose: Verify removes all occurrences of an orphan ref.
|
|
364
|
+
Why this matters: Ensures complete cleanup of invalid refs.
|
|
365
|
+
Setup summary: Text with repeated orphan ref, verify all instances removed.
|
|
366
|
+
"""
|
|
367
|
+
# Arrange
|
|
368
|
+
response = "First <sup>3</sup> middle <sup>3</sup> end <sup>3</sup>"
|
|
369
|
+
refs = [{"sequenceNumber": 1}] # Ref 3 is orphan
|
|
370
|
+
|
|
371
|
+
# Act
|
|
372
|
+
result = _remove_extra_refs(response, refs)
|
|
373
|
+
|
|
374
|
+
# Assert
|
|
375
|
+
assert "<sup>3</sup>" not in result
|
|
376
|
+
assert result == "First middle end "
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
# Tests for _prepare_sub_agent_response_refs
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
@pytest.mark.ai
|
|
383
|
+
def test_prepare_sub_agent_response_refs__replaces_ref_format__correctly() -> None:
|
|
384
|
+
"""
|
|
385
|
+
Purpose: Verify refs are replaced with sub-agent format.
|
|
386
|
+
Why this matters: Core functionality for sub-agent reference attribution.
|
|
387
|
+
Setup summary: Text with standard refs, verify replaced with named format.
|
|
388
|
+
"""
|
|
389
|
+
# Arrange
|
|
390
|
+
response = "Check this <sup>1</sup> reference"
|
|
391
|
+
name = "ResearchAgent"
|
|
392
|
+
sequence_number = 1
|
|
393
|
+
refs = [{"sequenceNumber": 1}]
|
|
394
|
+
|
|
395
|
+
# Act
|
|
396
|
+
result = _prepare_sub_agent_response_refs(response, name, sequence_number, refs)
|
|
397
|
+
|
|
398
|
+
# Assert
|
|
399
|
+
assert "<sup>1</sup>" not in result
|
|
400
|
+
assert "<sup><name>ResearchAgent 1</name>1</sup>" in result
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
@pytest.mark.ai
|
|
404
|
+
def test_prepare_sub_agent_response_refs__handles_multiple_refs__correctly() -> None:
|
|
405
|
+
"""
|
|
406
|
+
Purpose: Verify multiple refs are all properly formatted.
|
|
407
|
+
Why this matters: Ensures batch processing of references works.
|
|
408
|
+
Setup summary: Text with multiple refs, verify all replaced correctly.
|
|
409
|
+
"""
|
|
410
|
+
# Arrange
|
|
411
|
+
response = "First <sup>1</sup> and second <sup>2</sup>"
|
|
412
|
+
name = "TestAgent"
|
|
413
|
+
sequence_number = 3
|
|
414
|
+
refs = [{"sequenceNumber": 1}, {"sequenceNumber": 2}]
|
|
415
|
+
|
|
416
|
+
# Act
|
|
417
|
+
result = _prepare_sub_agent_response_refs(response, name, sequence_number, refs)
|
|
418
|
+
|
|
419
|
+
# Assert
|
|
420
|
+
assert "<sup><name>TestAgent 3</name>1</sup>" in result
|
|
421
|
+
assert "<sup><name>TestAgent 3</name>2</sup>" in result
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
@pytest.mark.ai
|
|
425
|
+
def test_prepare_sub_agent_response_refs__preserves_text__around_refs() -> None:
|
|
426
|
+
"""
|
|
427
|
+
Purpose: Verify surrounding text is preserved when refs are replaced.
|
|
428
|
+
Why this matters: Ensures reference replacement doesn't corrupt text.
|
|
429
|
+
Setup summary: Text with refs, verify non-ref content unchanged.
|
|
430
|
+
"""
|
|
431
|
+
# Arrange
|
|
432
|
+
response = "Start text <sup>1</sup> middle text <sup>2</sup> end text"
|
|
433
|
+
name = "Agent"
|
|
434
|
+
sequence_number = 1
|
|
435
|
+
refs = [{"sequenceNumber": 1}, {"sequenceNumber": 2}]
|
|
436
|
+
|
|
437
|
+
# Act
|
|
438
|
+
result = _prepare_sub_agent_response_refs(response, name, sequence_number, refs)
|
|
439
|
+
|
|
440
|
+
# Assert
|
|
441
|
+
assert "Start text" in result
|
|
442
|
+
assert "middle text" in result
|
|
443
|
+
assert "end text" in result
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
@pytest.mark.ai
|
|
447
|
+
def test_prepare_sub_agent_response_refs__returns_unchanged__when_no_refs() -> None:
|
|
448
|
+
"""
|
|
449
|
+
Purpose: Verify text unchanged when refs list is empty.
|
|
450
|
+
Why this matters: Handles edge case of no references gracefully.
|
|
451
|
+
Setup summary: Empty refs list, verify text unchanged.
|
|
452
|
+
"""
|
|
453
|
+
# Arrange
|
|
454
|
+
response = "Text without any references"
|
|
455
|
+
name = "Agent"
|
|
456
|
+
sequence_number = 1
|
|
457
|
+
refs: list[dict[str, Any]] = []
|
|
458
|
+
|
|
459
|
+
# Act
|
|
460
|
+
result = _prepare_sub_agent_response_refs(response, name, sequence_number, refs)
|
|
461
|
+
|
|
462
|
+
# Assert
|
|
463
|
+
assert result == response
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
@pytest.mark.ai
|
|
467
|
+
def test_prepare_sub_agent_response_refs__uses_correct_sequence_number__in_format() -> (
|
|
468
|
+
None
|
|
469
|
+
):
|
|
470
|
+
"""
|
|
471
|
+
Purpose: Verify the sequence number is correctly used in the formatted reference.
|
|
472
|
+
Why this matters: Sequence number identifies the sub-agent call instance.
|
|
473
|
+
Setup summary: Specific sequence number, verify it appears in output format.
|
|
474
|
+
"""
|
|
475
|
+
# Arrange
|
|
476
|
+
response = "Ref <sup>1</sup>"
|
|
477
|
+
name = "MyAgent"
|
|
478
|
+
sequence_number = 42
|
|
479
|
+
refs = [{"sequenceNumber": 1}]
|
|
480
|
+
|
|
481
|
+
# Act
|
|
482
|
+
result = _prepare_sub_agent_response_refs(response, name, sequence_number, refs)
|
|
483
|
+
|
|
484
|
+
# Assert
|
|
485
|
+
assert "<name>MyAgent 42</name>" in result
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
# Tests for _get_sub_agent_system_reminders
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
@pytest.mark.ai
|
|
492
|
+
def test_get_sub_agent_system_reminders__returns_empty__when_no_configs() -> None:
|
|
493
|
+
"""
|
|
494
|
+
Purpose: Verify returns empty list when no reminder configs provided.
|
|
495
|
+
Why this matters: Handles no-config case gracefully.
|
|
496
|
+
Setup summary: Empty configs list, assert empty result.
|
|
497
|
+
"""
|
|
498
|
+
# Arrange
|
|
499
|
+
response = "Some response"
|
|
500
|
+
configs: list[Any] = []
|
|
501
|
+
name = "TestAgent"
|
|
502
|
+
display_name = "Test Agent"
|
|
503
|
+
sequence_number = 1
|
|
504
|
+
has_refs = False
|
|
505
|
+
|
|
506
|
+
# Act
|
|
507
|
+
result = _get_sub_agent_system_reminders(
|
|
508
|
+
response, configs, name, display_name, sequence_number, has_refs
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
# Assert
|
|
512
|
+
assert result == []
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
@pytest.mark.ai
|
|
516
|
+
def test_get_sub_agent_system_reminders__adds_fixed_reminder__always() -> None:
|
|
517
|
+
"""
|
|
518
|
+
Purpose: Verify FIXED type reminders are always added.
|
|
519
|
+
Why this matters: FIXED reminders should appear regardless of response content.
|
|
520
|
+
Setup summary: FIXED config, verify reminder added.
|
|
521
|
+
"""
|
|
522
|
+
# Arrange
|
|
523
|
+
response = "Any response"
|
|
524
|
+
configs = [FixedSystemReminderConfig(reminder="Always show this reminder")]
|
|
525
|
+
name = "TestAgent"
|
|
526
|
+
display_name = "Test Agent"
|
|
527
|
+
sequence_number = 1
|
|
528
|
+
has_refs = False
|
|
529
|
+
|
|
530
|
+
# Act
|
|
531
|
+
result = _get_sub_agent_system_reminders(
|
|
532
|
+
response, configs, name, display_name, sequence_number, has_refs
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
# Assert
|
|
536
|
+
assert len(result) == 1
|
|
537
|
+
assert "Always show this reminder" in result[0]
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
@pytest.mark.ai
|
|
541
|
+
def test_get_sub_agent_system_reminders__adds_reference_reminder__when_has_refs() -> (
|
|
542
|
+
None
|
|
543
|
+
):
|
|
544
|
+
"""
|
|
545
|
+
Purpose: Verify REFERENCE type reminder added when has_refs is True.
|
|
546
|
+
Why this matters: Reference reminders help LLM cite sources correctly.
|
|
547
|
+
Setup summary: REFERENCE config with has_refs=True, verify reminder added.
|
|
548
|
+
"""
|
|
549
|
+
# Arrange
|
|
550
|
+
response = "Response with refs"
|
|
551
|
+
configs = [ReferenceSystemReminderConfig()]
|
|
552
|
+
name = "TestAgent"
|
|
553
|
+
display_name = "Test Agent"
|
|
554
|
+
sequence_number = 1
|
|
555
|
+
has_refs = True
|
|
556
|
+
|
|
557
|
+
# Act
|
|
558
|
+
result = _get_sub_agent_system_reminders(
|
|
559
|
+
response, configs, name, display_name, sequence_number, has_refs
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
# Assert
|
|
563
|
+
assert len(result) == 1
|
|
564
|
+
assert "Test Agent" in result[0] # display_name should be rendered
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
@pytest.mark.ai
|
|
568
|
+
def test_get_sub_agent_system_reminders__skips_reference_reminder__when_no_refs() -> (
|
|
569
|
+
None
|
|
570
|
+
):
|
|
571
|
+
"""
|
|
572
|
+
Purpose: Verify REFERENCE reminder skipped when has_refs is False.
|
|
573
|
+
Why this matters: Avoids irrelevant reminders about citations.
|
|
574
|
+
Setup summary: REFERENCE config with has_refs=False, verify not added.
|
|
575
|
+
"""
|
|
576
|
+
# Arrange
|
|
577
|
+
response = "Response without refs"
|
|
578
|
+
configs = [ReferenceSystemReminderConfig()]
|
|
579
|
+
name = "TestAgent"
|
|
580
|
+
display_name = "Test Agent"
|
|
581
|
+
sequence_number = 1
|
|
582
|
+
has_refs = False
|
|
583
|
+
|
|
584
|
+
# Act
|
|
585
|
+
result = _get_sub_agent_system_reminders(
|
|
586
|
+
response, configs, name, display_name, sequence_number, has_refs
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
# Assert
|
|
590
|
+
assert result == []
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
@pytest.mark.ai
|
|
594
|
+
def test_get_sub_agent_system_reminders__adds_no_reference_reminder__when_no_refs() -> (
|
|
595
|
+
None
|
|
596
|
+
):
|
|
597
|
+
"""
|
|
598
|
+
Purpose: Verify NO_REFERENCE reminder added when has_refs is False.
|
|
599
|
+
Why this matters: Warns LLM not to fabricate citations.
|
|
600
|
+
Setup summary: NO_REFERENCE config with has_refs=False, verify added.
|
|
601
|
+
"""
|
|
602
|
+
# Arrange
|
|
603
|
+
response = "Response without refs"
|
|
604
|
+
configs = [NoReferenceSystemReminderConfig()]
|
|
605
|
+
name = "TestAgent"
|
|
606
|
+
display_name = "Test Agent"
|
|
607
|
+
sequence_number = 1
|
|
608
|
+
has_refs = False
|
|
609
|
+
|
|
610
|
+
# Act
|
|
611
|
+
result = _get_sub_agent_system_reminders(
|
|
612
|
+
response, configs, name, display_name, sequence_number, has_refs
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
# Assert
|
|
616
|
+
assert len(result) == 1
|
|
617
|
+
assert "NOT" in result[0] # Default reminder contains "Do NOT"
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
@pytest.mark.ai
|
|
621
|
+
def test_get_sub_agent_system_reminders__skips_no_reference_reminder__when_has_refs() -> (
|
|
622
|
+
None
|
|
623
|
+
):
|
|
624
|
+
"""
|
|
625
|
+
Purpose: Verify NO_REFERENCE reminder skipped when has_refs is True.
|
|
626
|
+
Why this matters: Avoids contradictory instructions when refs exist.
|
|
627
|
+
Setup summary: NO_REFERENCE config with has_refs=True, verify not added.
|
|
628
|
+
"""
|
|
629
|
+
# Arrange
|
|
630
|
+
response = "Response with refs"
|
|
631
|
+
configs = [NoReferenceSystemReminderConfig()]
|
|
632
|
+
name = "TestAgent"
|
|
633
|
+
display_name = "Test Agent"
|
|
634
|
+
sequence_number = 1
|
|
635
|
+
has_refs = True
|
|
636
|
+
|
|
637
|
+
# Act
|
|
638
|
+
result = _get_sub_agent_system_reminders(
|
|
639
|
+
response, configs, name, display_name, sequence_number, has_refs
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
# Assert
|
|
643
|
+
assert result == []
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
@pytest.mark.ai
|
|
647
|
+
def test_get_sub_agent_system_reminders__adds_regexp_reminder__when_pattern_matches() -> (
|
|
648
|
+
None
|
|
649
|
+
):
|
|
650
|
+
"""
|
|
651
|
+
Purpose: Verify REGEXP reminder added when pattern matches response.
|
|
652
|
+
Why this matters: Enables conditional reminders based on response content.
|
|
653
|
+
Setup summary: REGEXP config matching response, verify reminder added.
|
|
654
|
+
"""
|
|
655
|
+
# Arrange
|
|
656
|
+
response = "This response contains IMPORTANT_KEYWORD here"
|
|
657
|
+
configs = [
|
|
658
|
+
RegExpDetectedSystemReminderConfig(
|
|
659
|
+
regexp=re.compile(r"IMPORTANT_KEYWORD"),
|
|
660
|
+
reminder="Found keyword: {{ text_matches }}",
|
|
661
|
+
)
|
|
662
|
+
]
|
|
663
|
+
name = "TestAgent"
|
|
664
|
+
display_name = "Test Agent"
|
|
665
|
+
sequence_number = 1
|
|
666
|
+
has_refs = False
|
|
667
|
+
|
|
668
|
+
# Act
|
|
669
|
+
result = _get_sub_agent_system_reminders(
|
|
670
|
+
response, configs, name, display_name, sequence_number, has_refs
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
# Assert
|
|
674
|
+
assert len(result) == 1
|
|
675
|
+
assert "IMPORTANT_KEYWORD" in result[0]
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
@pytest.mark.ai
|
|
679
|
+
def test_get_sub_agent_system_reminders__skips_regexp_reminder__when_no_match() -> None:
|
|
680
|
+
"""
|
|
681
|
+
Purpose: Verify REGEXP reminder skipped when pattern doesn't match.
|
|
682
|
+
Why this matters: Avoids irrelevant conditional reminders.
|
|
683
|
+
Setup summary: REGEXP config not matching response, verify not added.
|
|
684
|
+
"""
|
|
685
|
+
# Arrange
|
|
686
|
+
response = "This response has no special keywords"
|
|
687
|
+
configs = [
|
|
688
|
+
RegExpDetectedSystemReminderConfig(
|
|
689
|
+
regexp=re.compile(r"NONEXISTENT_PATTERN"),
|
|
690
|
+
reminder="Should not appear",
|
|
691
|
+
)
|
|
692
|
+
]
|
|
693
|
+
name = "TestAgent"
|
|
694
|
+
display_name = "Test Agent"
|
|
695
|
+
sequence_number = 1
|
|
696
|
+
has_refs = False
|
|
697
|
+
|
|
698
|
+
# Act
|
|
699
|
+
result = _get_sub_agent_system_reminders(
|
|
700
|
+
response, configs, name, display_name, sequence_number, has_refs
|
|
701
|
+
)
|
|
702
|
+
|
|
703
|
+
# Assert
|
|
704
|
+
assert result == []
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
@pytest.mark.ai
|
|
708
|
+
def test_get_sub_agent_system_reminders__renders_jinja_template__correctly() -> None:
|
|
709
|
+
"""
|
|
710
|
+
Purpose: Verify Jinja templates in reminders are correctly rendered.
|
|
711
|
+
Why this matters: Templates allow dynamic reminder content.
|
|
712
|
+
Setup summary: FIXED config with template placeholders, verify rendered.
|
|
713
|
+
"""
|
|
714
|
+
# Arrange
|
|
715
|
+
response = "Some response"
|
|
716
|
+
configs = [
|
|
717
|
+
FixedSystemReminderConfig(
|
|
718
|
+
reminder="Tool {{ tool_name }} ({{ display_name }}) says hello"
|
|
719
|
+
)
|
|
720
|
+
]
|
|
721
|
+
name = "MyTool"
|
|
722
|
+
display_name = "My Special Tool"
|
|
723
|
+
sequence_number = 1
|
|
724
|
+
has_refs = False
|
|
725
|
+
|
|
726
|
+
# Act
|
|
727
|
+
result = _get_sub_agent_system_reminders(
|
|
728
|
+
response, configs, name, display_name, sequence_number, has_refs
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
# Assert
|
|
732
|
+
assert len(result) == 1
|
|
733
|
+
assert "Tool MyTool (My Special Tool) says hello" in result[0]
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
@pytest.mark.ai
|
|
737
|
+
def test_get_sub_agent_system_reminders__includes_sequence_in_tool_name__for_ref_types() -> (
|
|
738
|
+
None
|
|
739
|
+
):
|
|
740
|
+
"""
|
|
741
|
+
Purpose: Verify REFERENCE/NO_REFERENCE types include sequence number in tool_name.
|
|
742
|
+
Why this matters: Helps identify which sub-agent call produced the response.
|
|
743
|
+
Setup summary: REFERENCE config, verify tool_name includes sequence number.
|
|
744
|
+
"""
|
|
745
|
+
# Arrange
|
|
746
|
+
response = "Response"
|
|
747
|
+
configs = [ReferenceSystemReminderConfig(reminder="Cite {{ tool_name }} correctly")]
|
|
748
|
+
name = "Agent"
|
|
749
|
+
display_name = "Agent"
|
|
750
|
+
sequence_number = 5
|
|
751
|
+
has_refs = True
|
|
752
|
+
|
|
753
|
+
# Act
|
|
754
|
+
result = _get_sub_agent_system_reminders(
|
|
755
|
+
response, configs, name, display_name, sequence_number, has_refs
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
# Assert
|
|
759
|
+
assert len(result) == 1
|
|
760
|
+
assert "Agent 5" in result[0]
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
@pytest.mark.ai
|
|
764
|
+
def test_get_sub_agent_system_reminders__handles_multiple_configs__correctly() -> None:
|
|
765
|
+
"""
|
|
766
|
+
Purpose: Verify multiple configs are processed and applicable ones added.
|
|
767
|
+
Why this matters: Supports complex reminder configurations.
|
|
768
|
+
Setup summary: Multiple config types, verify correct ones added.
|
|
769
|
+
"""
|
|
770
|
+
# Arrange
|
|
771
|
+
response = "Response with SPECIAL text"
|
|
772
|
+
configs = [
|
|
773
|
+
FixedSystemReminderConfig(reminder="Fixed reminder"),
|
|
774
|
+
NoReferenceSystemReminderConfig(), # Should be added (has_refs=False)
|
|
775
|
+
ReferenceSystemReminderConfig(), # Should NOT be added (has_refs=False)
|
|
776
|
+
RegExpDetectedSystemReminderConfig(
|
|
777
|
+
regexp=re.compile(r"SPECIAL"),
|
|
778
|
+
reminder="Found special",
|
|
779
|
+
),
|
|
780
|
+
]
|
|
781
|
+
name = "TestAgent"
|
|
782
|
+
display_name = "Test Agent"
|
|
783
|
+
sequence_number = 1
|
|
784
|
+
has_refs = False
|
|
785
|
+
|
|
786
|
+
# Act
|
|
787
|
+
result = _get_sub_agent_system_reminders(
|
|
788
|
+
response, configs, name, display_name, sequence_number, has_refs
|
|
789
|
+
)
|
|
790
|
+
|
|
791
|
+
# Assert
|
|
792
|
+
assert len(result) == 3
|
|
793
|
+
assert any("Fixed reminder" in r for r in result)
|
|
794
|
+
assert any("NOT" in r for r in result) # NO_REFERENCE default
|
|
795
|
+
assert any("Found special" in r for r in result)
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
@pytest.mark.ai
|
|
799
|
+
def test_get_sub_agent_system_reminders__captures_all_regexp_matches__in_text_matches() -> (
|
|
800
|
+
None
|
|
801
|
+
):
|
|
802
|
+
"""
|
|
803
|
+
Purpose: Verify all regexp matches are captured in text_matches variable.
|
|
804
|
+
Why this matters: Allows templates to reference all matched content.
|
|
805
|
+
Setup summary: Response with multiple pattern matches, verify all captured.
|
|
806
|
+
"""
|
|
807
|
+
# Arrange
|
|
808
|
+
response = "Found CODE123 and CODE456 and CODE789"
|
|
809
|
+
configs = [
|
|
810
|
+
RegExpDetectedSystemReminderConfig(
|
|
811
|
+
regexp=re.compile(r"CODE\d+"),
|
|
812
|
+
reminder="Codes found: {{ text_matches | join(', ') }}",
|
|
813
|
+
)
|
|
814
|
+
]
|
|
815
|
+
name = "TestAgent"
|
|
816
|
+
display_name = "Test Agent"
|
|
817
|
+
sequence_number = 1
|
|
818
|
+
has_refs = False
|
|
819
|
+
|
|
820
|
+
# Act
|
|
821
|
+
result = _get_sub_agent_system_reminders(
|
|
822
|
+
response, configs, name, display_name, sequence_number, has_refs
|
|
823
|
+
)
|
|
824
|
+
|
|
825
|
+
# Assert
|
|
826
|
+
assert len(result) == 1
|
|
827
|
+
assert "CODE123" in result[0]
|
|
828
|
+
assert "CODE456" in result[0]
|
|
829
|
+
assert "CODE789" in result[0]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: unique_toolkit
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.36.0
|
|
4
4
|
Summary:
|
|
5
5
|
License: Proprietary
|
|
6
6
|
Author: Cedric Klinkert
|
|
@@ -121,6 +121,9 @@ All notable changes to this project will be documented in this file.
|
|
|
121
121
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
122
122
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
123
123
|
|
|
124
|
+
## [1.36.0] - 2025-12-11
|
|
125
|
+
- Add support for a sub agent tool system reminder when no references are present in the sub agent response.
|
|
126
|
+
|
|
124
127
|
## [1.35.4] - 2025-12-10
|
|
125
128
|
- Fix a potential stacktrace leak
|
|
126
129
|
|
|
@@ -22,7 +22,7 @@ unique_toolkit/_common/experimental/endpoint_requestor.py,sha256=YnDr8wASAEjZjLA
|
|
|
22
22
|
unique_toolkit/_common/feature_flags/schema.py,sha256=X32VqH4VMK7bhEfSd8Wbddl8FVs7Gh7ucuIEbmqc4Kw,268
|
|
23
23
|
unique_toolkit/_common/pydantic/rjsf_tags.py,sha256=T3AZIF8wny3fFov66s258nEl1GqfKevFouTtG6k9PqU,31219
|
|
24
24
|
unique_toolkit/_common/pydantic_helpers.py,sha256=Yg1CHD603wVrqvinHiyh3stjIK3MjuexUe9aQQUfmXs,5406
|
|
25
|
-
unique_toolkit/_common/referencing.py,sha256=
|
|
25
|
+
unique_toolkit/_common/referencing.py,sha256=UY7-9nJLcRqlHUNytfJZPVkdpjvy5wIps_7P33_qgA0,1496
|
|
26
26
|
unique_toolkit/_common/string_utilities.py,sha256=hiNyGCNISm2HuEGWYgu01dB2YJGN_NfHI345X7rRu80,4347
|
|
27
27
|
unique_toolkit/_common/tests/test_referencing.py,sha256=WYKGkO3OXPYyTL8f6fVE9pqKuFowUHja5CUCTpWzRJQ,16206
|
|
28
28
|
unique_toolkit/_common/tests/test_string_utilities.py,sha256=J2HYZ1fniOfJWI149wlooVrT7ot4qe5bhehGh9B7FN8,15641
|
|
@@ -93,20 +93,21 @@ unique_toolkit/agentic/tools/a2a/manager.py,sha256=pk06UUXKQdIUY-PyykYiItubBjmIy
|
|
|
93
93
|
unique_toolkit/agentic/tools/a2a/postprocessing/__init__.py,sha256=aVtUBPN7kDrqA6Bze34AbqQpcBBqpvfyJG-xF65w7R0,659
|
|
94
94
|
unique_toolkit/agentic/tools/a2a/postprocessing/_display_utils.py,sha256=eL_5bkud6aPCKGER705ZQkNttrUZEs-zco83SfbffDQ,7107
|
|
95
95
|
unique_toolkit/agentic/tools/a2a/postprocessing/_ref_utils.py,sha256=tfH3Bhx67uQtH3fYoUr2Zp45HVw7hV_N-5F_qRwiOas,2727
|
|
96
|
-
unique_toolkit/agentic/tools/a2a/postprocessing/config.py,sha256=
|
|
96
|
+
unique_toolkit/agentic/tools/a2a/postprocessing/config.py,sha256=bmdi5CbtpSWe33fUY-VDRu0PdYI6ZB9fFqCY1cUQz30,2841
|
|
97
97
|
unique_toolkit/agentic/tools/a2a/postprocessing/display.py,sha256=e8R92XJHwyVI8UxVqCG_cTzensHSfGOIfDe1nfGcDoE,9058
|
|
98
98
|
unique_toolkit/agentic/tools/a2a/postprocessing/references.py,sha256=DGiv8WXMjIwumI7tlpWRgV8wSxnE282ryxEf03fgck8,3465
|
|
99
99
|
unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display.py,sha256=pP7fjvCm4aamtizs4viZSwsrw4Vb4kMxwDPEEx8GI98,14676
|
|
100
100
|
unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display_utils.py,sha256=5d67L4msesDENsNhgeoDTH6pjw_Z4FUUC1lRM9j4PlI,63608
|
|
101
101
|
unique_toolkit/agentic/tools/a2a/postprocessing/test/test_ref_utils.py,sha256=u-eNrHjsRFcu4TmzkD5XfFrxvaIToB42YGyXZ-RpsR0,17830
|
|
102
|
-
unique_toolkit/agentic/tools/a2a/prompts.py,sha256=
|
|
102
|
+
unique_toolkit/agentic/tools/a2a/prompts.py,sha256=Qm4yGs2kVm1rUtO-qntDf87q_euG7ed1cp9yf81demQ,2515
|
|
103
103
|
unique_toolkit/agentic/tools/a2a/response_watcher/__init__.py,sha256=fS8Lq49DZo5spMcP8QGTMWwSg80rYSr2pTfYDbssGYs,184
|
|
104
104
|
unique_toolkit/agentic/tools/a2a/response_watcher/service.py,sha256=Tlzd7Tvk0X8a9Jyi4qHWuAx0PffAnt6BGBbQ4QWAZus,2485
|
|
105
105
|
unique_toolkit/agentic/tools/a2a/tool/__init__.py,sha256=JIJKZBTLTA39OWhxoUd6uairxmqINur1Ex6iXDk9ef8,197
|
|
106
106
|
unique_toolkit/agentic/tools/a2a/tool/_memory.py,sha256=w8bxjokrqHQZgApd55b5rHXF-DpgJwaKTg4CvLBLamc,1034
|
|
107
107
|
unique_toolkit/agentic/tools/a2a/tool/_schema.py,sha256=wMwyunViTnxaURvenkATEvyfXn5LvLaP0HxbYqdZGls,158
|
|
108
|
-
unique_toolkit/agentic/tools/a2a/tool/config.py,sha256=
|
|
109
|
-
unique_toolkit/agentic/tools/a2a/tool/service.py,sha256=
|
|
108
|
+
unique_toolkit/agentic/tools/a2a/tool/config.py,sha256=k-YRhy7ELVLj7WP-BIDxlPYL8WK2kQll87U7wTtCrUk,6480
|
|
109
|
+
unique_toolkit/agentic/tools/a2a/tool/service.py,sha256=XC5bHZfKlUTvbvUrjYpCdKFrCFqZP53CznGBUT_m6Uo,15152
|
|
110
|
+
unique_toolkit/agentic/tools/a2a/tool/test/test_service_utils.py,sha256=x4leRxnGgqD-XyXPkW2z1ekv4Ja7EStXd-24LGadPZA,24858
|
|
110
111
|
unique_toolkit/agentic/tools/agent_chunks_hanlder.py,sha256=x32Dp1Z8cVW5i-XzXbaMwX2KHPcNGmqEU-FB4AV9ZGo,1909
|
|
111
112
|
unique_toolkit/agentic/tools/config.py,sha256=71PSq8LoR80irk2iJfy_AgXOPi4Nh3luW5z5cDvY8i0,3920
|
|
112
113
|
unique_toolkit/agentic/tools/factory.py,sha256=A1Aliwx037UAk9ADiDsg0zjCWWnvzV_PxwJNoPTvW6c,1434
|
|
@@ -208,7 +209,7 @@ unique_toolkit/short_term_memory/service.py,sha256=5PeVBu1ZCAfyDb2HLVvlmqSbyzBBu
|
|
|
208
209
|
unique_toolkit/smart_rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
209
210
|
unique_toolkit/smart_rules/compile.py,sha256=Ozhh70qCn2yOzRWr9d8WmJeTo7AQurwd3tStgBMPFLA,1246
|
|
210
211
|
unique_toolkit/test_utilities/events.py,sha256=_mwV2bs5iLjxS1ynDCjaIq-gjjKhXYCK-iy3dRfvO3g,6410
|
|
211
|
-
unique_toolkit-1.
|
|
212
|
-
unique_toolkit-1.
|
|
213
|
-
unique_toolkit-1.
|
|
214
|
-
unique_toolkit-1.
|
|
212
|
+
unique_toolkit-1.36.0.dist-info/LICENSE,sha256=GlN8wHNdh53xwOPg44URnwag6TEolCjoq3YD_KrWgss,193
|
|
213
|
+
unique_toolkit-1.36.0.dist-info/METADATA,sha256=b-BMlh-JACksT29xq0D-VY8p3OZo-wXrrwJgn6Ronf0,46179
|
|
214
|
+
unique_toolkit-1.36.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
215
|
+
unique_toolkit-1.36.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|