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.
Files changed (44) hide show
  1. unique_toolkit/__init__.py +12 -6
  2. unique_toolkit/_common/docx_generator/service.py +8 -32
  3. unique_toolkit/_common/utils/jinja/helpers.py +10 -0
  4. unique_toolkit/_common/utils/jinja/render.py +18 -0
  5. unique_toolkit/_common/utils/jinja/schema.py +65 -0
  6. unique_toolkit/_common/utils/jinja/utils.py +80 -0
  7. unique_toolkit/agentic/message_log_manager/service.py +9 -0
  8. unique_toolkit/agentic/tools/a2a/postprocessing/_display_utils.py +58 -3
  9. unique_toolkit/agentic/tools/a2a/postprocessing/_ref_utils.py +11 -0
  10. unique_toolkit/agentic/tools/a2a/postprocessing/config.py +33 -0
  11. unique_toolkit/agentic/tools/a2a/postprocessing/display.py +99 -15
  12. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display.py +421 -0
  13. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display_utils.py +768 -0
  14. unique_toolkit/agentic/tools/a2a/tool/config.py +77 -1
  15. unique_toolkit/agentic/tools/a2a/tool/service.py +67 -3
  16. unique_toolkit/agentic/tools/config.py +5 -45
  17. unique_toolkit/agentic/tools/openai_builtin/base.py +4 -0
  18. unique_toolkit/agentic/tools/openai_builtin/code_interpreter/service.py +4 -0
  19. unique_toolkit/agentic/tools/tool_manager.py +16 -19
  20. unique_toolkit/app/__init__.py +3 -0
  21. unique_toolkit/app/fast_api_factory.py +131 -0
  22. unique_toolkit/app/webhook.py +77 -0
  23. unique_toolkit/chat/functions.py +1 -1
  24. unique_toolkit/content/functions.py +4 -4
  25. unique_toolkit/content/service.py +1 -1
  26. unique_toolkit/data_extraction/README.md +96 -0
  27. unique_toolkit/data_extraction/__init__.py +11 -0
  28. unique_toolkit/data_extraction/augmented/__init__.py +5 -0
  29. unique_toolkit/data_extraction/augmented/service.py +93 -0
  30. unique_toolkit/data_extraction/base.py +25 -0
  31. unique_toolkit/data_extraction/basic/__init__.py +11 -0
  32. unique_toolkit/data_extraction/basic/config.py +18 -0
  33. unique_toolkit/data_extraction/basic/prompt.py +13 -0
  34. unique_toolkit/data_extraction/basic/service.py +55 -0
  35. unique_toolkit/embedding/service.py +1 -1
  36. unique_toolkit/framework_utilities/langchain/__init__.py +10 -0
  37. unique_toolkit/framework_utilities/openai/client.py +2 -1
  38. unique_toolkit/language_model/infos.py +22 -1
  39. unique_toolkit/services/knowledge_base.py +4 -6
  40. {unique_toolkit-1.28.8.dist-info → unique_toolkit-1.33.3.dist-info}/METADATA +51 -2
  41. {unique_toolkit-1.28.8.dist-info → unique_toolkit-1.33.3.dist-info}/RECORD +43 -27
  42. unique_toolkit/agentic/tools/test/test_tool_manager.py +0 -1686
  43. {unique_toolkit-1.28.8.dist-info → unique_toolkit-1.33.3.dist-info}/LICENSE +0 -0
  44. {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