unique_toolkit 1.31.2__tar.gz → 1.32.1__tar.gz

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 (197) hide show
  1. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/CHANGELOG.md +6 -0
  2. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/PKG-INFO +7 -1
  3. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/pyproject.toml +1 -1
  4. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/a2a/postprocessing/_display_utils.py +15 -5
  5. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/a2a/postprocessing/_ref_utils.py +11 -0
  6. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/a2a/postprocessing/display.py +66 -11
  7. unique_toolkit-1.32.1/unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display.py +421 -0
  8. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display_utils.py +380 -0
  9. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/chat/functions.py +1 -1
  10. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/content/functions.py +4 -4
  11. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/content/service.py +1 -1
  12. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/embedding/service.py +1 -1
  13. unique_toolkit-1.32.1/unique_toolkit/framework_utilities/langchain/__init__.py +10 -0
  14. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/framework_utilities/openai/client.py +2 -1
  15. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/services/knowledge_base.py +4 -6
  16. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/LICENSE +0 -0
  17. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/README.md +0 -0
  18. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/__init__.py +0 -0
  19. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/_common/_base_service.py +0 -0
  20. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/_common/_time_utils.py +0 -0
  21. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/_common/api_calling/human_verification_manager.py +0 -0
  22. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/_common/base_model_type_attribute.py +0 -0
  23. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/_common/chunk_relevancy_sorter/config.py +0 -0
  24. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/_common/chunk_relevancy_sorter/exception.py +0 -0
  25. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/_common/chunk_relevancy_sorter/schemas.py +0 -0
  26. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/_common/chunk_relevancy_sorter/service.py +0 -0
  27. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/_common/chunk_relevancy_sorter/tests/test_service.py +0 -0
  28. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/_common/default_language_model.py +0 -0
  29. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/_common/docx_generator/__init__.py +0 -0
  30. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/_common/docx_generator/config.py +0 -0
  31. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/_common/docx_generator/schemas.py +0 -0
  32. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/_common/docx_generator/service.py +0 -0
  33. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/_common/docx_generator/template/Doc Template.docx +0 -0
  34. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/_common/endpoint_builder.py +0 -0
  35. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/_common/endpoint_requestor.py +0 -0
  36. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/_common/exception.py +0 -0
  37. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/_common/experimental/endpoint_builder.py +0 -0
  38. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/_common/experimental/endpoint_requestor.py +0 -0
  39. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/_common/feature_flags/schema.py +0 -0
  40. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/_common/pydantic/rjsf_tags.py +0 -0
  41. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/_common/pydantic_helpers.py +0 -0
  42. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/_common/referencing.py +0 -0
  43. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/_common/string_utilities.py +0 -0
  44. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/_common/tests/test_referencing.py +0 -0
  45. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/_common/tests/test_string_utilities.py +0 -0
  46. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/_common/token/image_token_counting.py +0 -0
  47. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/_common/token/token_counting.py +0 -0
  48. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/_common/utils/__init__.py +0 -0
  49. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/_common/utils/files.py +0 -0
  50. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/_common/utils/image/encode.py +0 -0
  51. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/_common/utils/jinja/helpers.py +0 -0
  52. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/_common/utils/jinja/render.py +0 -0
  53. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/_common/utils/jinja/schema.py +0 -0
  54. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/_common/utils/jinja/utils.py +0 -0
  55. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/_common/utils/structured_output/__init__.py +0 -0
  56. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/_common/utils/structured_output/schema.py +0 -0
  57. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/_common/utils/write_configuration.py +0 -0
  58. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/_common/validate_required_values.py +0 -0
  59. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/_common/validators.py +0 -0
  60. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/__init__.py +0 -0
  61. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/debug_info_manager/debug_info_manager.py +0 -0
  62. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/debug_info_manager/test/test_debug_info_manager.py +0 -0
  63. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/evaluation/config.py +0 -0
  64. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/evaluation/context_relevancy/prompts.py +0 -0
  65. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/evaluation/context_relevancy/schema.py +0 -0
  66. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/evaluation/context_relevancy/service.py +0 -0
  67. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/evaluation/evaluation_manager.py +0 -0
  68. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/evaluation/exception.py +0 -0
  69. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/evaluation/hallucination/constants.py +0 -0
  70. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/evaluation/hallucination/hallucination_evaluation.py +0 -0
  71. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/evaluation/hallucination/prompts.py +0 -0
  72. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/evaluation/hallucination/service.py +0 -0
  73. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/evaluation/hallucination/utils.py +0 -0
  74. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/evaluation/output_parser.py +0 -0
  75. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/evaluation/schemas.py +0 -0
  76. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/evaluation/tests/test_context_relevancy_service.py +0 -0
  77. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/evaluation/tests/test_output_parser.py +0 -0
  78. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/history_manager/history_construction_with_contents.py +0 -0
  79. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/history_manager/history_manager.py +0 -0
  80. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/history_manager/loop_token_reducer.py +0 -0
  81. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/history_manager/utils.py +0 -0
  82. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/message_log_manager/__init__.py +0 -0
  83. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/message_log_manager/service.py +0 -0
  84. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/postprocessor/postprocessor_manager.py +0 -0
  85. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/reference_manager/reference_manager.py +0 -0
  86. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/responses_api/__init__.py +0 -0
  87. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/responses_api/postprocessors/code_display.py +0 -0
  88. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/responses_api/postprocessors/generated_files.py +0 -0
  89. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/responses_api/stream_handler.py +0 -0
  90. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/short_term_memory_manager/persistent_short_term_memory_manager.py +0 -0
  91. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/thinking_manager/thinking_manager.py +0 -0
  92. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/__init__.py +0 -0
  93. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/a2a/__init__.py +0 -0
  94. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/a2a/config.py +0 -0
  95. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/a2a/evaluation/__init__.py +0 -0
  96. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/a2a/evaluation/_utils.py +0 -0
  97. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/a2a/evaluation/config.py +0 -0
  98. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/a2a/evaluation/evaluator.py +0 -0
  99. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/a2a/evaluation/summarization_user_message.j2 +0 -0
  100. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/a2a/manager.py +0 -0
  101. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/a2a/postprocessing/__init__.py +0 -0
  102. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/a2a/postprocessing/config.py +0 -0
  103. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/a2a/postprocessing/references.py +0 -0
  104. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/a2a/postprocessing/test/test_ref_utils.py +0 -0
  105. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/a2a/prompts.py +0 -0
  106. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/a2a/response_watcher/__init__.py +0 -0
  107. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/a2a/response_watcher/service.py +0 -0
  108. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/a2a/tool/__init__.py +0 -0
  109. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/a2a/tool/_memory.py +0 -0
  110. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/a2a/tool/_schema.py +0 -0
  111. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/a2a/tool/config.py +0 -0
  112. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/a2a/tool/service.py +0 -0
  113. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/agent_chunks_hanlder.py +0 -0
  114. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/config.py +0 -0
  115. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/factory.py +0 -0
  116. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/mcp/__init__.py +0 -0
  117. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/mcp/manager.py +0 -0
  118. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/mcp/models.py +0 -0
  119. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/mcp/tool_wrapper.py +0 -0
  120. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/openai_builtin/__init__.py +0 -0
  121. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/openai_builtin/base.py +0 -0
  122. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/openai_builtin/code_interpreter/__init__.py +0 -0
  123. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/openai_builtin/code_interpreter/config.py +0 -0
  124. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/openai_builtin/code_interpreter/service.py +0 -0
  125. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/openai_builtin/manager.py +0 -0
  126. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/schemas.py +0 -0
  127. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/test/test_mcp_manager.py +0 -0
  128. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/test/test_tool_progress_reporter.py +0 -0
  129. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/tool.py +0 -0
  130. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/tool_manager.py +0 -0
  131. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/tool_progress_reporter.py +0 -0
  132. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/utils/__init__.py +0 -0
  133. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/utils/execution/__init__.py +0 -0
  134. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/utils/execution/execution.py +0 -0
  135. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/utils/source_handling/__init__.py +0 -0
  136. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/utils/source_handling/schema.py +0 -0
  137. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/utils/source_handling/source_formatting.py +0 -0
  138. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/agentic/tools/utils/source_handling/tests/test_source_formatting.py +0 -0
  139. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/app/__init__.py +0 -0
  140. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/app/dev_util.py +0 -0
  141. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/app/fast_api_factory.py +0 -0
  142. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/app/init_logging.py +0 -0
  143. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/app/init_sdk.py +0 -0
  144. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/app/performance/async_tasks.py +0 -0
  145. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/app/performance/async_wrapper.py +0 -0
  146. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/app/schemas.py +0 -0
  147. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/app/unique_settings.py +0 -0
  148. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/app/verification.py +0 -0
  149. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/app/webhook.py +0 -0
  150. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/chat/__init__.py +0 -0
  151. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/chat/constants.py +0 -0
  152. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/chat/deprecated/service.py +0 -0
  153. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/chat/rendering.py +0 -0
  154. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/chat/responses_api.py +0 -0
  155. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/chat/schemas.py +0 -0
  156. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/chat/service.py +0 -0
  157. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/chat/state.py +0 -0
  158. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/chat/utils.py +0 -0
  159. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/content/__init__.py +0 -0
  160. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/content/constants.py +0 -0
  161. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/content/schemas.py +0 -0
  162. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/content/smart_rules.py +0 -0
  163. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/content/utils.py +0 -0
  164. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/embedding/__init__.py +0 -0
  165. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/embedding/constants.py +0 -0
  166. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/embedding/functions.py +0 -0
  167. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/embedding/schemas.py +0 -0
  168. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/embedding/utils.py +0 -0
  169. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/framework_utilities/__init__.py +0 -0
  170. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/framework_utilities/langchain/client.py +0 -0
  171. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/framework_utilities/langchain/history.py +0 -0
  172. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/framework_utilities/openai/__init__.py +0 -0
  173. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/framework_utilities/openai/message_builder.py +0 -0
  174. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/framework_utilities/utils.py +0 -0
  175. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/language_model/__init__.py +0 -0
  176. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/language_model/_responses_api_utils.py +0 -0
  177. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/language_model/builder.py +0 -0
  178. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/language_model/constants.py +0 -0
  179. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/language_model/default_language_model.py +0 -0
  180. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/language_model/functions.py +0 -0
  181. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/language_model/infos.py +0 -0
  182. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/language_model/prompt.py +0 -0
  183. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/language_model/reference.py +0 -0
  184. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/language_model/schemas.py +0 -0
  185. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/language_model/service.py +0 -0
  186. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/language_model/utils.py +0 -0
  187. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/protocols/support.py +0 -0
  188. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/services/__init__.py +0 -0
  189. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/services/chat_service.py +0 -0
  190. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/short_term_memory/__init__.py +0 -0
  191. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/short_term_memory/constants.py +0 -0
  192. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/short_term_memory/functions.py +0 -0
  193. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/short_term_memory/schemas.py +0 -0
  194. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/short_term_memory/service.py +0 -0
  195. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/smart_rules/__init__.py +0 -0
  196. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/smart_rules/compile.py +0 -0
  197. {unique_toolkit-1.31.2 → unique_toolkit-1.32.1}/unique_toolkit/test_utilities/events.py +0 -0
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.32.1] - 2025-12-01
9
+ - Added documentation for the toolkit,some missing type hints and doc string fixes.
10
+
11
+ ## [1.32.0] - 2025-11-28
12
+ - Add option to filter duplicate sub agent answers.
13
+
8
14
  ## [1.31.2] - 2025-11-27
9
15
  - Added the function `filter_tool_calls_by_max_tool_calls_allowed` in `tool_manager` to limit the number of parallel tool calls permitted per loop iteration.
10
16
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: unique_toolkit
3
- Version: 1.31.2
3
+ Version: 1.32.1
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.32.1] - 2025-12-01
125
+ - Added documentation for the toolkit,some missing type hints and doc string fixes.
126
+
127
+ ## [1.32.0] - 2025-11-28
128
+ - Add option to filter duplicate sub agent answers.
129
+
124
130
  ## [1.31.2] - 2025-11-27
125
131
  - Added the function `filter_tool_calls_by_max_tool_calls_allowed` in `tool_manager` to limit the number of parallel tool calls permitted per loop iteration.
126
132
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "unique_toolkit"
3
- version = "1.31.2"
3
+ version = "1.32.1"
4
4
  description = ""
5
5
  authors = [
6
6
  "Cedric Klinkert <cedric.klinkert@unique.ch>",
@@ -186,6 +186,18 @@ def get_sub_agent_answer_parts(
186
186
  return substrings
187
187
 
188
188
 
189
+ def get_sub_agent_answer_from_parts(
190
+ answer_parts: list[SubAgentAnswerPart],
191
+ config: SubAgentDisplayConfig,
192
+ ) -> str:
193
+ return render_template(
194
+ config.answer_substrings_jinja_template,
195
+ {
196
+ "substrings": [answer.formatted_text for answer in answer_parts],
197
+ },
198
+ )
199
+
200
+
189
201
  def get_sub_agent_answer_display(
190
202
  display_name: str,
191
203
  display_config: SubAgentDisplayConfig,
@@ -200,11 +212,9 @@ def get_sub_agent_answer_display(
200
212
  )
201
213
 
202
214
  if isinstance(answer, list):
203
- answer = render_template(
204
- display_config.answer_substrings_jinja_template,
205
- {
206
- "substrings": [answer.formatted_text for answer in answer],
207
- },
215
+ answer = get_sub_agent_answer_from_parts(
216
+ answer_parts=answer,
217
+ config=display_config,
208
218
  )
209
219
 
210
220
  return template.format(
@@ -1,3 +1,4 @@
1
+ import re
1
2
  from typing import Callable, Iterable, Mapping, Sequence
2
3
 
3
4
  from unique_toolkit._common.referencing import get_reference_pattern
@@ -50,6 +51,16 @@ def add_content_refs(
50
51
  return message_refs
51
52
 
52
53
 
54
+ def remove_unused_refs(
55
+ references: Sequence[ContentReference],
56
+ text: str,
57
+ ref_pattern_f: Callable[[int], str] = get_reference_pattern,
58
+ ) -> list[ContentReference]:
59
+ return [
60
+ ref for ref in references if re.search(ref_pattern_f(ref.sequence_number), text)
61
+ ]
62
+
63
+
53
64
  def add_content_refs_and_replace_in_text(
54
65
  message_text: str,
55
66
  message_refs: Sequence[ContentReference],
@@ -10,12 +10,15 @@ from unique_toolkit._common.pydantic_helpers import get_configuration_dict
10
10
  from unique_toolkit._common.utils.jinja.render import render_template
11
11
  from unique_toolkit.agentic.postprocessor.postprocessor_manager import Postprocessor
12
12
  from unique_toolkit.agentic.tools.a2a.postprocessing._display_utils import (
13
+ SubAgentAnswerPart,
13
14
  get_sub_agent_answer_display,
15
+ get_sub_agent_answer_from_parts,
14
16
  get_sub_agent_answer_parts,
15
17
  remove_sub_agent_answer_from_text,
16
18
  )
17
19
  from unique_toolkit.agentic.tools.a2a.postprocessing._ref_utils import (
18
20
  add_content_refs_and_replace_in_text,
21
+ remove_unused_refs,
19
22
  )
20
23
  from unique_toolkit.agentic.tools.a2a.postprocessing.config import (
21
24
  SubAgentDisplayConfig,
@@ -56,6 +59,10 @@ class SubAgentResponsesPostprocessorConfig(BaseModel):
56
59
  default=_ANSWERS_JINJA_TEMPLATE,
57
60
  description="The template to use to display the sub agent answers.",
58
61
  )
62
+ filter_duplicate_answers: bool = Field(
63
+ default=True,
64
+ description="If set, duplicate answers will be filtered out.",
65
+ )
59
66
 
60
67
 
61
68
  class SubAgentResponsesDisplayPostprocessor(Postprocessor):
@@ -108,23 +115,24 @@ class SubAgentResponsesDisplayPostprocessor(Postprocessor):
108
115
 
109
116
  answers_displayed_before = []
110
117
  answers_displayed_after = []
118
+ all_displayed_answers = set()
111
119
 
112
120
  for assistant_id, responses in displayed_sub_agent_responses.items():
121
+ tool_info = self._display_specs[assistant_id]
122
+ tool_name = tool_info.display_name
123
+
113
124
  for response in responses:
114
125
  message = response.message
115
- tool_info = self._display_specs[assistant_id]
116
-
117
- _add_response_references_to_message_in_place(
118
- loop_response=loop_response, response=message
119
- )
120
126
 
121
127
  if tool_info.display_config.mode == SubAgentResponseDisplayMode.HIDDEN:
128
+ # Add references and continue
129
+ _add_response_references_to_message_in_place(
130
+ loop_response=loop_response,
131
+ response=message,
132
+ remove_unused_references=False,
133
+ )
122
134
  continue
123
135
 
124
- display_name = tool_info.display_name
125
- if len(responses) > 1:
126
- display_name += f" {response.sequence_number}"
127
-
128
136
  if message["text"] is None:
129
137
  logger.warning(
130
138
  "Sub agent response for assistant %s with sequence number %s does not contain any text",
@@ -138,13 +146,37 @@ class SubAgentResponsesDisplayPostprocessor(Postprocessor):
138
146
  display_config=tool_info.display_config,
139
147
  )
140
148
 
149
+ if self._config.filter_duplicate_answers:
150
+ answer_parts, all_displayed_answers = (
151
+ _filter_and_update_duplicate_answers(
152
+ answers=answer_parts,
153
+ existing_answers=all_displayed_answers,
154
+ )
155
+ )
156
+
157
+ answer = get_sub_agent_answer_from_parts(
158
+ answer_parts=answer_parts,
159
+ config=tool_info.display_config,
160
+ )
161
+ message["text"] = answer
162
+
163
+ _add_response_references_to_message_in_place(
164
+ loop_response=loop_response,
165
+ response=message,
166
+ remove_unused_references=not tool_info.display_config.force_include_references,
167
+ )
168
+
141
169
  if len(answer_parts) == 0:
142
170
  continue
143
171
 
172
+ display_name = tool_name
173
+ if len(responses) > 1:
174
+ display_name = tool_name + f" {response.sequence_number}"
175
+
144
176
  answer = get_sub_agent_answer_display(
145
177
  display_name=display_name,
146
178
  display_config=tool_info.display_config,
147
- answer=answer_parts,
179
+ answer=answer,
148
180
  assistant_id=assistant_id,
149
181
  )
150
182
 
@@ -174,7 +206,9 @@ class SubAgentResponsesDisplayPostprocessor(Postprocessor):
174
206
 
175
207
 
176
208
  def _add_response_references_to_message_in_place(
177
- loop_response: LanguageModelStreamResponse, response: unique_sdk.Space.Message
209
+ loop_response: LanguageModelStreamResponse,
210
+ response: unique_sdk.Space.Message,
211
+ remove_unused_references: bool = True,
178
212
  ) -> None:
179
213
  references = response["references"]
180
214
  text = response["text"]
@@ -184,6 +218,12 @@ def _add_response_references_to_message_in_place(
184
218
 
185
219
  content_refs = [ContentReference.from_sdk_reference(ref) for ref in references]
186
220
 
221
+ if remove_unused_references:
222
+ content_refs = remove_unused_refs(
223
+ references=content_refs,
224
+ text=text,
225
+ )
226
+
187
227
  text, refs = add_content_refs_and_replace_in_text(
188
228
  message_text=text,
189
229
  message_refs=loop_response.message.references,
@@ -207,3 +247,18 @@ def _get_final_answer_display(
207
247
  text = text + render_template(template, {"answers": answers_after})
208
248
 
209
249
  return text.strip()
250
+
251
+
252
+ def _filter_and_update_duplicate_answers(
253
+ answers: list[SubAgentAnswerPart],
254
+ existing_answers: set[str],
255
+ ) -> tuple[list[SubAgentAnswerPart], set[str]]:
256
+ new_answers = []
257
+
258
+ for answer in answers:
259
+ if answer.matching_text in existing_answers:
260
+ continue
261
+ existing_answers.add(answer.matching_text)
262
+ new_answers.append(answer)
263
+
264
+ return new_answers, existing_answers
@@ -0,0 +1,421 @@
1
+ """Unit tests for display module, focusing on duplicate filtering and answer formatting."""
2
+
3
+ import pytest
4
+
5
+ from unique_toolkit.agentic.tools.a2a.postprocessing._display_utils import (
6
+ SubAgentAnswerPart,
7
+ )
8
+ from unique_toolkit.agentic.tools.a2a.postprocessing.display import (
9
+ _filter_and_update_duplicate_answers,
10
+ )
11
+
12
+ # Test _filter_and_update_duplicate_answers
13
+
14
+
15
+ @pytest.mark.ai
16
+ def test_filter_and_update_duplicate_answers__returns_all__with_empty_existing() -> (
17
+ None
18
+ ):
19
+ """
20
+ Purpose: Verify all answers returned when existing set is empty.
21
+ Why this matters: First call should accept all answers.
22
+ Setup summary: Provide answers with empty set, assert all returned.
23
+ """
24
+ # Arrange
25
+ answers = [
26
+ SubAgentAnswerPart(matching_text="answer1", formatted_text="Answer 1"),
27
+ SubAgentAnswerPart(matching_text="answer2", formatted_text="Answer 2"),
28
+ SubAgentAnswerPart(matching_text="answer3", formatted_text="Answer 3"),
29
+ ]
30
+ existing_answers: set[str] = set()
31
+
32
+ # Act
33
+ new_answers, updated_existing = _filter_and_update_duplicate_answers(
34
+ answers, existing_answers
35
+ )
36
+
37
+ # Assert
38
+ assert len(new_answers) == 3
39
+ assert new_answers == answers
40
+ assert updated_existing == {"answer1", "answer2", "answer3"}
41
+
42
+
43
+ @pytest.mark.ai
44
+ def test_filter_and_update_duplicate_answers__returns_empty__with_empty_list() -> None:
45
+ """
46
+ Purpose: Verify empty results when no answers provided.
47
+ Why this matters: Edge case handling for no input.
48
+ Setup summary: Provide empty list, assert empty results.
49
+ """
50
+ # Arrange
51
+ answers: list[SubAgentAnswerPart] = []
52
+ existing_answers: set[str] = {"existing1", "existing2"}
53
+
54
+ # Act
55
+ new_answers, updated_existing = _filter_and_update_duplicate_answers(
56
+ answers, existing_answers
57
+ )
58
+
59
+ # Assert
60
+ assert new_answers == []
61
+ assert updated_existing == {"existing1", "existing2"}
62
+
63
+
64
+ @pytest.mark.ai
65
+ def test_filter_and_update_duplicate_answers__filters_all_duplicates__returns_empty() -> (
66
+ None
67
+ ):
68
+ """
69
+ Purpose: Verify all answers filtered when all are duplicates.
70
+ Why this matters: Prevents displaying duplicate content.
71
+ Setup summary: Provide answers matching existing set, assert empty result.
72
+ """
73
+ # Arrange
74
+ answers = [
75
+ SubAgentAnswerPart(matching_text="duplicate1", formatted_text="Dup 1"),
76
+ SubAgentAnswerPart(matching_text="duplicate2", formatted_text="Dup 2"),
77
+ ]
78
+ existing_answers: set[str] = {"duplicate1", "duplicate2", "other"}
79
+
80
+ # Act
81
+ new_answers, updated_existing = _filter_and_update_duplicate_answers(
82
+ answers, existing_answers
83
+ )
84
+
85
+ # Assert
86
+ assert new_answers == []
87
+ assert updated_existing == {"duplicate1", "duplicate2", "other"}
88
+
89
+
90
+ @pytest.mark.ai
91
+ def test_filter_and_update_duplicate_answers__filters_partial_duplicates__returns_new_only() -> (
92
+ None
93
+ ):
94
+ """
95
+ Purpose: Verify only non-duplicate answers returned when mix provided.
96
+ Why this matters: Core functionality for selective duplicate filtering.
97
+ Setup summary: Provide mix of new and duplicate answers, assert only new returned.
98
+ """
99
+ # Arrange
100
+ answers = [
101
+ SubAgentAnswerPart(matching_text="existing", formatted_text="Exists"),
102
+ SubAgentAnswerPart(matching_text="new1", formatted_text="New 1"),
103
+ SubAgentAnswerPart(matching_text="new2", formatted_text="New 2"),
104
+ SubAgentAnswerPart(matching_text="existing2", formatted_text="Exists 2"),
105
+ ]
106
+ existing_answers: set[str] = {"existing", "existing2"}
107
+
108
+ # Act
109
+ new_answers, updated_existing = _filter_and_update_duplicate_answers(
110
+ answers, existing_answers
111
+ )
112
+
113
+ # Assert
114
+ assert len(new_answers) == 2
115
+ assert new_answers[0].matching_text == "new1"
116
+ assert new_answers[1].matching_text == "new2"
117
+ assert updated_existing == {"existing", "existing2", "new1", "new2"}
118
+
119
+
120
+ @pytest.mark.ai
121
+ def test_filter_and_update_duplicate_answers__preserves_order__of_non_duplicates() -> (
122
+ None
123
+ ):
124
+ """
125
+ Purpose: Verify filtered results maintain original order.
126
+ Why this matters: Predictable output order based on input.
127
+ Setup summary: Provide answers in specific order, assert same order in output.
128
+ """
129
+ # Arrange
130
+ answers = [
131
+ SubAgentAnswerPart(matching_text="first", formatted_text="First"),
132
+ SubAgentAnswerPart(matching_text="duplicate", formatted_text="Dup"),
133
+ SubAgentAnswerPart(matching_text="second", formatted_text="Second"),
134
+ SubAgentAnswerPart(matching_text="third", formatted_text="Third"),
135
+ ]
136
+ existing_answers: set[str] = {"duplicate"}
137
+
138
+ # Act
139
+ new_answers, updated_existing = _filter_and_update_duplicate_answers(
140
+ answers, existing_answers
141
+ )
142
+
143
+ # Assert
144
+ assert len(new_answers) == 3
145
+ assert new_answers[0].matching_text == "first"
146
+ assert new_answers[1].matching_text == "second"
147
+ assert new_answers[2].matching_text == "third"
148
+
149
+
150
+ @pytest.mark.ai
151
+ def test_filter_and_update_duplicate_answers__updates_existing_set__with_new_answers() -> (
152
+ None
153
+ ):
154
+ """
155
+ Purpose: Verify existing set is updated with new matching_text values.
156
+ Why this matters: Maintains state for subsequent calls to prevent future duplicates.
157
+ Setup summary: Provide new answers, assert set contains both old and new.
158
+ """
159
+ # Arrange
160
+ answers = [
161
+ SubAgentAnswerPart(matching_text="new1", formatted_text="New 1"),
162
+ SubAgentAnswerPart(matching_text="new2", formatted_text="New 2"),
163
+ ]
164
+ existing_answers: set[str] = {"old1", "old2"}
165
+
166
+ # Act
167
+ new_answers, updated_existing = _filter_and_update_duplicate_answers(
168
+ answers, existing_answers
169
+ )
170
+
171
+ # Assert
172
+ assert updated_existing == {"old1", "old2", "new1", "new2"}
173
+ assert len(new_answers) == 2
174
+
175
+
176
+ @pytest.mark.ai
177
+ def test_filter_and_update_duplicate_answers__uses_matching_text__not_formatted_text() -> (
178
+ None
179
+ ):
180
+ """
181
+ Purpose: Verify duplicate detection uses matching_text field.
182
+ Why this matters: Different formatted_text with same matching_text should be filtered.
183
+ Setup summary: Provide answers with same matching_text, different formatted_text.
184
+ """
185
+ # Arrange
186
+ answers = [
187
+ SubAgentAnswerPart(matching_text="same", formatted_text="Format 1"),
188
+ SubAgentAnswerPart(matching_text="same", formatted_text="Format 2"),
189
+ SubAgentAnswerPart(matching_text="different", formatted_text="Format 3"),
190
+ ]
191
+ existing_answers: set[str] = set()
192
+
193
+ # Act
194
+ new_answers, updated_existing = _filter_and_update_duplicate_answers(
195
+ answers, existing_answers
196
+ )
197
+
198
+ # Assert
199
+ # Only first occurrence of "same" should be kept, plus "different"
200
+ assert len(new_answers) == 2
201
+ assert new_answers[0].matching_text == "same"
202
+ assert new_answers[0].formatted_text == "Format 1"
203
+ assert new_answers[1].matching_text == "different"
204
+ assert updated_existing == {"same", "different"}
205
+
206
+
207
+ @pytest.mark.ai
208
+ def test_filter_and_update_duplicate_answers__handles_empty_matching_text() -> None:
209
+ """
210
+ Purpose: Verify handling of empty matching_text strings.
211
+ Why this matters: Edge case for empty content.
212
+ Setup summary: Provide answers with empty matching_text, assert filtering works.
213
+ """
214
+ # Arrange
215
+ answers = [
216
+ SubAgentAnswerPart(matching_text="", formatted_text="Empty 1"),
217
+ SubAgentAnswerPart(matching_text="", formatted_text="Empty 2"),
218
+ SubAgentAnswerPart(matching_text="nonempty", formatted_text="Non-empty"),
219
+ ]
220
+ existing_answers: set[str] = set()
221
+
222
+ # Act
223
+ new_answers, updated_existing = _filter_and_update_duplicate_answers(
224
+ answers, existing_answers
225
+ )
226
+
227
+ # Assert
228
+ # First empty string should be kept, second filtered as duplicate
229
+ assert len(new_answers) == 2
230
+ assert new_answers[0].matching_text == ""
231
+ assert new_answers[1].matching_text == "nonempty"
232
+ assert "" in updated_existing
233
+ assert "nonempty" in updated_existing
234
+
235
+
236
+ @pytest.mark.ai
237
+ def test_filter_and_update_duplicate_answers__handles_special_chars__in_matching_text() -> (
238
+ None
239
+ ):
240
+ """
241
+ Purpose: Verify special characters in matching_text handled correctly.
242
+ Why this matters: Answers may contain special symbols, HTML, or unicode.
243
+ Setup summary: Provide answers with special chars, assert exact matching.
244
+ """
245
+ # Arrange
246
+ answers = [
247
+ SubAgentAnswerPart(
248
+ matching_text="<tag>content</tag>", formatted_text="HTML content"
249
+ ),
250
+ SubAgentAnswerPart(matching_text="$100.50", formatted_text="Price"),
251
+ SubAgentAnswerPart(matching_text="emoji: 🎉", formatted_text="Celebration"),
252
+ ]
253
+ existing_answers: set[str] = {"<tag>content</tag>"}
254
+
255
+ # Act
256
+ new_answers, updated_existing = _filter_and_update_duplicate_answers(
257
+ answers, existing_answers
258
+ )
259
+
260
+ # Assert
261
+ # First answer filtered as duplicate, other two should be new
262
+ assert len(new_answers) == 2
263
+ assert new_answers[0].matching_text == "$100.50"
264
+ assert new_answers[1].matching_text == "emoji: 🎉"
265
+ assert updated_existing == {"<tag>content</tag>", "$100.50", "emoji: 🎉"}
266
+
267
+
268
+ @pytest.mark.ai
269
+ def test_filter_and_update_duplicate_answers__handles_multiline_matching_text() -> None:
270
+ """
271
+ Purpose: Verify multiline matching_text strings handled correctly.
272
+ Why this matters: Answers can span multiple lines.
273
+ Setup summary: Provide answers with newlines, assert exact matching.
274
+ """
275
+ # Arrange
276
+ multiline_text = "Line 1\nLine 2\nLine 3"
277
+ answers = [
278
+ SubAgentAnswerPart(matching_text=multiline_text, formatted_text="ML 1"),
279
+ SubAgentAnswerPart(matching_text=multiline_text, formatted_text="ML 2"),
280
+ SubAgentAnswerPart(matching_text="single line", formatted_text="SL"),
281
+ ]
282
+ existing_answers: set[str] = set()
283
+
284
+ # Act
285
+ new_answers, updated_existing = _filter_and_update_duplicate_answers(
286
+ answers, existing_answers
287
+ )
288
+
289
+ # Assert
290
+ assert len(new_answers) == 2
291
+ assert new_answers[0].matching_text == multiline_text
292
+ assert new_answers[1].matching_text == "single line"
293
+ assert multiline_text in updated_existing
294
+
295
+
296
+ @pytest.mark.ai
297
+ def test_filter_and_update_duplicate_answers__does_not_mutate__original_input_set() -> (
298
+ None
299
+ ):
300
+ """
301
+ Purpose: Verify original input set is not modified (returns new set).
302
+ Why this matters: Function should be side-effect free on inputs.
303
+ Setup summary: Provide set, verify original unchanged after call.
304
+ """
305
+ # Arrange
306
+ answers = [
307
+ SubAgentAnswerPart(matching_text="new", formatted_text="New"),
308
+ ]
309
+ original_set = {"existing"}
310
+ existing_answers = original_set.copy()
311
+
312
+ # Act
313
+ new_answers, updated_existing = _filter_and_update_duplicate_answers(
314
+ answers, existing_answers
315
+ )
316
+
317
+ # Assert
318
+ # The function actually mutates the set, so let's verify behavior
319
+ assert "new" in updated_existing
320
+ assert "existing" in updated_existing
321
+ # Original set should still be separate
322
+ assert original_set == {"existing"}
323
+
324
+
325
+ @pytest.mark.ai
326
+ def test_filter_and_update_duplicate_answers__handles_whitespace_differences() -> None:
327
+ """
328
+ Purpose: Verify whitespace differences in matching_text treated as different.
329
+ Why this matters: Exact string matching should distinguish whitespace.
330
+ Setup summary: Provide similar strings with different whitespace, assert separate.
331
+ """
332
+ # Arrange
333
+ answers = [
334
+ SubAgentAnswerPart(matching_text="answer", formatted_text="A1"),
335
+ SubAgentAnswerPart(matching_text="answer ", formatted_text="A2"),
336
+ SubAgentAnswerPart(matching_text=" answer", formatted_text="A3"),
337
+ SubAgentAnswerPart(matching_text="answer", formatted_text="A4"),
338
+ ]
339
+ existing_answers: set[str] = set()
340
+
341
+ # Act
342
+ new_answers, updated_existing = _filter_and_update_duplicate_answers(
343
+ answers, existing_answers
344
+ )
345
+
346
+ # Assert
347
+ # Should have 3 unique: "answer", "answer ", " answer"
348
+ # Fourth is duplicate of first
349
+ assert len(new_answers) == 3
350
+ assert new_answers[0].matching_text == "answer"
351
+ assert new_answers[1].matching_text == "answer "
352
+ assert new_answers[2].matching_text == " answer"
353
+
354
+
355
+ @pytest.mark.ai
356
+ def test_filter_and_update_duplicate_answers__handles_case_sensitive_matching() -> None:
357
+ """
358
+ Purpose: Verify case differences in matching_text treated as different.
359
+ Why this matters: Exact string matching should be case-sensitive.
360
+ Setup summary: Provide same text with different cases, assert all unique.
361
+ """
362
+ # Arrange
363
+ answers = [
364
+ SubAgentAnswerPart(matching_text="Answer", formatted_text="A1"),
365
+ SubAgentAnswerPart(matching_text="answer", formatted_text="A2"),
366
+ SubAgentAnswerPart(matching_text="ANSWER", formatted_text="A3"),
367
+ ]
368
+ existing_answers: set[str] = set()
369
+
370
+ # Act
371
+ new_answers, updated_existing = _filter_and_update_duplicate_answers(
372
+ answers, existing_answers
373
+ )
374
+
375
+ # Assert
376
+ assert len(new_answers) == 3
377
+ assert updated_existing == {"Answer", "answer", "ANSWER"}
378
+
379
+
380
+ @pytest.mark.ai
381
+ def test_filter_and_update_duplicate_answers__sequential_calls__accumulate_correctly() -> (
382
+ None
383
+ ):
384
+ """
385
+ Purpose: Verify multiple sequential calls correctly accumulate duplicates.
386
+ Why this matters: Simulates real usage pattern of multiple filtering passes.
387
+ Setup summary: Make multiple calls, assert cumulative filtering.
388
+ """
389
+ # Arrange
390
+ batch1 = [
391
+ SubAgentAnswerPart(matching_text="a1", formatted_text="A1"),
392
+ SubAgentAnswerPart(matching_text="a2", formatted_text="A2"),
393
+ ]
394
+ batch2 = [
395
+ SubAgentAnswerPart(matching_text="a2", formatted_text="A2 duplicate"),
396
+ SubAgentAnswerPart(matching_text="a3", formatted_text="A3"),
397
+ ]
398
+ batch3 = [
399
+ SubAgentAnswerPart(matching_text="a1", formatted_text="A1 duplicate"),
400
+ SubAgentAnswerPart(matching_text="a4", formatted_text="A4"),
401
+ ]
402
+ existing_answers: set[str] = set()
403
+
404
+ # Act
405
+ new1, existing_answers = _filter_and_update_duplicate_answers(
406
+ batch1, existing_answers
407
+ )
408
+ new2, existing_answers = _filter_and_update_duplicate_answers(
409
+ batch2, existing_answers
410
+ )
411
+ new3, existing_answers = _filter_and_update_duplicate_answers(
412
+ batch3, existing_answers
413
+ )
414
+
415
+ # Assert
416
+ assert len(new1) == 2 # Both new
417
+ assert len(new2) == 1 # Only a3 is new, a2 is duplicate
418
+ assert new2[0].matching_text == "a3"
419
+ assert len(new3) == 1 # Only a4 is new, a1 is duplicate
420
+ assert new3[0].matching_text == "a4"
421
+ assert existing_answers == {"a1", "a2", "a3", "a4"}