unique_toolkit 1.28.8__py3-none-any.whl → 1.33.3__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/__init__.py +12 -6
- unique_toolkit/_common/docx_generator/service.py +8 -32
- unique_toolkit/_common/utils/jinja/helpers.py +10 -0
- unique_toolkit/_common/utils/jinja/render.py +18 -0
- unique_toolkit/_common/utils/jinja/schema.py +65 -0
- unique_toolkit/_common/utils/jinja/utils.py +80 -0
- unique_toolkit/agentic/message_log_manager/service.py +9 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/_display_utils.py +58 -3
- unique_toolkit/agentic/tools/a2a/postprocessing/_ref_utils.py +11 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/config.py +33 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/display.py +99 -15
- unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display.py +421 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display_utils.py +768 -0
- unique_toolkit/agentic/tools/a2a/tool/config.py +77 -1
- unique_toolkit/agentic/tools/a2a/tool/service.py +67 -3
- unique_toolkit/agentic/tools/config.py +5 -45
- unique_toolkit/agentic/tools/openai_builtin/base.py +4 -0
- unique_toolkit/agentic/tools/openai_builtin/code_interpreter/service.py +4 -0
- unique_toolkit/agentic/tools/tool_manager.py +16 -19
- 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/chat/functions.py +1 -1
- unique_toolkit/content/functions.py +4 -4
- unique_toolkit/content/service.py +1 -1
- unique_toolkit/data_extraction/README.md +96 -0
- unique_toolkit/data_extraction/__init__.py +11 -0
- unique_toolkit/data_extraction/augmented/__init__.py +5 -0
- unique_toolkit/data_extraction/augmented/service.py +93 -0
- unique_toolkit/data_extraction/base.py +25 -0
- unique_toolkit/data_extraction/basic/__init__.py +11 -0
- unique_toolkit/data_extraction/basic/config.py +18 -0
- unique_toolkit/data_extraction/basic/prompt.py +13 -0
- unique_toolkit/data_extraction/basic/service.py +55 -0
- unique_toolkit/embedding/service.py +1 -1
- unique_toolkit/framework_utilities/langchain/__init__.py +10 -0
- unique_toolkit/framework_utilities/openai/client.py +2 -1
- unique_toolkit/language_model/infos.py +22 -1
- unique_toolkit/services/knowledge_base.py +4 -6
- {unique_toolkit-1.28.8.dist-info → unique_toolkit-1.33.3.dist-info}/METADATA +51 -2
- {unique_toolkit-1.28.8.dist-info → unique_toolkit-1.33.3.dist-info}/RECORD +43 -27
- unique_toolkit/agentic/tools/test/test_tool_manager.py +0 -1686
- {unique_toolkit-1.28.8.dist-info → unique_toolkit-1.33.3.dist-info}/LICENSE +0 -0
- {unique_toolkit-1.28.8.dist-info → unique_toolkit-1.33.3.dist-info}/WHEEL +0 -0
|
@@ -5,6 +5,7 @@ import re
|
|
|
5
5
|
import pytest
|
|
6
6
|
|
|
7
7
|
from unique_toolkit.agentic.tools.a2a.postprocessing._display_utils import (
|
|
8
|
+
SubAgentAnswerPart,
|
|
8
9
|
_add_line_break,
|
|
9
10
|
_get_display_removal_re,
|
|
10
11
|
_get_display_template,
|
|
@@ -16,9 +17,12 @@ from unique_toolkit.agentic.tools.a2a.postprocessing._display_utils import (
|
|
|
16
17
|
_wrap_with_details_tag,
|
|
17
18
|
_wrap_with_quote_border,
|
|
18
19
|
get_sub_agent_answer_display,
|
|
20
|
+
get_sub_agent_answer_from_parts,
|
|
21
|
+
get_sub_agent_answer_parts,
|
|
19
22
|
remove_sub_agent_answer_from_text,
|
|
20
23
|
)
|
|
21
24
|
from unique_toolkit.agentic.tools.a2a.postprocessing.config import (
|
|
25
|
+
SubAgentAnswerSubstringConfig,
|
|
22
26
|
SubAgentDisplayConfig,
|
|
23
27
|
SubAgentResponseDisplayMode,
|
|
24
28
|
)
|
|
@@ -1333,3 +1337,767 @@ def test_remove_sub_agent_answer__no_op_when_assistant_not_found() -> None:
|
|
|
1333
1337
|
assert result == original_text
|
|
1334
1338
|
assert "Present answer" in result
|
|
1335
1339
|
assert "Present Agent" in result
|
|
1340
|
+
|
|
1341
|
+
|
|
1342
|
+
# Test get_sub_agent_answer_parts
|
|
1343
|
+
|
|
1344
|
+
|
|
1345
|
+
@pytest.mark.ai
|
|
1346
|
+
def test_get_sub_agent_answer_parts__returns_empty__when_hidden_mode() -> None:
|
|
1347
|
+
"""
|
|
1348
|
+
Purpose: Verify empty list returned for HIDDEN display mode.
|
|
1349
|
+
Why this matters: Hidden mode should not extract any answer parts.
|
|
1350
|
+
Setup summary: Set mode to HIDDEN, assert empty list.
|
|
1351
|
+
"""
|
|
1352
|
+
# Arrange
|
|
1353
|
+
answer = "Some answer text"
|
|
1354
|
+
config = SubAgentDisplayConfig(
|
|
1355
|
+
mode=SubAgentResponseDisplayMode.HIDDEN,
|
|
1356
|
+
)
|
|
1357
|
+
|
|
1358
|
+
# Act
|
|
1359
|
+
result = get_sub_agent_answer_parts(answer=answer, display_config=config)
|
|
1360
|
+
|
|
1361
|
+
# Assert
|
|
1362
|
+
assert result == []
|
|
1363
|
+
|
|
1364
|
+
|
|
1365
|
+
@pytest.mark.ai
|
|
1366
|
+
def test_get_sub_agent_answer_parts__returns_full_answer__when_no_config() -> None:
|
|
1367
|
+
"""
|
|
1368
|
+
Purpose: Verify full answer returned when no substring config provided.
|
|
1369
|
+
Why this matters: Default behavior should return entire answer.
|
|
1370
|
+
Setup summary: Provide answer without substring config, assert full answer.
|
|
1371
|
+
"""
|
|
1372
|
+
# Arrange
|
|
1373
|
+
answer = "This is the complete answer"
|
|
1374
|
+
config = SubAgentDisplayConfig(
|
|
1375
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1376
|
+
answer_substrings_config=[],
|
|
1377
|
+
)
|
|
1378
|
+
|
|
1379
|
+
# Act
|
|
1380
|
+
result = get_sub_agent_answer_parts(answer=answer, display_config=config)
|
|
1381
|
+
|
|
1382
|
+
# Assert
|
|
1383
|
+
assert len(result) == 1
|
|
1384
|
+
assert result[0].matching_text == answer
|
|
1385
|
+
assert result[0].formatted_text == answer
|
|
1386
|
+
|
|
1387
|
+
|
|
1388
|
+
@pytest.mark.ai
|
|
1389
|
+
def test_get_sub_agent_answer_parts__extracts_single_match__with_one_regexp() -> None:
|
|
1390
|
+
"""
|
|
1391
|
+
Purpose: Verify single substring extracted with one regexp config.
|
|
1392
|
+
Why this matters: Core functionality for extracting specific answer parts.
|
|
1393
|
+
Setup summary: Provide answer with single regexp config, assert match extracted.
|
|
1394
|
+
"""
|
|
1395
|
+
# Arrange
|
|
1396
|
+
answer = "The price is $42.99 for the item"
|
|
1397
|
+
config = SubAgentDisplayConfig(
|
|
1398
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1399
|
+
answer_substrings_config=[
|
|
1400
|
+
SubAgentAnswerSubstringConfig(regexp=r"\$\d+\.\d+"),
|
|
1401
|
+
],
|
|
1402
|
+
)
|
|
1403
|
+
|
|
1404
|
+
# Act
|
|
1405
|
+
result = get_sub_agent_answer_parts(answer=answer, display_config=config)
|
|
1406
|
+
|
|
1407
|
+
# Assert
|
|
1408
|
+
assert len(result) == 1
|
|
1409
|
+
assert result[0].matching_text == "$42.99"
|
|
1410
|
+
assert result[0].formatted_text == "$42.99"
|
|
1411
|
+
|
|
1412
|
+
|
|
1413
|
+
@pytest.mark.ai
|
|
1414
|
+
def test_get_sub_agent_answer_parts__extracts_multiple_matches__with_multiple_regexps() -> (
|
|
1415
|
+
None
|
|
1416
|
+
):
|
|
1417
|
+
"""
|
|
1418
|
+
Purpose: Verify multiple substrings extracted with multiple regexp configs.
|
|
1419
|
+
Why this matters: Supports extracting different types of information.
|
|
1420
|
+
Setup summary: Provide answer with multiple regexp configs, assert all matches.
|
|
1421
|
+
"""
|
|
1422
|
+
# Arrange
|
|
1423
|
+
answer = "Contact John at john@example.com or call 555-1234"
|
|
1424
|
+
config = SubAgentDisplayConfig(
|
|
1425
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1426
|
+
answer_substrings_config=[
|
|
1427
|
+
SubAgentAnswerSubstringConfig(
|
|
1428
|
+
regexp=r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b"
|
|
1429
|
+
),
|
|
1430
|
+
SubAgentAnswerSubstringConfig(regexp=r"\d{3}-\d{4}"),
|
|
1431
|
+
],
|
|
1432
|
+
)
|
|
1433
|
+
|
|
1434
|
+
# Act
|
|
1435
|
+
result = get_sub_agent_answer_parts(answer=answer, display_config=config)
|
|
1436
|
+
|
|
1437
|
+
# Assert
|
|
1438
|
+
assert len(result) == 2
|
|
1439
|
+
assert result[0].matching_text == "john@example.com"
|
|
1440
|
+
assert result[1].matching_text == "555-1234"
|
|
1441
|
+
|
|
1442
|
+
|
|
1443
|
+
@pytest.mark.ai
|
|
1444
|
+
def test_get_sub_agent_answer_parts__applies_display_template__to_matched_text() -> (
|
|
1445
|
+
None
|
|
1446
|
+
):
|
|
1447
|
+
"""
|
|
1448
|
+
Purpose: Verify display template is applied to format matched text.
|
|
1449
|
+
Why this matters: Allows customization of how extracted parts are displayed.
|
|
1450
|
+
Setup summary: Provide template with placeholder, assert formatted output.
|
|
1451
|
+
"""
|
|
1452
|
+
# Arrange
|
|
1453
|
+
answer = "The temperature is 72 degrees"
|
|
1454
|
+
config = SubAgentDisplayConfig(
|
|
1455
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1456
|
+
answer_substrings_config=[
|
|
1457
|
+
SubAgentAnswerSubstringConfig(
|
|
1458
|
+
regexp=r"\d+",
|
|
1459
|
+
display_template="Temperature: {}°F",
|
|
1460
|
+
),
|
|
1461
|
+
],
|
|
1462
|
+
)
|
|
1463
|
+
|
|
1464
|
+
# Act
|
|
1465
|
+
result = get_sub_agent_answer_parts(answer=answer, display_config=config)
|
|
1466
|
+
|
|
1467
|
+
# Assert
|
|
1468
|
+
assert len(result) == 1
|
|
1469
|
+
assert result[0].matching_text == "72"
|
|
1470
|
+
assert result[0].formatted_text == "Temperature: 72°F"
|
|
1471
|
+
|
|
1472
|
+
|
|
1473
|
+
@pytest.mark.ai
|
|
1474
|
+
def test_get_sub_agent_answer_parts__returns_empty_list__when_no_matches() -> None:
|
|
1475
|
+
"""
|
|
1476
|
+
Purpose: Verify empty list returned when regexp doesn't match answer.
|
|
1477
|
+
Why this matters: Handles cases where expected pattern not present.
|
|
1478
|
+
Setup summary: Provide regexp that doesn't match, assert empty list.
|
|
1479
|
+
"""
|
|
1480
|
+
# Arrange
|
|
1481
|
+
answer = "This is plain text without numbers"
|
|
1482
|
+
config = SubAgentDisplayConfig(
|
|
1483
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1484
|
+
answer_substrings_config=[
|
|
1485
|
+
SubAgentAnswerSubstringConfig(regexp=r"\d+"),
|
|
1486
|
+
],
|
|
1487
|
+
)
|
|
1488
|
+
|
|
1489
|
+
# Act
|
|
1490
|
+
result = get_sub_agent_answer_parts(answer=answer, display_config=config)
|
|
1491
|
+
|
|
1492
|
+
# Assert
|
|
1493
|
+
assert result == []
|
|
1494
|
+
|
|
1495
|
+
|
|
1496
|
+
@pytest.mark.ai
|
|
1497
|
+
def test_get_sub_agent_answer_parts__extracts_all_matches__for_each_regexp() -> None:
|
|
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) == 2
|
|
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) == 4
|
|
1665
|
+
# Results follow config order, not text order
|
|
1666
|
+
assert result[0].matching_text == "first"
|
|
1667
|
+
assert result[1].matching_text == "then"
|
|
1668
|
+
assert result[2].matching_text == "abc"
|
|
1669
|
+
assert result[3].matching_text == "123"
|
|
1670
|
+
|
|
1671
|
+
|
|
1672
|
+
@pytest.mark.ai
|
|
1673
|
+
def test_get_sub_agent_answer_parts__handles_complex_template__with_multiple_placeholders() -> (
|
|
1674
|
+
None
|
|
1675
|
+
):
|
|
1676
|
+
"""
|
|
1677
|
+
Purpose: Verify complex display templates with formatting work correctly.
|
|
1678
|
+
Why this matters: Supports rich formatting of extracted content.
|
|
1679
|
+
Setup summary: Provide template with additional text, assert formatted correctly.
|
|
1680
|
+
"""
|
|
1681
|
+
# Arrange
|
|
1682
|
+
answer = "User score: 95"
|
|
1683
|
+
config = SubAgentDisplayConfig(
|
|
1684
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1685
|
+
answer_substrings_config=[
|
|
1686
|
+
SubAgentAnswerSubstringConfig(
|
|
1687
|
+
regexp=r"\d+",
|
|
1688
|
+
display_template="**Score: {}%**",
|
|
1689
|
+
),
|
|
1690
|
+
],
|
|
1691
|
+
)
|
|
1692
|
+
|
|
1693
|
+
# Act
|
|
1694
|
+
result = get_sub_agent_answer_parts(answer=answer, display_config=config)
|
|
1695
|
+
|
|
1696
|
+
# Assert
|
|
1697
|
+
assert len(result) == 1
|
|
1698
|
+
assert result[0].matching_text == "95"
|
|
1699
|
+
assert result[0].formatted_text == "**Score: 95%**"
|
|
1700
|
+
|
|
1701
|
+
|
|
1702
|
+
@pytest.mark.ai
|
|
1703
|
+
def test_get_sub_agent_answer_parts__works_with_details_modes__extracts_normally() -> (
|
|
1704
|
+
None
|
|
1705
|
+
):
|
|
1706
|
+
"""
|
|
1707
|
+
Purpose: Verify extraction works regardless of display mode (except HIDDEN).
|
|
1708
|
+
Why this matters: Substring extraction independent of display mode.
|
|
1709
|
+
Setup summary: Use DETAILS modes, assert extraction still works.
|
|
1710
|
+
"""
|
|
1711
|
+
# Arrange
|
|
1712
|
+
answer = "Result: SUCCESS"
|
|
1713
|
+
config = SubAgentDisplayConfig(
|
|
1714
|
+
mode=SubAgentResponseDisplayMode.DETAILS_CLOSED,
|
|
1715
|
+
answer_substrings_config=[
|
|
1716
|
+
SubAgentAnswerSubstringConfig(regexp=r"SUCCESS"),
|
|
1717
|
+
],
|
|
1718
|
+
)
|
|
1719
|
+
|
|
1720
|
+
# Act
|
|
1721
|
+
result = get_sub_agent_answer_parts(answer=answer, display_config=config)
|
|
1722
|
+
|
|
1723
|
+
# Assert
|
|
1724
|
+
assert len(result) == 1
|
|
1725
|
+
assert result[0].matching_text == "SUCCESS"
|
|
1726
|
+
|
|
1727
|
+
|
|
1728
|
+
# Test get_sub_agent_answer_from_parts
|
|
1729
|
+
|
|
1730
|
+
|
|
1731
|
+
@pytest.mark.ai
|
|
1732
|
+
def test_get_sub_agent_answer_from_parts__returns_empty__with_empty_list() -> None:
|
|
1733
|
+
"""
|
|
1734
|
+
Purpose: Verify empty or minimal output when no answer parts provided.
|
|
1735
|
+
Why this matters: Handles edge case of no extracted content.
|
|
1736
|
+
Setup summary: Provide empty list, assert minimal rendered output.
|
|
1737
|
+
"""
|
|
1738
|
+
# Arrange
|
|
1739
|
+
answer_parts: list[SubAgentAnswerPart] = []
|
|
1740
|
+
config = SubAgentDisplayConfig(
|
|
1741
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1742
|
+
)
|
|
1743
|
+
|
|
1744
|
+
# Act
|
|
1745
|
+
result = get_sub_agent_answer_from_parts(
|
|
1746
|
+
answer_parts=answer_parts,
|
|
1747
|
+
config=config,
|
|
1748
|
+
)
|
|
1749
|
+
|
|
1750
|
+
# Assert
|
|
1751
|
+
assert result == ""
|
|
1752
|
+
|
|
1753
|
+
|
|
1754
|
+
@pytest.mark.ai
|
|
1755
|
+
def test_get_sub_agent_answer_from_parts__renders_single_part__with_default_template() -> (
|
|
1756
|
+
None
|
|
1757
|
+
):
|
|
1758
|
+
"""
|
|
1759
|
+
Purpose: Verify single answer part is rendered using default template.
|
|
1760
|
+
Why this matters: Core functionality for single substring display.
|
|
1761
|
+
Setup summary: Provide single part with default template, assert rendered text.
|
|
1762
|
+
"""
|
|
1763
|
+
# Arrange
|
|
1764
|
+
answer_parts = [
|
|
1765
|
+
SubAgentAnswerPart(matching_text="42", formatted_text="The answer is 42"),
|
|
1766
|
+
]
|
|
1767
|
+
config = SubAgentDisplayConfig(
|
|
1768
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1769
|
+
)
|
|
1770
|
+
|
|
1771
|
+
# Act
|
|
1772
|
+
result = get_sub_agent_answer_from_parts(
|
|
1773
|
+
answer_parts=answer_parts,
|
|
1774
|
+
config=config,
|
|
1775
|
+
)
|
|
1776
|
+
|
|
1777
|
+
# Assert
|
|
1778
|
+
assert "The answer is 42" in result
|
|
1779
|
+
|
|
1780
|
+
|
|
1781
|
+
@pytest.mark.ai
|
|
1782
|
+
def test_get_sub_agent_answer_from_parts__renders_multiple_parts__with_default_template() -> (
|
|
1783
|
+
None
|
|
1784
|
+
):
|
|
1785
|
+
"""
|
|
1786
|
+
Purpose: Verify multiple answer parts are rendered with default template.
|
|
1787
|
+
Why this matters: Supports displaying multiple extracted substrings.
|
|
1788
|
+
Setup summary: Provide multiple parts, assert all rendered in output.
|
|
1789
|
+
"""
|
|
1790
|
+
# Arrange
|
|
1791
|
+
answer_parts = [
|
|
1792
|
+
SubAgentAnswerPart(
|
|
1793
|
+
matching_text="john@example.com", formatted_text="Email: john@example.com"
|
|
1794
|
+
),
|
|
1795
|
+
SubAgentAnswerPart(matching_text="555-1234", formatted_text="Phone: 555-1234"),
|
|
1796
|
+
SubAgentAnswerPart(matching_text="John Doe", formatted_text="Name: John Doe"),
|
|
1797
|
+
]
|
|
1798
|
+
config = SubAgentDisplayConfig(
|
|
1799
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1800
|
+
)
|
|
1801
|
+
|
|
1802
|
+
# Act
|
|
1803
|
+
result = get_sub_agent_answer_from_parts(
|
|
1804
|
+
answer_parts=answer_parts,
|
|
1805
|
+
config=config,
|
|
1806
|
+
)
|
|
1807
|
+
|
|
1808
|
+
# Assert
|
|
1809
|
+
assert "Email: john@example.com" in result
|
|
1810
|
+
assert "Phone: 555-1234" in result
|
|
1811
|
+
assert "Name: John Doe" in result
|
|
1812
|
+
|
|
1813
|
+
|
|
1814
|
+
@pytest.mark.ai
|
|
1815
|
+
def test_get_sub_agent_answer_from_parts__uses_formatted_text__not_matching_text() -> (
|
|
1816
|
+
None
|
|
1817
|
+
):
|
|
1818
|
+
"""
|
|
1819
|
+
Purpose: Verify function uses formatted_text from parts, not matching_text.
|
|
1820
|
+
Why this matters: Formatted text includes display template application.
|
|
1821
|
+
Setup summary: Provide parts with different matching vs formatted text, assert formatted used.
|
|
1822
|
+
"""
|
|
1823
|
+
# Arrange
|
|
1824
|
+
answer_parts = [
|
|
1825
|
+
SubAgentAnswerPart(matching_text="72", formatted_text="Temperature: 72°F"),
|
|
1826
|
+
]
|
|
1827
|
+
config = SubAgentDisplayConfig(
|
|
1828
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1829
|
+
)
|
|
1830
|
+
|
|
1831
|
+
# Act
|
|
1832
|
+
result = get_sub_agent_answer_from_parts(
|
|
1833
|
+
answer_parts=answer_parts,
|
|
1834
|
+
config=config,
|
|
1835
|
+
)
|
|
1836
|
+
|
|
1837
|
+
# Assert
|
|
1838
|
+
assert "Temperature: 72°F" in result
|
|
1839
|
+
assert result.count("72") == 1 # Only formatted version, not raw matching_text
|
|
1840
|
+
|
|
1841
|
+
|
|
1842
|
+
@pytest.mark.ai
|
|
1843
|
+
def test_get_sub_agent_answer_from_parts__renders_with_custom_template__single_part() -> (
|
|
1844
|
+
None
|
|
1845
|
+
):
|
|
1846
|
+
"""
|
|
1847
|
+
Purpose: Verify custom Jinja template is applied correctly for single part.
|
|
1848
|
+
Why this matters: Supports custom formatting via configuration.
|
|
1849
|
+
Setup summary: Provide custom template with HTML, assert custom rendering.
|
|
1850
|
+
"""
|
|
1851
|
+
# Arrange
|
|
1852
|
+
answer_parts = [
|
|
1853
|
+
SubAgentAnswerPart(matching_text="Success", formatted_text="Status: Success"),
|
|
1854
|
+
]
|
|
1855
|
+
custom_template = "<div class='result'>{{ substrings[0] }}</div>"
|
|
1856
|
+
config = SubAgentDisplayConfig(
|
|
1857
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1858
|
+
answer_substrings_jinja_template=custom_template,
|
|
1859
|
+
)
|
|
1860
|
+
|
|
1861
|
+
# Act
|
|
1862
|
+
result = get_sub_agent_answer_from_parts(
|
|
1863
|
+
answer_parts=answer_parts,
|
|
1864
|
+
config=config,
|
|
1865
|
+
)
|
|
1866
|
+
|
|
1867
|
+
# Assert
|
|
1868
|
+
assert result == "<div class='result'>Status: Success</div>"
|
|
1869
|
+
|
|
1870
|
+
|
|
1871
|
+
@pytest.mark.ai
|
|
1872
|
+
def test_get_sub_agent_answer_from_parts__renders_with_custom_template__multiple_parts() -> (
|
|
1873
|
+
None
|
|
1874
|
+
):
|
|
1875
|
+
"""
|
|
1876
|
+
Purpose: Verify custom template works with multiple parts and loop constructs.
|
|
1877
|
+
Why this matters: Supports complex formatting with iteration.
|
|
1878
|
+
Setup summary: Provide custom template with for loop, assert all parts rendered.
|
|
1879
|
+
"""
|
|
1880
|
+
# Arrange
|
|
1881
|
+
answer_parts = [
|
|
1882
|
+
SubAgentAnswerPart(matching_text="Item1", formatted_text="- Item 1"),
|
|
1883
|
+
SubAgentAnswerPart(matching_text="Item2", formatted_text="- Item 2"),
|
|
1884
|
+
SubAgentAnswerPart(matching_text="Item3", formatted_text="- Item 3"),
|
|
1885
|
+
]
|
|
1886
|
+
custom_template = """
|
|
1887
|
+
<ul>
|
|
1888
|
+
{% for substring in substrings %}
|
|
1889
|
+
<li>{{ substring }}</li>
|
|
1890
|
+
{% endfor %}
|
|
1891
|
+
</ul>
|
|
1892
|
+
""".strip()
|
|
1893
|
+
config = SubAgentDisplayConfig(
|
|
1894
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1895
|
+
answer_substrings_jinja_template=custom_template,
|
|
1896
|
+
)
|
|
1897
|
+
|
|
1898
|
+
# Act
|
|
1899
|
+
result = get_sub_agent_answer_from_parts(
|
|
1900
|
+
answer_parts=answer_parts,
|
|
1901
|
+
config=config,
|
|
1902
|
+
)
|
|
1903
|
+
|
|
1904
|
+
# Assert
|
|
1905
|
+
assert "<ul>" in result
|
|
1906
|
+
assert "</ul>" in result
|
|
1907
|
+
assert "<li>- Item 1</li>" in result
|
|
1908
|
+
assert "<li>- Item 2</li>" in result
|
|
1909
|
+
assert "<li>- Item 3</li>" in result
|
|
1910
|
+
|
|
1911
|
+
|
|
1912
|
+
@pytest.mark.ai
|
|
1913
|
+
def test_get_sub_agent_answer_from_parts__preserves_order__of_parts() -> None:
|
|
1914
|
+
"""
|
|
1915
|
+
Purpose: Verify parts are rendered in the order they appear in the list.
|
|
1916
|
+
Why this matters: Predictable output order based on input order.
|
|
1917
|
+
Setup summary: Provide parts in specific order, assert same order in output.
|
|
1918
|
+
"""
|
|
1919
|
+
# Arrange
|
|
1920
|
+
answer_parts = [
|
|
1921
|
+
SubAgentAnswerPart(matching_text="First", formatted_text="1. First"),
|
|
1922
|
+
SubAgentAnswerPart(matching_text="Second", formatted_text="2. Second"),
|
|
1923
|
+
SubAgentAnswerPart(matching_text="Third", formatted_text="3. Third"),
|
|
1924
|
+
]
|
|
1925
|
+
config = SubAgentDisplayConfig(
|
|
1926
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1927
|
+
)
|
|
1928
|
+
|
|
1929
|
+
# Act
|
|
1930
|
+
result = get_sub_agent_answer_from_parts(
|
|
1931
|
+
answer_parts=answer_parts,
|
|
1932
|
+
config=config,
|
|
1933
|
+
)
|
|
1934
|
+
|
|
1935
|
+
# Assert
|
|
1936
|
+
# Check order by finding positions
|
|
1937
|
+
pos_first = result.find("1. First")
|
|
1938
|
+
pos_second = result.find("2. Second")
|
|
1939
|
+
pos_third = result.find("3. Third")
|
|
1940
|
+
assert pos_first < pos_second < pos_third
|
|
1941
|
+
|
|
1942
|
+
|
|
1943
|
+
@pytest.mark.ai
|
|
1944
|
+
def test_get_sub_agent_answer_from_parts__handles_special_chars__in_formatted_text() -> (
|
|
1945
|
+
None
|
|
1946
|
+
):
|
|
1947
|
+
"""
|
|
1948
|
+
Purpose: Verify formatted text with special characters renders correctly.
|
|
1949
|
+
Why this matters: Answers may contain HTML entities or special symbols.
|
|
1950
|
+
Setup summary: Provide parts with special chars, assert rendered as-is.
|
|
1951
|
+
"""
|
|
1952
|
+
# Arrange
|
|
1953
|
+
answer_parts = [
|
|
1954
|
+
SubAgentAnswerPart(
|
|
1955
|
+
matching_text="test",
|
|
1956
|
+
formatted_text="Result: <tag> & 'quotes' & \"double\" & 50% & $100",
|
|
1957
|
+
),
|
|
1958
|
+
]
|
|
1959
|
+
config = SubAgentDisplayConfig(
|
|
1960
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1961
|
+
)
|
|
1962
|
+
|
|
1963
|
+
# Act
|
|
1964
|
+
result = get_sub_agent_answer_from_parts(
|
|
1965
|
+
answer_parts=answer_parts,
|
|
1966
|
+
config=config,
|
|
1967
|
+
)
|
|
1968
|
+
|
|
1969
|
+
# Assert
|
|
1970
|
+
# Jinja2 default behavior does not escape, so special chars should be preserved
|
|
1971
|
+
assert "<tag>" in result
|
|
1972
|
+
assert "&" in result
|
|
1973
|
+
assert "'" in result
|
|
1974
|
+
assert '"' in result
|
|
1975
|
+
assert "%" in result
|
|
1976
|
+
assert "$" in result
|
|
1977
|
+
|
|
1978
|
+
|
|
1979
|
+
@pytest.mark.ai
|
|
1980
|
+
def test_get_sub_agent_answer_from_parts__handles_multiline_formatted_text() -> None:
|
|
1981
|
+
"""
|
|
1982
|
+
Purpose: Verify formatted text with newlines renders correctly.
|
|
1983
|
+
Why this matters: Formatted content may span multiple lines.
|
|
1984
|
+
Setup summary: Provide parts with newlines, assert multiline output.
|
|
1985
|
+
"""
|
|
1986
|
+
# Arrange
|
|
1987
|
+
answer_parts = [
|
|
1988
|
+
SubAgentAnswerPart(
|
|
1989
|
+
matching_text="multiline",
|
|
1990
|
+
formatted_text="Line 1\nLine 2\nLine 3",
|
|
1991
|
+
),
|
|
1992
|
+
]
|
|
1993
|
+
config = SubAgentDisplayConfig(
|
|
1994
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
1995
|
+
)
|
|
1996
|
+
|
|
1997
|
+
# Act
|
|
1998
|
+
result = get_sub_agent_answer_from_parts(
|
|
1999
|
+
answer_parts=answer_parts,
|
|
2000
|
+
config=config,
|
|
2001
|
+
)
|
|
2002
|
+
|
|
2003
|
+
# Assert
|
|
2004
|
+
assert "Line 1" in result
|
|
2005
|
+
assert "Line 2" in result
|
|
2006
|
+
assert "Line 3" in result
|
|
2007
|
+
|
|
2008
|
+
|
|
2009
|
+
@pytest.mark.ai
|
|
2010
|
+
def test_get_sub_agent_answer_from_parts__works_with_custom_template__conditional_logic() -> (
|
|
2011
|
+
None
|
|
2012
|
+
):
|
|
2013
|
+
"""
|
|
2014
|
+
Purpose: Verify custom template with Jinja conditionals works correctly.
|
|
2015
|
+
Why this matters: Supports advanced formatting with conditional rendering.
|
|
2016
|
+
Setup summary: Provide custom template with if statement, assert conditional output.
|
|
2017
|
+
"""
|
|
2018
|
+
# Arrange
|
|
2019
|
+
answer_parts = [
|
|
2020
|
+
SubAgentAnswerPart(matching_text="a", formatted_text="Item A"),
|
|
2021
|
+
SubAgentAnswerPart(matching_text="b", formatted_text="Item B"),
|
|
2022
|
+
]
|
|
2023
|
+
custom_template = """
|
|
2024
|
+
{% if substrings|length > 1 %}
|
|
2025
|
+
Multiple items: {{ substrings|join(', ') }}
|
|
2026
|
+
{% else %}
|
|
2027
|
+
Single item: {{ substrings[0] }}
|
|
2028
|
+
{% endif %}
|
|
2029
|
+
""".strip()
|
|
2030
|
+
config = SubAgentDisplayConfig(
|
|
2031
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
2032
|
+
answer_substrings_jinja_template=custom_template,
|
|
2033
|
+
)
|
|
2034
|
+
|
|
2035
|
+
# Act
|
|
2036
|
+
result = get_sub_agent_answer_from_parts(
|
|
2037
|
+
answer_parts=answer_parts,
|
|
2038
|
+
config=config,
|
|
2039
|
+
)
|
|
2040
|
+
|
|
2041
|
+
# Assert
|
|
2042
|
+
assert "Multiple items:" in result
|
|
2043
|
+
assert "Item A, Item B" in result
|
|
2044
|
+
|
|
2045
|
+
|
|
2046
|
+
@pytest.mark.ai
|
|
2047
|
+
def test_get_sub_agent_answer_from_parts__empty_formatted_text__renders_empty() -> None:
|
|
2048
|
+
"""
|
|
2049
|
+
Purpose: Verify parts with empty formatted_text render as empty.
|
|
2050
|
+
Why this matters: Edge case handling for empty content.
|
|
2051
|
+
Setup summary: Provide parts with empty formatted_text, assert minimal output.
|
|
2052
|
+
"""
|
|
2053
|
+
# Arrange
|
|
2054
|
+
answer_parts = [
|
|
2055
|
+
SubAgentAnswerPart(matching_text="something", formatted_text=""),
|
|
2056
|
+
]
|
|
2057
|
+
config = SubAgentDisplayConfig(
|
|
2058
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
2059
|
+
)
|
|
2060
|
+
|
|
2061
|
+
# Act
|
|
2062
|
+
result = get_sub_agent_answer_from_parts(
|
|
2063
|
+
answer_parts=answer_parts,
|
|
2064
|
+
config=config,
|
|
2065
|
+
)
|
|
2066
|
+
|
|
2067
|
+
# Assert
|
|
2068
|
+
# Default template renders empty string for empty formatted_text
|
|
2069
|
+
assert result.strip() == ""
|
|
2070
|
+
|
|
2071
|
+
|
|
2072
|
+
@pytest.mark.ai
|
|
2073
|
+
def test_get_sub_agent_answer_from_parts__integration__with_get_sub_agent_answer_parts() -> (
|
|
2074
|
+
None
|
|
2075
|
+
):
|
|
2076
|
+
"""
|
|
2077
|
+
Purpose: Verify integration between get_sub_agent_answer_parts and get_sub_agent_answer_from_parts.
|
|
2078
|
+
Why this matters: These functions work together in the display pipeline.
|
|
2079
|
+
Setup summary: Use get_sub_agent_answer_parts output as input, assert complete workflow.
|
|
2080
|
+
"""
|
|
2081
|
+
# Arrange
|
|
2082
|
+
answer = "Contact: john@example.com or call 555-1234"
|
|
2083
|
+
config = SubAgentDisplayConfig(
|
|
2084
|
+
mode=SubAgentResponseDisplayMode.PLAIN,
|
|
2085
|
+
answer_substrings_config=[
|
|
2086
|
+
SubAgentAnswerSubstringConfig(
|
|
2087
|
+
regexp=r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b",
|
|
2088
|
+
display_template="Email: {}",
|
|
2089
|
+
),
|
|
2090
|
+
SubAgentAnswerSubstringConfig(
|
|
2091
|
+
regexp=r"\d{3}-\d{4}",
|
|
2092
|
+
display_template="Phone: {}",
|
|
2093
|
+
),
|
|
2094
|
+
],
|
|
2095
|
+
)
|
|
2096
|
+
|
|
2097
|
+
# Act
|
|
2098
|
+
parts = get_sub_agent_answer_parts(answer=answer, display_config=config)
|
|
2099
|
+
result = get_sub_agent_answer_from_parts(answer_parts=parts, config=config)
|
|
2100
|
+
|
|
2101
|
+
# Assert
|
|
2102
|
+
assert "Email: john@example.com" in result
|
|
2103
|
+
assert "Phone: 555-1234" in result
|