unique_toolkit 0.7.9__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 (190) hide show
  1. unique_toolkit/__init__.py +36 -3
  2. unique_toolkit/_common/api_calling/human_verification_manager.py +357 -0
  3. unique_toolkit/_common/base_model_type_attribute.py +303 -0
  4. unique_toolkit/_common/chunk_relevancy_sorter/config.py +49 -0
  5. unique_toolkit/_common/chunk_relevancy_sorter/exception.py +5 -0
  6. unique_toolkit/_common/chunk_relevancy_sorter/schemas.py +46 -0
  7. unique_toolkit/_common/chunk_relevancy_sorter/service.py +374 -0
  8. unique_toolkit/_common/chunk_relevancy_sorter/tests/test_service.py +275 -0
  9. unique_toolkit/_common/default_language_model.py +12 -0
  10. unique_toolkit/_common/docx_generator/__init__.py +7 -0
  11. unique_toolkit/_common/docx_generator/config.py +12 -0
  12. unique_toolkit/_common/docx_generator/schemas.py +80 -0
  13. unique_toolkit/_common/docx_generator/service.py +225 -0
  14. unique_toolkit/_common/docx_generator/template/Doc Template.docx +0 -0
  15. unique_toolkit/_common/endpoint_builder.py +368 -0
  16. unique_toolkit/_common/endpoint_requestor.py +480 -0
  17. unique_toolkit/_common/exception.py +24 -0
  18. unique_toolkit/_common/experimental/endpoint_builder.py +368 -0
  19. unique_toolkit/_common/experimental/endpoint_requestor.py +488 -0
  20. unique_toolkit/_common/feature_flags/schema.py +9 -0
  21. unique_toolkit/_common/pydantic/rjsf_tags.py +936 -0
  22. unique_toolkit/_common/pydantic_helpers.py +174 -0
  23. unique_toolkit/_common/referencing.py +53 -0
  24. unique_toolkit/_common/string_utilities.py +140 -0
  25. unique_toolkit/_common/tests/test_referencing.py +521 -0
  26. unique_toolkit/_common/tests/test_string_utilities.py +506 -0
  27. unique_toolkit/_common/token/image_token_counting.py +67 -0
  28. unique_toolkit/_common/token/token_counting.py +204 -0
  29. unique_toolkit/_common/utils/__init__.py +1 -0
  30. unique_toolkit/_common/utils/files.py +43 -0
  31. unique_toolkit/_common/utils/image/encode.py +25 -0
  32. unique_toolkit/_common/utils/jinja/helpers.py +10 -0
  33. unique_toolkit/_common/utils/jinja/render.py +18 -0
  34. unique_toolkit/_common/utils/jinja/schema.py +65 -0
  35. unique_toolkit/_common/utils/jinja/utils.py +80 -0
  36. unique_toolkit/_common/utils/structured_output/__init__.py +1 -0
  37. unique_toolkit/_common/utils/structured_output/schema.py +5 -0
  38. unique_toolkit/_common/utils/write_configuration.py +51 -0
  39. unique_toolkit/_common/validators.py +101 -4
  40. unique_toolkit/agentic/__init__.py +1 -0
  41. unique_toolkit/agentic/debug_info_manager/debug_info_manager.py +28 -0
  42. unique_toolkit/agentic/debug_info_manager/test/test_debug_info_manager.py +278 -0
  43. unique_toolkit/agentic/evaluation/config.py +36 -0
  44. unique_toolkit/{evaluators → agentic/evaluation}/context_relevancy/prompts.py +25 -0
  45. unique_toolkit/agentic/evaluation/context_relevancy/schema.py +80 -0
  46. unique_toolkit/agentic/evaluation/context_relevancy/service.py +273 -0
  47. unique_toolkit/agentic/evaluation/evaluation_manager.py +218 -0
  48. unique_toolkit/agentic/evaluation/hallucination/constants.py +61 -0
  49. unique_toolkit/agentic/evaluation/hallucination/hallucination_evaluation.py +112 -0
  50. unique_toolkit/{evaluators → agentic/evaluation}/hallucination/prompts.py +1 -1
  51. unique_toolkit/{evaluators → agentic/evaluation}/hallucination/service.py +20 -16
  52. unique_toolkit/{evaluators → agentic/evaluation}/hallucination/utils.py +32 -21
  53. unique_toolkit/{evaluators → agentic/evaluation}/output_parser.py +20 -2
  54. unique_toolkit/{evaluators → agentic/evaluation}/schemas.py +27 -7
  55. unique_toolkit/agentic/evaluation/tests/test_context_relevancy_service.py +253 -0
  56. unique_toolkit/agentic/evaluation/tests/test_output_parser.py +87 -0
  57. unique_toolkit/agentic/history_manager/history_construction_with_contents.py +298 -0
  58. unique_toolkit/agentic/history_manager/history_manager.py +241 -0
  59. unique_toolkit/agentic/history_manager/loop_token_reducer.py +484 -0
  60. unique_toolkit/agentic/history_manager/utils.py +96 -0
  61. unique_toolkit/agentic/message_log_manager/__init__.py +5 -0
  62. unique_toolkit/agentic/message_log_manager/service.py +93 -0
  63. unique_toolkit/agentic/postprocessor/postprocessor_manager.py +212 -0
  64. unique_toolkit/agentic/reference_manager/reference_manager.py +103 -0
  65. unique_toolkit/agentic/responses_api/__init__.py +19 -0
  66. unique_toolkit/agentic/responses_api/postprocessors/code_display.py +71 -0
  67. unique_toolkit/agentic/responses_api/postprocessors/generated_files.py +297 -0
  68. unique_toolkit/agentic/responses_api/stream_handler.py +15 -0
  69. unique_toolkit/agentic/short_term_memory_manager/persistent_short_term_memory_manager.py +141 -0
  70. unique_toolkit/agentic/thinking_manager/thinking_manager.py +103 -0
  71. unique_toolkit/agentic/tools/__init__.py +1 -0
  72. unique_toolkit/agentic/tools/a2a/__init__.py +36 -0
  73. unique_toolkit/agentic/tools/a2a/config.py +17 -0
  74. unique_toolkit/agentic/tools/a2a/evaluation/__init__.py +15 -0
  75. unique_toolkit/agentic/tools/a2a/evaluation/_utils.py +66 -0
  76. unique_toolkit/agentic/tools/a2a/evaluation/config.py +55 -0
  77. unique_toolkit/agentic/tools/a2a/evaluation/evaluator.py +260 -0
  78. unique_toolkit/agentic/tools/a2a/evaluation/summarization_user_message.j2 +9 -0
  79. unique_toolkit/agentic/tools/a2a/manager.py +55 -0
  80. unique_toolkit/agentic/tools/a2a/postprocessing/__init__.py +21 -0
  81. unique_toolkit/agentic/tools/a2a/postprocessing/_display_utils.py +240 -0
  82. unique_toolkit/agentic/tools/a2a/postprocessing/_ref_utils.py +84 -0
  83. unique_toolkit/agentic/tools/a2a/postprocessing/config.py +78 -0
  84. unique_toolkit/agentic/tools/a2a/postprocessing/display.py +264 -0
  85. unique_toolkit/agentic/tools/a2a/postprocessing/references.py +101 -0
  86. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display.py +421 -0
  87. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display_utils.py +2103 -0
  88. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_ref_utils.py +603 -0
  89. unique_toolkit/agentic/tools/a2a/prompts.py +46 -0
  90. unique_toolkit/agentic/tools/a2a/response_watcher/__init__.py +6 -0
  91. unique_toolkit/agentic/tools/a2a/response_watcher/service.py +91 -0
  92. unique_toolkit/agentic/tools/a2a/tool/__init__.py +4 -0
  93. unique_toolkit/agentic/tools/a2a/tool/_memory.py +26 -0
  94. unique_toolkit/agentic/tools/a2a/tool/_schema.py +9 -0
  95. unique_toolkit/agentic/tools/a2a/tool/config.py +158 -0
  96. unique_toolkit/agentic/tools/a2a/tool/service.py +393 -0
  97. unique_toolkit/agentic/tools/agent_chunks_hanlder.py +65 -0
  98. unique_toolkit/agentic/tools/config.py +128 -0
  99. unique_toolkit/agentic/tools/factory.py +44 -0
  100. unique_toolkit/agentic/tools/mcp/__init__.py +4 -0
  101. unique_toolkit/agentic/tools/mcp/manager.py +71 -0
  102. unique_toolkit/agentic/tools/mcp/models.py +28 -0
  103. unique_toolkit/agentic/tools/mcp/tool_wrapper.py +234 -0
  104. unique_toolkit/agentic/tools/openai_builtin/__init__.py +11 -0
  105. unique_toolkit/agentic/tools/openai_builtin/base.py +46 -0
  106. unique_toolkit/agentic/tools/openai_builtin/code_interpreter/__init__.py +8 -0
  107. unique_toolkit/agentic/tools/openai_builtin/code_interpreter/config.py +88 -0
  108. unique_toolkit/agentic/tools/openai_builtin/code_interpreter/service.py +250 -0
  109. unique_toolkit/agentic/tools/openai_builtin/manager.py +79 -0
  110. unique_toolkit/agentic/tools/schemas.py +145 -0
  111. unique_toolkit/agentic/tools/test/test_mcp_manager.py +536 -0
  112. unique_toolkit/agentic/tools/test/test_tool_progress_reporter.py +445 -0
  113. unique_toolkit/agentic/tools/tool.py +187 -0
  114. unique_toolkit/agentic/tools/tool_manager.py +492 -0
  115. unique_toolkit/agentic/tools/tool_progress_reporter.py +285 -0
  116. unique_toolkit/agentic/tools/utils/__init__.py +19 -0
  117. unique_toolkit/agentic/tools/utils/execution/__init__.py +1 -0
  118. unique_toolkit/agentic/tools/utils/execution/execution.py +286 -0
  119. unique_toolkit/agentic/tools/utils/source_handling/__init__.py +0 -0
  120. unique_toolkit/agentic/tools/utils/source_handling/schema.py +21 -0
  121. unique_toolkit/agentic/tools/utils/source_handling/source_formatting.py +207 -0
  122. unique_toolkit/agentic/tools/utils/source_handling/tests/test_source_formatting.py +216 -0
  123. unique_toolkit/app/__init__.py +9 -0
  124. unique_toolkit/app/dev_util.py +180 -0
  125. unique_toolkit/app/fast_api_factory.py +131 -0
  126. unique_toolkit/app/init_sdk.py +32 -1
  127. unique_toolkit/app/schemas.py +206 -31
  128. unique_toolkit/app/unique_settings.py +367 -0
  129. unique_toolkit/app/webhook.py +77 -0
  130. unique_toolkit/chat/__init__.py +8 -1
  131. unique_toolkit/chat/deprecated/service.py +232 -0
  132. unique_toolkit/chat/functions.py +648 -78
  133. unique_toolkit/chat/rendering.py +34 -0
  134. unique_toolkit/chat/responses_api.py +461 -0
  135. unique_toolkit/chat/schemas.py +134 -2
  136. unique_toolkit/chat/service.py +115 -767
  137. unique_toolkit/content/functions.py +353 -8
  138. unique_toolkit/content/schemas.py +128 -15
  139. unique_toolkit/content/service.py +321 -45
  140. unique_toolkit/content/smart_rules.py +301 -0
  141. unique_toolkit/content/utils.py +10 -3
  142. unique_toolkit/data_extraction/README.md +96 -0
  143. unique_toolkit/data_extraction/__init__.py +11 -0
  144. unique_toolkit/data_extraction/augmented/__init__.py +5 -0
  145. unique_toolkit/data_extraction/augmented/service.py +93 -0
  146. unique_toolkit/data_extraction/base.py +25 -0
  147. unique_toolkit/data_extraction/basic/__init__.py +11 -0
  148. unique_toolkit/data_extraction/basic/config.py +18 -0
  149. unique_toolkit/data_extraction/basic/prompt.py +13 -0
  150. unique_toolkit/data_extraction/basic/service.py +55 -0
  151. unique_toolkit/embedding/service.py +103 -12
  152. unique_toolkit/framework_utilities/__init__.py +1 -0
  153. unique_toolkit/framework_utilities/langchain/__init__.py +10 -0
  154. unique_toolkit/framework_utilities/langchain/client.py +71 -0
  155. unique_toolkit/framework_utilities/langchain/history.py +19 -0
  156. unique_toolkit/framework_utilities/openai/__init__.py +6 -0
  157. unique_toolkit/framework_utilities/openai/client.py +84 -0
  158. unique_toolkit/framework_utilities/openai/message_builder.py +229 -0
  159. unique_toolkit/framework_utilities/utils.py +23 -0
  160. unique_toolkit/language_model/__init__.py +3 -0
  161. unique_toolkit/language_model/_responses_api_utils.py +93 -0
  162. unique_toolkit/language_model/builder.py +27 -11
  163. unique_toolkit/language_model/default_language_model.py +3 -0
  164. unique_toolkit/language_model/functions.py +345 -43
  165. unique_toolkit/language_model/infos.py +1288 -46
  166. unique_toolkit/language_model/reference.py +242 -0
  167. unique_toolkit/language_model/schemas.py +481 -49
  168. unique_toolkit/language_model/service.py +229 -28
  169. unique_toolkit/protocols/support.py +145 -0
  170. unique_toolkit/services/__init__.py +7 -0
  171. unique_toolkit/services/chat_service.py +1631 -0
  172. unique_toolkit/services/knowledge_base.py +1094 -0
  173. unique_toolkit/short_term_memory/service.py +178 -41
  174. unique_toolkit/smart_rules/__init__.py +0 -0
  175. unique_toolkit/smart_rules/compile.py +56 -0
  176. unique_toolkit/test_utilities/events.py +197 -0
  177. unique_toolkit-1.33.3.dist-info/METADATA +1145 -0
  178. unique_toolkit-1.33.3.dist-info/RECORD +205 -0
  179. unique_toolkit/evaluators/__init__.py +0 -1
  180. unique_toolkit/evaluators/config.py +0 -35
  181. unique_toolkit/evaluators/constants.py +0 -1
  182. unique_toolkit/evaluators/context_relevancy/constants.py +0 -32
  183. unique_toolkit/evaluators/context_relevancy/service.py +0 -53
  184. unique_toolkit/evaluators/context_relevancy/utils.py +0 -142
  185. unique_toolkit/evaluators/hallucination/constants.py +0 -41
  186. unique_toolkit-0.7.9.dist-info/METADATA +0 -413
  187. unique_toolkit-0.7.9.dist-info/RECORD +0 -64
  188. /unique_toolkit/{evaluators → agentic/evaluation}/exception.py +0 -0
  189. {unique_toolkit-0.7.9.dist-info → unique_toolkit-1.33.3.dist-info}/LICENSE +0 -0
  190. {unique_toolkit-0.7.9.dist-info → unique_toolkit-1.33.3.dist-info}/WHEEL +0 -0
@@ -0,0 +1,78 @@
1
+ import re
2
+ from enum import StrEnum
3
+ from typing import Literal
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+ from unique_toolkit._common.pydantic_helpers import get_configuration_dict
8
+
9
+
10
+ class SubAgentResponseDisplayMode(StrEnum):
11
+ HIDDEN = "hidden"
12
+ DETAILS_OPEN = "details_open"
13
+ DETAILS_CLOSED = "details_closed"
14
+ PLAIN = "plain"
15
+
16
+
17
+ class SubAgentAnswerSubstringConfig(BaseModel):
18
+ model_config = get_configuration_dict()
19
+
20
+ regexp: re.Pattern[str] = Field(
21
+ description="The regular expression to use to extract the substring. The first capture group will always be used.",
22
+ )
23
+ display_template: str = Field(
24
+ default="{}",
25
+ description="The template to use to display the substring. It should contain exactly one empty placeholder '{}' for the substring.",
26
+ )
27
+
28
+
29
+ _ANSWER_SUBSTRINGS_JINJA_TEMPLATE = """
30
+ {% for substring in substrings %}
31
+ {{ substring }}
32
+ {% endfor %}
33
+ """.strip()
34
+
35
+
36
+ class SubAgentDisplayConfig(BaseModel):
37
+ model_config = get_configuration_dict()
38
+
39
+ mode: SubAgentResponseDisplayMode = Field(
40
+ default=SubAgentResponseDisplayMode.HIDDEN,
41
+ description="Controls how to display the sub agent response.",
42
+ )
43
+ remove_from_history: bool = Field(
44
+ default=True,
45
+ description="If set, sub agent responses will be removed from the history on subsequent calls to the assistant.",
46
+ )
47
+ add_quote_border: bool = Field(
48
+ default=True,
49
+ description="If set, a quote border is added to the left of the sub agent response.",
50
+ )
51
+ add_block_border: bool = Field(
52
+ default=False,
53
+ description="If set, a block border is added around the sub agent response.",
54
+ )
55
+ display_title_template: str = Field(
56
+ default="Answer from <strong>{}</strong>",
57
+ description=(
58
+ "The template to use for the display title of the sub agent response."
59
+ "If a placeholder '{}' is present, it will be replaced with the display name of the sub agent."
60
+ ),
61
+ )
62
+ position: Literal["before", "after"] = Field(
63
+ default="before",
64
+ description="The position of the sub agent response in the main agent response.",
65
+ )
66
+ force_include_references: bool = Field(
67
+ default=False,
68
+ description="If set, the sub agent references will be added to the main agent response references even in not mentioned in the main agent response text.",
69
+ )
70
+
71
+ answer_substrings_config: list[SubAgentAnswerSubstringConfig] = Field(
72
+ default=[],
73
+ description="If set, only parts of the answer matching the provided regular expressions will be displayed.",
74
+ )
75
+ answer_substrings_jinja_template: str = Field(
76
+ default=_ANSWER_SUBSTRINGS_JINJA_TEMPLATE,
77
+ description="The template to use in order to format the different answer substrings, if any.",
78
+ )
@@ -0,0 +1,264 @@
1
+ import asyncio
2
+ import logging
3
+ from collections import defaultdict
4
+ from typing import NamedTuple, override
5
+
6
+ import unique_sdk
7
+ from pydantic import BaseModel, Field
8
+
9
+ from unique_toolkit._common.pydantic_helpers import get_configuration_dict
10
+ from unique_toolkit._common.utils.jinja.render import render_template
11
+ from unique_toolkit.agentic.postprocessor.postprocessor_manager import Postprocessor
12
+ from unique_toolkit.agentic.tools.a2a.postprocessing._display_utils import (
13
+ SubAgentAnswerPart,
14
+ get_sub_agent_answer_display,
15
+ get_sub_agent_answer_from_parts,
16
+ get_sub_agent_answer_parts,
17
+ remove_sub_agent_answer_from_text,
18
+ )
19
+ from unique_toolkit.agentic.tools.a2a.postprocessing._ref_utils import (
20
+ add_content_refs_and_replace_in_text,
21
+ remove_unused_refs,
22
+ )
23
+ from unique_toolkit.agentic.tools.a2a.postprocessing.config import (
24
+ SubAgentDisplayConfig,
25
+ SubAgentResponseDisplayMode,
26
+ )
27
+ from unique_toolkit.agentic.tools.a2a.response_watcher import (
28
+ SubAgentResponse,
29
+ SubAgentResponseWatcher,
30
+ )
31
+ from unique_toolkit.content import ContentReference
32
+ from unique_toolkit.language_model.schemas import LanguageModelStreamResponse
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+ SpaceMessage = unique_sdk.Space.Message
37
+
38
+
39
+ class SubAgentDisplaySpec(NamedTuple):
40
+ assistant_id: str
41
+ display_name: str
42
+ display_config: SubAgentDisplayConfig
43
+
44
+
45
+ _ANSWERS_JINJA_TEMPLATE = """
46
+ {% for answer in answers %}
47
+ {{ answer }}
48
+ {% endfor %}
49
+ """.strip()
50
+
51
+
52
+ class SubAgentResponsesPostprocessorConfig(BaseModel):
53
+ model_config = get_configuration_dict()
54
+
55
+ sleep_time_before_update: float = Field(
56
+ default=0, description="Time to sleep before updating the main agent message."
57
+ )
58
+ answers_jinja_template: str = Field(
59
+ default=_ANSWERS_JINJA_TEMPLATE,
60
+ description="The template to use to display the sub agent answers.",
61
+ )
62
+ filter_duplicate_answers: bool = Field(
63
+ default=True,
64
+ description="If set, duplicate answers will be filtered out.",
65
+ )
66
+
67
+
68
+ class SubAgentResponsesDisplayPostprocessor(Postprocessor):
69
+ def __init__(
70
+ self,
71
+ config: SubAgentResponsesPostprocessorConfig,
72
+ response_watcher: SubAgentResponseWatcher,
73
+ display_specs: list[SubAgentDisplaySpec],
74
+ ) -> None:
75
+ super().__init__(name=self.__class__.__name__)
76
+
77
+ self._config = config
78
+ self._response_watcher = response_watcher
79
+ self._display_specs: dict[str, SubAgentDisplaySpec] = {
80
+ display_spec.assistant_id: display_spec
81
+ for display_spec in display_specs
82
+ if display_spec.display_config.mode != SubAgentResponseDisplayMode.HIDDEN
83
+ # We should keep track of these messages even if they are hidden
84
+ or display_spec.display_config.force_include_references
85
+ }
86
+
87
+ @override
88
+ async def run(self, loop_response: LanguageModelStreamResponse) -> None:
89
+ await asyncio.sleep(
90
+ self._config.sleep_time_before_update
91
+ ) # Frontend rendering issues
92
+
93
+ def _get_displayed_sub_agent_responses(
94
+ self,
95
+ ) -> dict[str, list[SubAgentResponse]]:
96
+ responses = defaultdict(list)
97
+ all_responses = self._response_watcher.get_all_responses()
98
+ for response in all_responses:
99
+ assistant_id = response.assistant_id
100
+ if assistant_id in self._display_specs:
101
+ responses[assistant_id].append(response)
102
+ return responses
103
+
104
+ @override
105
+ def apply_postprocessing_to_response(
106
+ self, loop_response: LanguageModelStreamResponse
107
+ ) -> bool:
108
+ displayed_sub_agent_responses = self._get_displayed_sub_agent_responses()
109
+
110
+ if len(displayed_sub_agent_responses) == 0:
111
+ logger.info("No sub agent responses to prepend")
112
+ return False
113
+
114
+ logger.info("Prepending sub agent responses to the main agent response")
115
+
116
+ answers_displayed_before = []
117
+ answers_displayed_after = []
118
+ all_displayed_answers = set()
119
+
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
+
124
+ for response in responses:
125
+ message = response.message
126
+
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
+ )
134
+ continue
135
+
136
+ if message["text"] is None:
137
+ logger.warning(
138
+ "Sub agent response for assistant %s with sequence number %s does not contain any text",
139
+ assistant_id,
140
+ response.sequence_number,
141
+ )
142
+ continue
143
+
144
+ answer_parts = get_sub_agent_answer_parts(
145
+ answer=message["text"],
146
+ display_config=tool_info.display_config,
147
+ )
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
+
169
+ if len(answer_parts) == 0:
170
+ continue
171
+
172
+ display_name = tool_name
173
+ if len(responses) > 1:
174
+ display_name = tool_name + f" {response.sequence_number}"
175
+
176
+ answer = get_sub_agent_answer_display(
177
+ display_name=display_name,
178
+ display_config=tool_info.display_config,
179
+ answer=answer,
180
+ assistant_id=assistant_id,
181
+ )
182
+
183
+ if tool_info.display_config.position == "before":
184
+ answers_displayed_before.append(answer)
185
+ else:
186
+ answers_displayed_after.append(answer)
187
+
188
+ loop_response.message.text = _get_final_answer_display(
189
+ text=loop_response.message.text,
190
+ answers_before=answers_displayed_before,
191
+ answers_after=answers_displayed_after,
192
+ template=self._config.answers_jinja_template,
193
+ )
194
+
195
+ return True
196
+
197
+ @override
198
+ async def remove_from_text(self, text) -> str:
199
+ for display_info in self._display_specs.values():
200
+ text = remove_sub_agent_answer_from_text(
201
+ display_config=display_info.display_config,
202
+ text=text,
203
+ assistant_id=display_info.assistant_id,
204
+ )
205
+ return text
206
+
207
+
208
+ def _add_response_references_to_message_in_place(
209
+ loop_response: LanguageModelStreamResponse,
210
+ response: unique_sdk.Space.Message,
211
+ remove_unused_references: bool = True,
212
+ ) -> None:
213
+ references = response["references"]
214
+ text = response["text"]
215
+
216
+ if references is None or len(references) == 0 or text is None:
217
+ return
218
+
219
+ content_refs = [ContentReference.from_sdk_reference(ref) for ref in references]
220
+
221
+ if remove_unused_references:
222
+ content_refs = remove_unused_refs(
223
+ references=content_refs,
224
+ text=text,
225
+ )
226
+
227
+ text, refs = add_content_refs_and_replace_in_text(
228
+ message_text=text,
229
+ message_refs=loop_response.message.references,
230
+ new_refs=content_refs,
231
+ )
232
+
233
+ response["text"] = text # Diplayed at a later stage
234
+ loop_response.message.references = refs
235
+
236
+
237
+ def _get_final_answer_display(
238
+ text: str,
239
+ answers_before: list[str],
240
+ answers_after: list[str],
241
+ template: str = _ANSWERS_JINJA_TEMPLATE,
242
+ ) -> str:
243
+ if len(answers_before) > 0:
244
+ text = render_template(template, {"answers": answers_before}) + text
245
+
246
+ if len(answers_after) > 0:
247
+ text = text + render_template(template, {"answers": answers_after})
248
+
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,101 @@
1
+ import logging
2
+ import re
3
+ from typing import override
4
+
5
+ from unique_toolkit._common.referencing import (
6
+ get_reference_pattern,
7
+ remove_consecutive_ref_space,
8
+ )
9
+ from unique_toolkit.agentic.postprocessor.postprocessor_manager import Postprocessor
10
+ from unique_toolkit.agentic.tools.a2a.postprocessing._ref_utils import (
11
+ add_content_refs_and_replace_in_text,
12
+ )
13
+ from unique_toolkit.agentic.tools.a2a.response_watcher import (
14
+ SubAgentResponse,
15
+ SubAgentResponseWatcher,
16
+ )
17
+ from unique_toolkit.agentic.tools.a2a.tool import SubAgentTool
18
+ from unique_toolkit.content import ContentReference
19
+ from unique_toolkit.language_model.schemas import LanguageModelStreamResponse
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ class SubAgentReferencesPostprocessor(Postprocessor):
25
+ def __init__(self, response_watcher: SubAgentResponseWatcher) -> None:
26
+ super().__init__(name=self.__class__.__name__)
27
+ self._response_watcher = response_watcher
28
+
29
+ @override
30
+ async def run(self, loop_response: LanguageModelStreamResponse) -> None:
31
+ return
32
+
33
+ @override
34
+ def apply_postprocessing_to_response(
35
+ self, loop_response: LanguageModelStreamResponse
36
+ ) -> bool:
37
+ logger.info("Adding sub agent references to the main agent response")
38
+
39
+ num_sources = len(loop_response.message.references)
40
+
41
+ # At the moment, the `PostprocessorManager` expects modifications to happen in place
42
+ _add_sub_agent_references_in_place(
43
+ loop_response=loop_response,
44
+ responses=self._response_watcher.get_all_responses(),
45
+ )
46
+
47
+ return num_sources != len(
48
+ loop_response.message.references
49
+ ) # We only add references
50
+
51
+ @override
52
+ async def remove_from_text(self, text: str) -> str:
53
+ """
54
+ It is not possible to **only** remove sub agent references from the text,
55
+ as they are identical to normal references.
56
+ """
57
+ return text
58
+
59
+
60
+ def _add_sub_agent_references_in_place(
61
+ loop_response: LanguageModelStreamResponse,
62
+ responses: list[SubAgentResponse],
63
+ ) -> None:
64
+ text = loop_response.message.text
65
+ refs = []
66
+
67
+ for response in responses:
68
+ sub_agent_refs = []
69
+ references = response.message["references"]
70
+
71
+ if references is None or len(references) == 0:
72
+ continue
73
+
74
+ for reference in sorted(references, key=lambda r: r["sequenceNumber"]):
75
+ reference_re = SubAgentTool.get_sub_agent_reference_re(
76
+ name=response.name,
77
+ sequence_number=response.sequence_number,
78
+ reference_number=reference["sequenceNumber"],
79
+ )
80
+
81
+ if re.search(reference_re, text) is None:
82
+ # Reference not used
83
+ continue
84
+
85
+ sub_agent_refs.append(ContentReference.from_sdk_reference(reference))
86
+
87
+ text, refs = add_content_refs_and_replace_in_text(
88
+ message_text=text,
89
+ message_refs=refs,
90
+ new_refs=sub_agent_refs,
91
+ ref_pattern_f=lambda x: r"\s*" # Normalize spaces
92
+ + SubAgentTool.get_sub_agent_reference_re(
93
+ name=response.name,
94
+ sequence_number=response.sequence_number,
95
+ reference_number=x,
96
+ ),
97
+ ref_replacement_f=lambda x: " " + get_reference_pattern(x),
98
+ )
99
+
100
+ loop_response.message.references = refs
101
+ loop_response.message.text = remove_consecutive_ref_space(text)