unique_toolkit 1.29.3__py3-none-any.whl → 1.30.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/agentic/tools/a2a/postprocessing/_display_utils.py +37 -3
- unique_toolkit/agentic/tools/a2a/postprocessing/config.py +21 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/display.py +40 -3
- unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display_utils.py +388 -0
- unique_toolkit/agentic/tools/openai_builtin/base.py +4 -0
- unique_toolkit/agentic/tools/openai_builtin/code_interpreter/service.py +4 -0
- unique_toolkit/app/__init__.py +3 -0
- unique_toolkit/app/fast_api_factory.py +131 -0
- unique_toolkit/app/webhook.py +77 -0
- {unique_toolkit-1.29.3.dist-info → unique_toolkit-1.30.0.dist-info}/METADATA +7 -1
- {unique_toolkit-1.29.3.dist-info → unique_toolkit-1.30.0.dist-info}/RECORD +13 -11
- {unique_toolkit-1.29.3.dist-info → unique_toolkit-1.30.0.dist-info}/LICENSE +0 -0
- {unique_toolkit-1.29.3.dist-info → unique_toolkit-1.30.0.dist-info}/WHEEL +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import re
|
|
2
|
-
from typing import Literal
|
|
2
|
+
from typing import Literal, NamedTuple
|
|
3
3
|
|
|
4
4
|
from unique_toolkit.agentic.tools.a2a.postprocessing.config import (
|
|
5
5
|
SubAgentDisplayConfig,
|
|
@@ -126,7 +126,7 @@ def _get_display_template(
|
|
|
126
126
|
if add_block_border:
|
|
127
127
|
template = _wrap_with_block_border(template)
|
|
128
128
|
|
|
129
|
-
return template
|
|
129
|
+
return template.strip()
|
|
130
130
|
|
|
131
131
|
|
|
132
132
|
def _get_display_removal_re(
|
|
@@ -150,10 +150,40 @@ def _get_display_removal_re(
|
|
|
150
150
|
return re.compile(pattern, flags=re.DOTALL)
|
|
151
151
|
|
|
152
152
|
|
|
153
|
+
class SubAgentAnswerPart(NamedTuple):
|
|
154
|
+
matching_text: str # Matching text as found in the answer
|
|
155
|
+
formatted_text: str # Formatted text to be displayed
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def get_sub_agent_answer_parts(
|
|
159
|
+
answer: str,
|
|
160
|
+
display_config: SubAgentDisplayConfig,
|
|
161
|
+
) -> list[SubAgentAnswerPart]:
|
|
162
|
+
if display_config.mode == SubAgentResponseDisplayMode.HIDDEN:
|
|
163
|
+
return []
|
|
164
|
+
|
|
165
|
+
if len(display_config.answer_substrings_config) == 0:
|
|
166
|
+
return [SubAgentAnswerPart(matching_text=answer, formatted_text=answer)]
|
|
167
|
+
|
|
168
|
+
substrings = []
|
|
169
|
+
for config in display_config.answer_substrings_config:
|
|
170
|
+
match = re.search(config.regexp, answer)
|
|
171
|
+
if match is not None:
|
|
172
|
+
text = match.group(0)
|
|
173
|
+
substrings.append(
|
|
174
|
+
SubAgentAnswerPart(
|
|
175
|
+
matching_text=text,
|
|
176
|
+
formatted_text=config.display_template.format(text),
|
|
177
|
+
)
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
return substrings
|
|
181
|
+
|
|
182
|
+
|
|
153
183
|
def get_sub_agent_answer_display(
|
|
154
184
|
display_name: str,
|
|
155
185
|
display_config: SubAgentDisplayConfig,
|
|
156
|
-
answer: str,
|
|
186
|
+
answer: str | list[str],
|
|
157
187
|
assistant_id: str,
|
|
158
188
|
) -> str:
|
|
159
189
|
template = _get_display_template(
|
|
@@ -162,6 +192,10 @@ def get_sub_agent_answer_display(
|
|
|
162
192
|
add_block_border=display_config.add_block_border,
|
|
163
193
|
display_title_template=display_config.display_title_template,
|
|
164
194
|
)
|
|
195
|
+
|
|
196
|
+
if isinstance(answer, list):
|
|
197
|
+
answer = display_config.answer_substrings_separator.join(answer)
|
|
198
|
+
|
|
165
199
|
return template.format(
|
|
166
200
|
display_name=display_name, answer=answer, assistant_id=assistant_id
|
|
167
201
|
)
|
|
@@ -13,6 +13,18 @@ class SubAgentResponseDisplayMode(StrEnum):
|
|
|
13
13
|
PLAIN = "plain"
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
class SubAgentAnswerSubstringConfig(BaseModel):
|
|
17
|
+
model_config = get_configuration_dict()
|
|
18
|
+
|
|
19
|
+
regexp: str = Field(
|
|
20
|
+
description="The regular expression to use to extract the substring. The first capture group will always be used.",
|
|
21
|
+
)
|
|
22
|
+
display_template: str = Field(
|
|
23
|
+
default="{}",
|
|
24
|
+
description="The template to use to display the substring. It should contain exactly one empty placeholder '{}' for the substring.",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
16
28
|
class SubAgentDisplayConfig(BaseModel):
|
|
17
29
|
model_config = get_configuration_dict()
|
|
18
30
|
|
|
@@ -47,3 +59,12 @@ class SubAgentDisplayConfig(BaseModel):
|
|
|
47
59
|
default=False,
|
|
48
60
|
description="If set, the sub agent references will be added to the main agent response references even in not mentioned in the main agent response text.",
|
|
49
61
|
)
|
|
62
|
+
|
|
63
|
+
answer_substrings_config: list[SubAgentAnswerSubstringConfig] = Field(
|
|
64
|
+
default=[],
|
|
65
|
+
description="If set, only parts of the answer matching the provided regular expressions will be displayed.",
|
|
66
|
+
)
|
|
67
|
+
answer_substrings_separator: str = Field(
|
|
68
|
+
default="\n",
|
|
69
|
+
description="The separator to use between the substrings.",
|
|
70
|
+
)
|
|
@@ -10,6 +10,7 @@ from unique_toolkit._common.pydantic_helpers import get_configuration_dict
|
|
|
10
10
|
from unique_toolkit.agentic.postprocessor.postprocessor_manager import Postprocessor
|
|
11
11
|
from unique_toolkit.agentic.tools.a2a.postprocessing._display_utils import (
|
|
12
12
|
get_sub_agent_answer_display,
|
|
13
|
+
get_sub_agent_answer_parts,
|
|
13
14
|
remove_sub_agent_answer_from_text,
|
|
14
15
|
)
|
|
15
16
|
from unique_toolkit.agentic.tools.a2a.postprocessing._ref_utils import (
|
|
@@ -44,6 +45,15 @@ class SubAgentResponsesPostprocessorConfig(BaseModel):
|
|
|
44
45
|
default=1, description="Time to sleep before updating the main agent message."
|
|
45
46
|
)
|
|
46
47
|
|
|
48
|
+
remove_duplicate_answers: bool = Field(
|
|
49
|
+
default=False,
|
|
50
|
+
description="If set, duplicate answers will only be displayed once. If sub agent is configured to display only substrings, this will remove duplicate substrings across different responses.",
|
|
51
|
+
)
|
|
52
|
+
answer_separator: str = Field(
|
|
53
|
+
default="",
|
|
54
|
+
description="The separator to use between the different sub agent answers.",
|
|
55
|
+
)
|
|
56
|
+
|
|
47
57
|
|
|
48
58
|
class SubAgentResponsesDisplayPostprocessor(Postprocessor):
|
|
49
59
|
def __init__(
|
|
@@ -96,6 +106,8 @@ class SubAgentResponsesDisplayPostprocessor(Postprocessor):
|
|
|
96
106
|
answers_displayed_before = []
|
|
97
107
|
answers_displayed_after = []
|
|
98
108
|
|
|
109
|
+
all_answers_displayed = set()
|
|
110
|
+
|
|
99
111
|
for assistant_id, responses in displayed_sub_agent_responses.items():
|
|
100
112
|
for response in responses:
|
|
101
113
|
message = response.message
|
|
@@ -105,6 +117,9 @@ class SubAgentResponsesDisplayPostprocessor(Postprocessor):
|
|
|
105
117
|
loop_response=loop_response, response=message
|
|
106
118
|
)
|
|
107
119
|
|
|
120
|
+
if tool_info.display_config.mode == SubAgentResponseDisplayMode.HIDDEN:
|
|
121
|
+
continue
|
|
122
|
+
|
|
108
123
|
display_name = tool_info.display_name
|
|
109
124
|
if len(responses) > 1:
|
|
110
125
|
display_name += f" {response.sequence_number}"
|
|
@@ -116,13 +131,33 @@ class SubAgentResponsesDisplayPostprocessor(Postprocessor):
|
|
|
116
131
|
response.sequence_number,
|
|
117
132
|
)
|
|
118
133
|
|
|
119
|
-
|
|
134
|
+
message_text = message["text"] or ""
|
|
135
|
+
|
|
136
|
+
answer_parts = get_sub_agent_answer_parts(
|
|
137
|
+
answer=message_text,
|
|
138
|
+
display_config=tool_info.display_config,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if len(answer_parts) == 0:
|
|
120
142
|
continue
|
|
121
143
|
|
|
144
|
+
answer_display_texts = []
|
|
145
|
+
if self._config.remove_duplicate_answers:
|
|
146
|
+
for answer_part in answer_parts:
|
|
147
|
+
if answer_part.matching_text in all_answers_displayed:
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
all_answers_displayed.add(answer_part.matching_text)
|
|
151
|
+
answer_display_texts.append(answer_part.formatted_text)
|
|
152
|
+
else:
|
|
153
|
+
answer_display_texts = [
|
|
154
|
+
answer_part.formatted_text for answer_part in answer_parts
|
|
155
|
+
]
|
|
156
|
+
|
|
122
157
|
answer = get_sub_agent_answer_display(
|
|
123
158
|
display_name=display_name,
|
|
124
159
|
display_config=tool_info.display_config,
|
|
125
|
-
answer=
|
|
160
|
+
answer=answer_display_texts,
|
|
126
161
|
assistant_id=assistant_id,
|
|
127
162
|
)
|
|
128
163
|
|
|
@@ -135,6 +170,7 @@ class SubAgentResponsesDisplayPostprocessor(Postprocessor):
|
|
|
135
170
|
text=loop_response.message.text,
|
|
136
171
|
answers_before=answers_displayed_before,
|
|
137
172
|
answers_after=answers_displayed_after,
|
|
173
|
+
sep=self._config.answer_separator,
|
|
138
174
|
)
|
|
139
175
|
|
|
140
176
|
return True
|
|
@@ -182,4 +218,5 @@ def _get_final_answer_display(
|
|
|
182
218
|
|
|
183
219
|
if len(answers_after) > 0:
|
|
184
220
|
text = text + sep + sep.join(answers_after)
|
|
185
|
-
|
|
221
|
+
|
|
222
|
+
return text.strip()
|
|
@@ -16,9 +16,11 @@ from unique_toolkit.agentic.tools.a2a.postprocessing._display_utils import (
|
|
|
16
16
|
_wrap_with_details_tag,
|
|
17
17
|
_wrap_with_quote_border,
|
|
18
18
|
get_sub_agent_answer_display,
|
|
19
|
+
get_sub_agent_answer_parts,
|
|
19
20
|
remove_sub_agent_answer_from_text,
|
|
20
21
|
)
|
|
21
22
|
from unique_toolkit.agentic.tools.a2a.postprocessing.config import (
|
|
23
|
+
SubAgentAnswerSubstringConfig,
|
|
22
24
|
SubAgentDisplayConfig,
|
|
23
25
|
SubAgentResponseDisplayMode,
|
|
24
26
|
)
|
|
@@ -1333,3 +1335,389 @@ def test_remove_sub_agent_answer__no_op_when_assistant_not_found() -> None:
|
|
|
1333
1335
|
assert result == original_text
|
|
1334
1336
|
assert "Present answer" in result
|
|
1335
1337
|
assert "Present Agent" in result
|
|
1338
|
+
|
|
1339
|
+
|
|
1340
|
+
# Test get_sub_agent_answer_parts
|
|
1341
|
+
|
|
1342
|
+
|
|
1343
|
+
@pytest.mark.ai
|
|
1344
|
+
def test_get_sub_agent_answer_parts__returns_empty__when_hidden_mode() -> None:
|
|
1345
|
+
"""
|
|
1346
|
+
Purpose: Verify empty list returned for HIDDEN display mode.
|
|
1347
|
+
Why this matters: Hidden mode should not extract any answer parts.
|
|
1348
|
+
Setup summary: Set mode to HIDDEN, assert empty list.
|
|
1349
|
+
"""
|
|
1350
|
+
# Arrange
|
|
1351
|
+
answer = "Some answer text"
|
|
1352
|
+
config = SubAgentDisplayConfig(
|
|
1353
|
+
mode=SubAgentResponseDisplayMode.HIDDEN,
|
|
1354
|
+
)
|
|
1355
|
+
|
|
1356
|
+
# Act
|
|
1357
|
+
result = get_sub_agent_answer_parts(answer=answer, display_config=config)
|
|
1358
|
+
|
|
1359
|
+
# Assert
|
|
1360
|
+
assert result == []
|
|
1361
|
+
|
|
1362
|
+
|
|
1363
|
+
@pytest.mark.ai
|
|
1364
|
+
def test_get_sub_agent_answer_parts__returns_full_answer__when_no_config() -> None:
|
|
1365
|
+
"""
|
|
1366
|
+
Purpose: Verify full answer returned when no substring config provided.
|
|
1367
|
+
Why this matters: Default behavior should return entire answer.
|
|
1368
|
+
Setup summary: Provide answer without substring config, assert full answer.
|
|
1369
|
+
"""
|
|
1370
|
+
# Arrange
|
|
1371
|
+
answer = "This is the complete answer"
|
|
1372
|
+
config = SubAgentDisplayConfig(
|
|
1373
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1374
|
+
answer_substrings_config=[],
|
|
1375
|
+
)
|
|
1376
|
+
|
|
1377
|
+
# Act
|
|
1378
|
+
result = get_sub_agent_answer_parts(answer=answer, display_config=config)
|
|
1379
|
+
|
|
1380
|
+
# Assert
|
|
1381
|
+
assert len(result) == 1
|
|
1382
|
+
assert result[0].matching_text == answer
|
|
1383
|
+
assert result[0].formatted_text == answer
|
|
1384
|
+
|
|
1385
|
+
|
|
1386
|
+
@pytest.mark.ai
|
|
1387
|
+
def test_get_sub_agent_answer_parts__extracts_single_match__with_one_regexp() -> None:
|
|
1388
|
+
"""
|
|
1389
|
+
Purpose: Verify single substring extracted with one regexp config.
|
|
1390
|
+
Why this matters: Core functionality for extracting specific answer parts.
|
|
1391
|
+
Setup summary: Provide answer with single regexp config, assert match extracted.
|
|
1392
|
+
"""
|
|
1393
|
+
# Arrange
|
|
1394
|
+
answer = "The price is $42.99 for the item"
|
|
1395
|
+
config = SubAgentDisplayConfig(
|
|
1396
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1397
|
+
answer_substrings_config=[
|
|
1398
|
+
SubAgentAnswerSubstringConfig(regexp=r"\$\d+\.\d+"),
|
|
1399
|
+
],
|
|
1400
|
+
)
|
|
1401
|
+
|
|
1402
|
+
# Act
|
|
1403
|
+
result = get_sub_agent_answer_parts(answer=answer, display_config=config)
|
|
1404
|
+
|
|
1405
|
+
# Assert
|
|
1406
|
+
assert len(result) == 1
|
|
1407
|
+
assert result[0].matching_text == "$42.99"
|
|
1408
|
+
assert result[0].formatted_text == "$42.99"
|
|
1409
|
+
|
|
1410
|
+
|
|
1411
|
+
@pytest.mark.ai
|
|
1412
|
+
def test_get_sub_agent_answer_parts__extracts_multiple_matches__with_multiple_regexps() -> (
|
|
1413
|
+
None
|
|
1414
|
+
):
|
|
1415
|
+
"""
|
|
1416
|
+
Purpose: Verify multiple substrings extracted with multiple regexp configs.
|
|
1417
|
+
Why this matters: Supports extracting different types of information.
|
|
1418
|
+
Setup summary: Provide answer with multiple regexp configs, assert all matches.
|
|
1419
|
+
"""
|
|
1420
|
+
# Arrange
|
|
1421
|
+
answer = "Contact John at john@example.com or call 555-1234"
|
|
1422
|
+
config = SubAgentDisplayConfig(
|
|
1423
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1424
|
+
answer_substrings_config=[
|
|
1425
|
+
SubAgentAnswerSubstringConfig(
|
|
1426
|
+
regexp=r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b"
|
|
1427
|
+
),
|
|
1428
|
+
SubAgentAnswerSubstringConfig(regexp=r"\d{3}-\d{4}"),
|
|
1429
|
+
],
|
|
1430
|
+
)
|
|
1431
|
+
|
|
1432
|
+
# Act
|
|
1433
|
+
result = get_sub_agent_answer_parts(answer=answer, display_config=config)
|
|
1434
|
+
|
|
1435
|
+
# Assert
|
|
1436
|
+
assert len(result) == 2
|
|
1437
|
+
assert result[0].matching_text == "john@example.com"
|
|
1438
|
+
assert result[1].matching_text == "555-1234"
|
|
1439
|
+
|
|
1440
|
+
|
|
1441
|
+
@pytest.mark.ai
|
|
1442
|
+
def test_get_sub_agent_answer_parts__applies_display_template__to_matched_text() -> (
|
|
1443
|
+
None
|
|
1444
|
+
):
|
|
1445
|
+
"""
|
|
1446
|
+
Purpose: Verify display template is applied to format matched text.
|
|
1447
|
+
Why this matters: Allows customization of how extracted parts are displayed.
|
|
1448
|
+
Setup summary: Provide template with placeholder, assert formatted output.
|
|
1449
|
+
"""
|
|
1450
|
+
# Arrange
|
|
1451
|
+
answer = "The temperature is 72 degrees"
|
|
1452
|
+
config = SubAgentDisplayConfig(
|
|
1453
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1454
|
+
answer_substrings_config=[
|
|
1455
|
+
SubAgentAnswerSubstringConfig(
|
|
1456
|
+
regexp=r"\d+",
|
|
1457
|
+
display_template="Temperature: {}°F",
|
|
1458
|
+
),
|
|
1459
|
+
],
|
|
1460
|
+
)
|
|
1461
|
+
|
|
1462
|
+
# Act
|
|
1463
|
+
result = get_sub_agent_answer_parts(answer=answer, display_config=config)
|
|
1464
|
+
|
|
1465
|
+
# Assert
|
|
1466
|
+
assert len(result) == 1
|
|
1467
|
+
assert result[0].matching_text == "72"
|
|
1468
|
+
assert result[0].formatted_text == "Temperature: 72°F"
|
|
1469
|
+
|
|
1470
|
+
|
|
1471
|
+
@pytest.mark.ai
|
|
1472
|
+
def test_get_sub_agent_answer_parts__returns_empty_list__when_no_matches() -> None:
|
|
1473
|
+
"""
|
|
1474
|
+
Purpose: Verify empty list returned when regexp doesn't match answer.
|
|
1475
|
+
Why this matters: Handles cases where expected pattern not present.
|
|
1476
|
+
Setup summary: Provide regexp that doesn't match, assert empty list.
|
|
1477
|
+
"""
|
|
1478
|
+
# Arrange
|
|
1479
|
+
answer = "This is plain text without numbers"
|
|
1480
|
+
config = SubAgentDisplayConfig(
|
|
1481
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1482
|
+
answer_substrings_config=[
|
|
1483
|
+
SubAgentAnswerSubstringConfig(regexp=r"\d+"),
|
|
1484
|
+
],
|
|
1485
|
+
)
|
|
1486
|
+
|
|
1487
|
+
# Act
|
|
1488
|
+
result = get_sub_agent_answer_parts(answer=answer, display_config=config)
|
|
1489
|
+
|
|
1490
|
+
# Assert
|
|
1491
|
+
assert result == []
|
|
1492
|
+
|
|
1493
|
+
|
|
1494
|
+
@pytest.mark.ai
|
|
1495
|
+
def test_get_sub_agent_answer_parts__extracts_first_match_only__for_each_regexp() -> (
|
|
1496
|
+
None
|
|
1497
|
+
):
|
|
1498
|
+
"""
|
|
1499
|
+
Purpose: Verify only first match per regexp is extracted.
|
|
1500
|
+
Why this matters: Function uses re.search which finds first occurrence.
|
|
1501
|
+
Setup summary: Provide answer with multiple numbers, assert only first extracted.
|
|
1502
|
+
"""
|
|
1503
|
+
# Arrange
|
|
1504
|
+
answer = "First number is 42 and second is 99"
|
|
1505
|
+
config = SubAgentDisplayConfig(
|
|
1506
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1507
|
+
answer_substrings_config=[
|
|
1508
|
+
SubAgentAnswerSubstringConfig(regexp=r"\d+"),
|
|
1509
|
+
],
|
|
1510
|
+
)
|
|
1511
|
+
|
|
1512
|
+
# Act
|
|
1513
|
+
result = get_sub_agent_answer_parts(answer=answer, display_config=config)
|
|
1514
|
+
|
|
1515
|
+
# Assert
|
|
1516
|
+
assert len(result) == 1
|
|
1517
|
+
assert result[0].matching_text == "42"
|
|
1518
|
+
|
|
1519
|
+
|
|
1520
|
+
@pytest.mark.ai
|
|
1521
|
+
def test_get_sub_agent_answer_parts__handles_empty_answer__with_no_config() -> None:
|
|
1522
|
+
"""
|
|
1523
|
+
Purpose: Verify empty answer returned as single part when no config.
|
|
1524
|
+
Why this matters: Edge case handling for empty content.
|
|
1525
|
+
Setup summary: Provide empty answer, assert single empty part.
|
|
1526
|
+
"""
|
|
1527
|
+
# Arrange
|
|
1528
|
+
answer = ""
|
|
1529
|
+
config = SubAgentDisplayConfig(
|
|
1530
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1531
|
+
answer_substrings_config=[],
|
|
1532
|
+
)
|
|
1533
|
+
|
|
1534
|
+
# Act
|
|
1535
|
+
result = get_sub_agent_answer_parts(answer=answer, display_config=config)
|
|
1536
|
+
|
|
1537
|
+
# Assert
|
|
1538
|
+
assert len(result) == 1
|
|
1539
|
+
assert result[0].matching_text == ""
|
|
1540
|
+
assert result[0].formatted_text == ""
|
|
1541
|
+
|
|
1542
|
+
|
|
1543
|
+
@pytest.mark.ai
|
|
1544
|
+
def test_get_sub_agent_answer_parts__handles_empty_answer__with_regexp_config() -> None:
|
|
1545
|
+
"""
|
|
1546
|
+
Purpose: Verify empty list returned for empty answer with regexp config.
|
|
1547
|
+
Why this matters: No matches possible in empty string.
|
|
1548
|
+
Setup summary: Provide empty answer with regexp, assert empty list.
|
|
1549
|
+
"""
|
|
1550
|
+
# Arrange
|
|
1551
|
+
answer = ""
|
|
1552
|
+
config = SubAgentDisplayConfig(
|
|
1553
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1554
|
+
answer_substrings_config=[
|
|
1555
|
+
SubAgentAnswerSubstringConfig(regexp=r"\d+"),
|
|
1556
|
+
],
|
|
1557
|
+
)
|
|
1558
|
+
|
|
1559
|
+
# Act
|
|
1560
|
+
result = get_sub_agent_answer_parts(answer=answer, display_config=config)
|
|
1561
|
+
|
|
1562
|
+
# Assert
|
|
1563
|
+
assert result == []
|
|
1564
|
+
|
|
1565
|
+
|
|
1566
|
+
@pytest.mark.ai
|
|
1567
|
+
def test_get_sub_agent_answer_parts__handles_multiline_answer__with_regexp() -> None:
|
|
1568
|
+
"""
|
|
1569
|
+
Purpose: Verify regexp matching works across multiple lines.
|
|
1570
|
+
Why this matters: Answers can span multiple lines.
|
|
1571
|
+
Setup summary: Provide multiline answer with pattern, assert match found.
|
|
1572
|
+
"""
|
|
1573
|
+
# Arrange
|
|
1574
|
+
answer = "Line 1\nThe code is ABC123\nLine 3"
|
|
1575
|
+
config = SubAgentDisplayConfig(
|
|
1576
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1577
|
+
answer_substrings_config=[
|
|
1578
|
+
SubAgentAnswerSubstringConfig(regexp=r"[A-Z]{3}\d{3}"),
|
|
1579
|
+
],
|
|
1580
|
+
)
|
|
1581
|
+
|
|
1582
|
+
# Act
|
|
1583
|
+
result = get_sub_agent_answer_parts(answer=answer, display_config=config)
|
|
1584
|
+
|
|
1585
|
+
# Assert
|
|
1586
|
+
assert len(result) == 1
|
|
1587
|
+
assert result[0].matching_text == "ABC123"
|
|
1588
|
+
|
|
1589
|
+
|
|
1590
|
+
@pytest.mark.ai
|
|
1591
|
+
def test_get_sub_agent_answer_parts__handles_special_regex_chars__in_answer() -> None:
|
|
1592
|
+
"""
|
|
1593
|
+
Purpose: Verify regexp can match content with special regex characters.
|
|
1594
|
+
Why this matters: Answers may contain special characters.
|
|
1595
|
+
Setup summary: Provide answer with special chars, use proper escaping in regexp.
|
|
1596
|
+
"""
|
|
1597
|
+
# Arrange
|
|
1598
|
+
answer = "The expression is: [test] (value)"
|
|
1599
|
+
config = SubAgentDisplayConfig(
|
|
1600
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1601
|
+
answer_substrings_config=[
|
|
1602
|
+
SubAgentAnswerSubstringConfig(regexp=r"\[test\]"),
|
|
1603
|
+
],
|
|
1604
|
+
)
|
|
1605
|
+
|
|
1606
|
+
# Act
|
|
1607
|
+
result = get_sub_agent_answer_parts(answer=answer, display_config=config)
|
|
1608
|
+
|
|
1609
|
+
# Assert
|
|
1610
|
+
assert len(result) == 1
|
|
1611
|
+
assert result[0].matching_text == "[test]"
|
|
1612
|
+
|
|
1613
|
+
|
|
1614
|
+
@pytest.mark.ai
|
|
1615
|
+
def test_get_sub_agent_answer_parts__skips_non_matching_configs__returns_matches_only() -> (
|
|
1616
|
+
None
|
|
1617
|
+
):
|
|
1618
|
+
"""
|
|
1619
|
+
Purpose: Verify only matching regexp configs produce results.
|
|
1620
|
+
Why this matters: Should not fail on partial matches, only return what matches.
|
|
1621
|
+
Setup summary: Provide multiple configs where only some match, assert partial results.
|
|
1622
|
+
"""
|
|
1623
|
+
# Arrange
|
|
1624
|
+
answer = "Value is 42"
|
|
1625
|
+
config = SubAgentDisplayConfig(
|
|
1626
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1627
|
+
answer_substrings_config=[
|
|
1628
|
+
SubAgentAnswerSubstringConfig(regexp=r"\d+"), # Matches
|
|
1629
|
+
SubAgentAnswerSubstringConfig(regexp=r"[A-Z]{3}"), # Doesn't match
|
|
1630
|
+
SubAgentAnswerSubstringConfig(regexp=r"Value"), # Matches
|
|
1631
|
+
],
|
|
1632
|
+
)
|
|
1633
|
+
|
|
1634
|
+
# Act
|
|
1635
|
+
result = get_sub_agent_answer_parts(answer=answer, display_config=config)
|
|
1636
|
+
|
|
1637
|
+
# Assert
|
|
1638
|
+
assert len(result) == 2
|
|
1639
|
+
assert result[0].matching_text == "42"
|
|
1640
|
+
assert result[1].matching_text == "Value"
|
|
1641
|
+
|
|
1642
|
+
|
|
1643
|
+
@pytest.mark.ai
|
|
1644
|
+
def test_get_sub_agent_answer_parts__preserves_order__of_configs_not_matches() -> None:
|
|
1645
|
+
"""
|
|
1646
|
+
Purpose: Verify results follow config order, not match order in text.
|
|
1647
|
+
Why this matters: Predictable output order based on configuration.
|
|
1648
|
+
Setup summary: Provide configs in specific order, assert results match config order.
|
|
1649
|
+
"""
|
|
1650
|
+
# Arrange
|
|
1651
|
+
answer = "first 123 then abc"
|
|
1652
|
+
config = SubAgentDisplayConfig(
|
|
1653
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1654
|
+
answer_substrings_config=[
|
|
1655
|
+
SubAgentAnswerSubstringConfig(regexp=r"[a-z]{3,}"), # Matches "first"
|
|
1656
|
+
SubAgentAnswerSubstringConfig(regexp=r"\d+"), # Matches "123"
|
|
1657
|
+
],
|
|
1658
|
+
)
|
|
1659
|
+
|
|
1660
|
+
# Act
|
|
1661
|
+
result = get_sub_agent_answer_parts(answer=answer, display_config=config)
|
|
1662
|
+
|
|
1663
|
+
# Assert
|
|
1664
|
+
assert len(result) == 2
|
|
1665
|
+
# Results follow config order, not text order
|
|
1666
|
+
assert result[0].matching_text == "first"
|
|
1667
|
+
assert result[1].matching_text == "123"
|
|
1668
|
+
|
|
1669
|
+
|
|
1670
|
+
@pytest.mark.ai
|
|
1671
|
+
def test_get_sub_agent_answer_parts__handles_complex_template__with_multiple_placeholders() -> (
|
|
1672
|
+
None
|
|
1673
|
+
):
|
|
1674
|
+
"""
|
|
1675
|
+
Purpose: Verify complex display templates with formatting work correctly.
|
|
1676
|
+
Why this matters: Supports rich formatting of extracted content.
|
|
1677
|
+
Setup summary: Provide template with additional text, assert formatted correctly.
|
|
1678
|
+
"""
|
|
1679
|
+
# Arrange
|
|
1680
|
+
answer = "User score: 95"
|
|
1681
|
+
config = SubAgentDisplayConfig(
|
|
1682
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1683
|
+
answer_substrings_config=[
|
|
1684
|
+
SubAgentAnswerSubstringConfig(
|
|
1685
|
+
regexp=r"\d+",
|
|
1686
|
+
display_template="**Score: {}%**",
|
|
1687
|
+
),
|
|
1688
|
+
],
|
|
1689
|
+
)
|
|
1690
|
+
|
|
1691
|
+
# Act
|
|
1692
|
+
result = get_sub_agent_answer_parts(answer=answer, display_config=config)
|
|
1693
|
+
|
|
1694
|
+
# Assert
|
|
1695
|
+
assert len(result) == 1
|
|
1696
|
+
assert result[0].matching_text == "95"
|
|
1697
|
+
assert result[0].formatted_text == "**Score: 95%**"
|
|
1698
|
+
|
|
1699
|
+
|
|
1700
|
+
@pytest.mark.ai
|
|
1701
|
+
def test_get_sub_agent_answer_parts__works_with_details_modes__extracts_normally() -> (
|
|
1702
|
+
None
|
|
1703
|
+
):
|
|
1704
|
+
"""
|
|
1705
|
+
Purpose: Verify extraction works regardless of display mode (except HIDDEN).
|
|
1706
|
+
Why this matters: Substring extraction independent of display mode.
|
|
1707
|
+
Setup summary: Use DETAILS modes, assert extraction still works.
|
|
1708
|
+
"""
|
|
1709
|
+
# Arrange
|
|
1710
|
+
answer = "Result: SUCCESS"
|
|
1711
|
+
config = SubAgentDisplayConfig(
|
|
1712
|
+
mode=SubAgentResponseDisplayMode.DETAILS_CLOSED,
|
|
1713
|
+
answer_substrings_config=[
|
|
1714
|
+
SubAgentAnswerSubstringConfig(regexp=r"SUCCESS"),
|
|
1715
|
+
],
|
|
1716
|
+
)
|
|
1717
|
+
|
|
1718
|
+
# Act
|
|
1719
|
+
result = get_sub_agent_answer_parts(answer=answer, display_config=config)
|
|
1720
|
+
|
|
1721
|
+
# Assert
|
|
1722
|
+
assert len(result) == 1
|
|
1723
|
+
assert result[0].matching_text == "SUCCESS"
|
|
@@ -244,3 +244,7 @@ class OpenAICodeInterpreterTool(OpenAIBuiltInTool[CodeInterpreter]):
|
|
|
244
244
|
tool_format_information_for_user_prompt=self._config.tool_format_information_for_user_prompt,
|
|
245
245
|
input_model={},
|
|
246
246
|
)
|
|
247
|
+
|
|
248
|
+
@override
|
|
249
|
+
def display_name(self) -> str:
|
|
250
|
+
return self.DISPLAY_NAME
|
unique_toolkit/app/__init__.py
CHANGED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from logging import getLogger
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Callable, TypeVar
|
|
4
|
+
|
|
5
|
+
from pydantic import ValidationError
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from fastapi import BackgroundTasks, FastAPI, Request, status
|
|
9
|
+
from fastapi.responses import JSONResponse
|
|
10
|
+
else:
|
|
11
|
+
try:
|
|
12
|
+
from fastapi import BackgroundTasks, FastAPI, Request, status
|
|
13
|
+
from fastapi.responses import JSONResponse
|
|
14
|
+
except ImportError:
|
|
15
|
+
FastAPI = None # type: ignore[assignment, misc]
|
|
16
|
+
Request = None # type: ignore[assignment, misc]
|
|
17
|
+
status = None # type: ignore[assignment, misc]
|
|
18
|
+
JSONResponse = None # type: ignore[assignment, misc]
|
|
19
|
+
BackgroundTasks = None # type: ignore[assignment, misc]
|
|
20
|
+
|
|
21
|
+
from unique_toolkit.app.schemas import BaseEvent, ChatEvent, EventName
|
|
22
|
+
from unique_toolkit.app.unique_settings import UniqueSettings
|
|
23
|
+
|
|
24
|
+
logger = getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def default_event_handler(event: Any) -> int:
|
|
28
|
+
logger.info("Event received at event handler")
|
|
29
|
+
if status is not None:
|
|
30
|
+
return status.HTTP_200_OK
|
|
31
|
+
else:
|
|
32
|
+
# No fastapi installed
|
|
33
|
+
return 200
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
T = TypeVar("T", bound=BaseEvent)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def build_unique_custom_app(
|
|
40
|
+
*,
|
|
41
|
+
title: str = "Unique Chat App",
|
|
42
|
+
webhook_path: str = "/webhook",
|
|
43
|
+
settings: UniqueSettings,
|
|
44
|
+
event_handler: Callable[[T], int] = default_event_handler,
|
|
45
|
+
event_constructor: Callable[..., T] = ChatEvent,
|
|
46
|
+
subscribed_event_names: list[str] | None = None,
|
|
47
|
+
) -> "FastAPI":
|
|
48
|
+
"""Factory class for creating FastAPI apps with Unique webhook handling."""
|
|
49
|
+
if FastAPI is None:
|
|
50
|
+
raise ImportError(
|
|
51
|
+
"FastAPI is not installed. Install it with: poetry install --with fastapi"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
app = FastAPI(title=title)
|
|
55
|
+
|
|
56
|
+
if subscribed_event_names is None:
|
|
57
|
+
subscribed_event_names = [EventName.EXTERNAL_MODULE_CHOSEN]
|
|
58
|
+
|
|
59
|
+
@app.get(path="/")
|
|
60
|
+
async def health_check() -> JSONResponse:
|
|
61
|
+
"""Health check endpoint."""
|
|
62
|
+
return JSONResponse(content={"status": "healthy", "service": title})
|
|
63
|
+
|
|
64
|
+
@app.post(path=webhook_path)
|
|
65
|
+
async def webhook_handler(
|
|
66
|
+
request: Request, background_tasks: BackgroundTasks
|
|
67
|
+
) -> JSONResponse:
|
|
68
|
+
"""
|
|
69
|
+
Webhook endpoint for receiving events from Unique platform.
|
|
70
|
+
|
|
71
|
+
This endpoint:
|
|
72
|
+
1. Verifies the webhook signature
|
|
73
|
+
2. Constructs an event from the payload
|
|
74
|
+
3. Calls the configured event handler
|
|
75
|
+
"""
|
|
76
|
+
# Get raw body and headers
|
|
77
|
+
body = await request.body()
|
|
78
|
+
headers = dict(request.headers)
|
|
79
|
+
|
|
80
|
+
from unique_toolkit.app.webhook import is_webhook_signature_valid
|
|
81
|
+
|
|
82
|
+
if not is_webhook_signature_valid(
|
|
83
|
+
headers=headers,
|
|
84
|
+
payload=body,
|
|
85
|
+
endpoint_secret=settings.app.endpoint_secret.get_secret_value(),
|
|
86
|
+
):
|
|
87
|
+
return JSONResponse(
|
|
88
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
89
|
+
content={"error": "Invalid webhook signature"},
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
event_data = json.loads(body.decode(encoding="utf-8"))
|
|
94
|
+
except json.JSONDecodeError as e:
|
|
95
|
+
logger.error(f"Error parsing event: {e}", exc_info=True)
|
|
96
|
+
return JSONResponse(
|
|
97
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
98
|
+
content={"error": f"Invalid event format: {str(e)}"},
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if event_data["event"] not in subscribed_event_names:
|
|
102
|
+
return JSONResponse(
|
|
103
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
104
|
+
content={"error": "Not subscribed event"},
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
event = event_constructor(**event_data)
|
|
109
|
+
if event.filter_event(filter_options=settings.chat_event_filter_options):
|
|
110
|
+
return JSONResponse(
|
|
111
|
+
status_code=status.HTTP_200_OK,
|
|
112
|
+
content={"error": "Event filtered out"},
|
|
113
|
+
)
|
|
114
|
+
except ValidationError as e:
|
|
115
|
+
# pydantic errors https://docs.pydantic.dev/2.10/errors/errors/
|
|
116
|
+
logger.error(f"Validation error with model: {e.json()}", exc_info=True)
|
|
117
|
+
raise e
|
|
118
|
+
except ValueError as e:
|
|
119
|
+
logger.error(f"Error deserializing event: {e}", exc_info=True)
|
|
120
|
+
return JSONResponse(
|
|
121
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
122
|
+
content={"error": "Invalid event"},
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Run the task in background so that we don't block for long running tasks
|
|
126
|
+
background_tasks.add_task(event_handler, event)
|
|
127
|
+
return JSONResponse(
|
|
128
|
+
status_code=status.HTTP_200_OK, content={"message": "Event received"}
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
return app
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Webhook signature verification for Unique platform.
|
|
3
|
+
|
|
4
|
+
Extracted from unique_sdk to provide standalone verification without event construction.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import hashlib
|
|
8
|
+
import hmac
|
|
9
|
+
import logging
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
_LOGGER = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def is_webhook_signature_valid(
|
|
16
|
+
headers: dict[str, str],
|
|
17
|
+
payload: bytes,
|
|
18
|
+
endpoint_secret: str,
|
|
19
|
+
tolerance: int = 300,
|
|
20
|
+
) -> bool:
|
|
21
|
+
"""
|
|
22
|
+
Verify webhook signature from Unique platform.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
headers: Request headers with X-Unique-Signature and X-Unique-Created-At
|
|
26
|
+
payload: Raw request body bytes
|
|
27
|
+
endpoint_secret: App endpoint secret from Unique platform
|
|
28
|
+
tolerance: Max seconds between timestamp and now (default: 300)
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
True if signature is valid, False otherwise
|
|
32
|
+
"""
|
|
33
|
+
# Extract headers
|
|
34
|
+
signature = headers.get("X-Unique-Signature") or headers.get("x-unique-signature")
|
|
35
|
+
timestamp_str = headers.get("X-Unique-Created-At") or headers.get(
|
|
36
|
+
"x-unique-created-at"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
if not signature:
|
|
40
|
+
_LOGGER.error("Missing X-Unique-Signature header")
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
if not timestamp_str:
|
|
44
|
+
_LOGGER.error("Missing X-Unique-Created-At header")
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
# Convert timestamp to int
|
|
48
|
+
try:
|
|
49
|
+
timestamp = int(timestamp_str)
|
|
50
|
+
except ValueError:
|
|
51
|
+
_LOGGER.error(f"Invalid timestamp: {timestamp_str}")
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
# Decode payload if bytes
|
|
55
|
+
message = payload.decode("utf-8") if isinstance(payload, bytes) else payload
|
|
56
|
+
|
|
57
|
+
# Compute expected signature: HMAC-SHA256(message, secret)
|
|
58
|
+
expected_signature = hmac.new(
|
|
59
|
+
endpoint_secret.encode("utf-8"),
|
|
60
|
+
msg=message.encode("utf-8"),
|
|
61
|
+
digestmod=hashlib.sha256,
|
|
62
|
+
).hexdigest()
|
|
63
|
+
|
|
64
|
+
# Compare signatures (constant-time to prevent timing attacks)
|
|
65
|
+
if not hmac.compare_digest(expected_signature, signature):
|
|
66
|
+
_LOGGER.error("Signature mismatch. Ensure you're using the raw request body.")
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
# Check timestamp tolerance (prevent replay attacks)
|
|
70
|
+
if tolerance and timestamp < time.time() - tolerance:
|
|
71
|
+
_LOGGER.error(
|
|
72
|
+
f"Timestamp outside tolerance ({tolerance}s). Possible replay attack."
|
|
73
|
+
)
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
_LOGGER.debug("✅ Webhook signature verified successfully")
|
|
77
|
+
return True
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: unique_toolkit
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.30.0
|
|
4
4
|
Summary:
|
|
5
5
|
License: Proprietary
|
|
6
6
|
Author: Cedric Klinkert
|
|
@@ -121,6 +121,12 @@ 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.30.0] - 2025-11-26
|
|
125
|
+
- Add option to only display parts of sub agent responses.
|
|
126
|
+
|
|
127
|
+
## [1.29.4] - 2025-11-25
|
|
128
|
+
- Add display name to openai builtin tools
|
|
129
|
+
|
|
124
130
|
## [1.29.3] - 2025-11-24
|
|
125
131
|
- Fix jinja utility helpers import
|
|
126
132
|
|
|
@@ -82,12 +82,12 @@ unique_toolkit/agentic/tools/a2a/evaluation/evaluator.py,sha256=AJvXu0UJKHe72nRm
|
|
|
82
82
|
unique_toolkit/agentic/tools/a2a/evaluation/summarization_user_message.j2,sha256=acP1YqD_sCy6DT0V2EIfhQTmaUKeqpeWNJ7RGgceo8I,271
|
|
83
83
|
unique_toolkit/agentic/tools/a2a/manager.py,sha256=pk06UUXKQdIUY-PyykYiItubBjmIydOaqWvBBDwhMN4,1939
|
|
84
84
|
unique_toolkit/agentic/tools/a2a/postprocessing/__init__.py,sha256=aVtUBPN7kDrqA6Bze34AbqQpcBBqpvfyJG-xF65w7R0,659
|
|
85
|
-
unique_toolkit/agentic/tools/a2a/postprocessing/_display_utils.py,sha256=
|
|
85
|
+
unique_toolkit/agentic/tools/a2a/postprocessing/_display_utils.py,sha256=lpcyCTm3KKhaYMF4Wh7DHrkP_WAOkbXha5QJc7zy2nc,6539
|
|
86
86
|
unique_toolkit/agentic/tools/a2a/postprocessing/_ref_utils.py,sha256=E3KybH9SX5oOkh14sSaIaLnht4DhKVrHAiUkoACNBsg,2430
|
|
87
|
-
unique_toolkit/agentic/tools/a2a/postprocessing/config.py,sha256=
|
|
88
|
-
unique_toolkit/agentic/tools/a2a/postprocessing/display.py,sha256=
|
|
87
|
+
unique_toolkit/agentic/tools/a2a/postprocessing/config.py,sha256=U5N2y6ok14daNf_u-E1SXnV9v9DwpW4jL_U_3o8oJ7Q,2633
|
|
88
|
+
unique_toolkit/agentic/tools/a2a/postprocessing/display.py,sha256=9t6zsVAYCyGXMHkxyH_XHdIbU0w0WBY1O08wjJvjoig,7850
|
|
89
89
|
unique_toolkit/agentic/tools/a2a/postprocessing/references.py,sha256=DGiv8WXMjIwumI7tlpWRgV8wSxnE282ryxEf03fgck8,3465
|
|
90
|
-
unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display_utils.py,sha256=
|
|
90
|
+
unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display_utils.py,sha256=mj2kR96ZzVIcgW3x-70oAjOLgYl5QreCIWBum1eQE_I,51989
|
|
91
91
|
unique_toolkit/agentic/tools/a2a/postprocessing/test/test_ref_utils.py,sha256=u-eNrHjsRFcu4TmzkD5XfFrxvaIToB42YGyXZ-RpsR0,17830
|
|
92
92
|
unique_toolkit/agentic/tools/a2a/prompts.py,sha256=0ILHL_RAcT04gFm2d470j4Gho7PoJXdCJy-bkZgf_wk,2401
|
|
93
93
|
unique_toolkit/agentic/tools/a2a/response_watcher/__init__.py,sha256=fS8Lq49DZo5spMcP8QGTMWwSg80rYSr2pTfYDbssGYs,184
|
|
@@ -105,10 +105,10 @@ unique_toolkit/agentic/tools/mcp/manager.py,sha256=DPYwwDe6RSZyuPaxn-je49fP_qOOs
|
|
|
105
105
|
unique_toolkit/agentic/tools/mcp/models.py,sha256=OyCCb7Vwv1ftzC_OCpFkf3TX9Aeb74ZZagG-qK5peIU,722
|
|
106
106
|
unique_toolkit/agentic/tools/mcp/tool_wrapper.py,sha256=m1-Uf0pSM5SoEKsxm9WqBsPRrKhmYX5AsxpdSCHLdmw,8724
|
|
107
107
|
unique_toolkit/agentic/tools/openai_builtin/__init__.py,sha256=NdVjkTa3LbW-JHhzPRjinTmgOCtEv090Zr9jGZXmgqs,345
|
|
108
|
-
unique_toolkit/agentic/tools/openai_builtin/base.py,sha256=
|
|
108
|
+
unique_toolkit/agentic/tools/openai_builtin/base.py,sha256=qtUtv-iyid4GpTRKU7IwWuY_OCPHLKEi9x7MCftyLG0,1178
|
|
109
109
|
unique_toolkit/agentic/tools/openai_builtin/code_interpreter/__init__.py,sha256=w2vONpnC6hKRPoJGwzDuRtNBsQd_o-gMUqArgIl_5KY,305
|
|
110
110
|
unique_toolkit/agentic/tools/openai_builtin/code_interpreter/config.py,sha256=FdmVd4VeAv0E4CiZdPeZcODDYnmmSB4e_YQesIW8rU0,4266
|
|
111
|
-
unique_toolkit/agentic/tools/openai_builtin/code_interpreter/service.py,sha256=
|
|
111
|
+
unique_toolkit/agentic/tools/openai_builtin/code_interpreter/service.py,sha256=XXBBEUI9tKa8z1HCpv-lwKx3pSEEPeL1RUx9LGGPS04,8007
|
|
112
112
|
unique_toolkit/agentic/tools/openai_builtin/manager.py,sha256=QeDVgLfnCXrSmXI3b9bgQa9oyfQe_L15wa_YfhfNe9E,2633
|
|
113
113
|
unique_toolkit/agentic/tools/schemas.py,sha256=TXshRvivr2hD-McXHumO0bp-Z0mz_GnAmQRiVjT59rU,5025
|
|
114
114
|
unique_toolkit/agentic/tools/test/test_mcp_manager.py,sha256=VpB4k4Dh0lQWakilJMQSzO8sBXapuEC26cub_lorl-M,19221
|
|
@@ -124,8 +124,9 @@ unique_toolkit/agentic/tools/utils/source_handling/__init__.py,sha256=47DEQpj8HB
|
|
|
124
124
|
unique_toolkit/agentic/tools/utils/source_handling/schema.py,sha256=iHBKuks6tUy8tvian4Pd0B6_-8__SehVVNcxIUAUjEA,882
|
|
125
125
|
unique_toolkit/agentic/tools/utils/source_handling/source_formatting.py,sha256=uZ0QXqrPWgId3ZA67dvjHQ6xrW491LK1xxx_sVJmFHg,9160
|
|
126
126
|
unique_toolkit/agentic/tools/utils/source_handling/tests/test_source_formatting.py,sha256=EA8iVvb3L91OFk2XMbGcFuhe2etqm3Sx9QCYDGiOSOM,6995
|
|
127
|
-
unique_toolkit/app/__init__.py,sha256=
|
|
127
|
+
unique_toolkit/app/__init__.py,sha256=OaylhLwxeRlsHlcFGSlR5R7oREFsjv9wRdxuVZBYM_8,1371
|
|
128
128
|
unique_toolkit/app/dev_util.py,sha256=J20peCvrSQKfMGdYPYwCirs3Yq2v_e33GzNBzNKbWN4,5531
|
|
129
|
+
unique_toolkit/app/fast_api_factory.py,sha256=l6AZoR8RAh4IJxAHZR_ajAL5ojcGdVh7Z8pfSpDSEKM,4634
|
|
129
130
|
unique_toolkit/app/init_logging.py,sha256=Sh26SRxOj8i8dzobKhYha2lLrkrMTHfB1V4jR3h23gQ,678
|
|
130
131
|
unique_toolkit/app/init_sdk.py,sha256=5_oDoETr6akwYyBCb0ivTdMNu3SVgPSkrXcDS6ELyY8,2269
|
|
131
132
|
unique_toolkit/app/performance/async_tasks.py,sha256=H0l3OAcosLwNHZ8d2pd-Di4wHIXfclEvagi5kfqLFPA,1941
|
|
@@ -133,6 +134,7 @@ unique_toolkit/app/performance/async_wrapper.py,sha256=yVVcRDkcdyfjsxro-N29SBvi-
|
|
|
133
134
|
unique_toolkit/app/schemas.py,sha256=1KziC9FzPtjOJ1R2ZoeJqVV9PNjGuKJ9uIMvPpA2yus,10159
|
|
134
135
|
unique_toolkit/app/unique_settings.py,sha256=NTfa3a8wWzBDx4_4Irqyhy4mpXyPU6Munqs41ozPFnE,12366
|
|
135
136
|
unique_toolkit/app/verification.py,sha256=GxFFwcJMy25fCA_Xe89wKW7bgqOu8PAs5y8QpHF0GSc,3861
|
|
137
|
+
unique_toolkit/app/webhook.py,sha256=k7DP1UTR3p7D4qzuKPKVmGMAkDVHfALrnMIzTZqj_OI,2320
|
|
136
138
|
unique_toolkit/chat/__init__.py,sha256=uP7P6YPeOjEOvpX3bhcU6ND_m0QLr4wMklcrnAKK0q4,804
|
|
137
139
|
unique_toolkit/chat/constants.py,sha256=05kq6zjqUVB2d6_P7s-90nbljpB3ryxwCI-CAz0r2O4,83
|
|
138
140
|
unique_toolkit/chat/deprecated/service.py,sha256=CYwzXi7OB0RjHd73CO2jq8SlpdBmDYLatzPFkb5sA0k,6529
|
|
@@ -187,7 +189,7 @@ unique_toolkit/short_term_memory/service.py,sha256=5PeVBu1ZCAfyDb2HLVvlmqSbyzBBu
|
|
|
187
189
|
unique_toolkit/smart_rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
188
190
|
unique_toolkit/smart_rules/compile.py,sha256=Ozhh70qCn2yOzRWr9d8WmJeTo7AQurwd3tStgBMPFLA,1246
|
|
189
191
|
unique_toolkit/test_utilities/events.py,sha256=_mwV2bs5iLjxS1ynDCjaIq-gjjKhXYCK-iy3dRfvO3g,6410
|
|
190
|
-
unique_toolkit-1.
|
|
191
|
-
unique_toolkit-1.
|
|
192
|
-
unique_toolkit-1.
|
|
193
|
-
unique_toolkit-1.
|
|
192
|
+
unique_toolkit-1.30.0.dist-info/LICENSE,sha256=GlN8wHNdh53xwOPg44URnwag6TEolCjoq3YD_KrWgss,193
|
|
193
|
+
unique_toolkit-1.30.0.dist-info/METADATA,sha256=xCyY5fyJ2rm-5VAC72GF7XB0AdFyU_T1FCGSCJjUMWk,44547
|
|
194
|
+
unique_toolkit-1.30.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
195
|
+
unique_toolkit-1.30.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|