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,260 @@
1
+ import logging
2
+ from typing import NamedTuple, override
3
+
4
+ import unique_sdk
5
+ from jinja2 import Template
6
+ from pydantic import BaseModel
7
+
8
+ from unique_toolkit.agentic.evaluation.evaluation_manager import Evaluation
9
+ from unique_toolkit.agentic.evaluation.schemas import (
10
+ EvaluationAssessmentMessage,
11
+ EvaluationMetricName,
12
+ EvaluationMetricResult,
13
+ )
14
+ from unique_toolkit.agentic.tools.a2a.evaluation._utils import (
15
+ get_valid_assessments,
16
+ get_worst_label,
17
+ sort_assessments,
18
+ )
19
+ from unique_toolkit.agentic.tools.a2a.evaluation.config import (
20
+ SubAgentEvaluationConfig,
21
+ SubAgentEvaluationServiceConfig,
22
+ )
23
+ from unique_toolkit.agentic.tools.a2a.response_watcher import (
24
+ SubAgentResponse,
25
+ SubAgentResponseWatcher,
26
+ )
27
+ from unique_toolkit.agentic.tools.utils import failsafe
28
+ from unique_toolkit.chat.schemas import (
29
+ ChatMessageAssessmentLabel,
30
+ ChatMessageAssessmentStatus,
31
+ ChatMessageAssessmentType,
32
+ )
33
+ from unique_toolkit.language_model.builder import MessagesBuilder
34
+ from unique_toolkit.language_model.schemas import LanguageModelStreamResponse
35
+ from unique_toolkit.language_model.service import LanguageModelService
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+
40
+ class SubAgentEvaluationSpec(NamedTuple):
41
+ display_name: str
42
+ assistant_id: str
43
+ config: SubAgentEvaluationConfig
44
+
45
+
46
+ _NO_ASSESSMENTS_FOUND = "NO_ASSESSMENTS_FOUND"
47
+
48
+
49
+ class _SingleAssessmentData(BaseModel):
50
+ name: str
51
+ explanation: str
52
+
53
+
54
+ def _format_single_assessment_found(name: str, explanation: str) -> str:
55
+ return _SingleAssessmentData(name=name, explanation=explanation).model_dump_json()
56
+
57
+
58
+ @failsafe(failure_return_value=None, log_exceptions=False)
59
+ def _parse_single_assesment_found(value: str) -> _SingleAssessmentData | None:
60
+ return _SingleAssessmentData.model_validate_json(value)
61
+
62
+
63
+ def _find_single_assessment(
64
+ responses: dict[str, list[SubAgentResponse]],
65
+ ) -> unique_sdk.Space.Assessment | None:
66
+ if len(responses) == 1:
67
+ sub_agent_responses = next(iter(responses.values()))
68
+ if len(sub_agent_responses) == 1:
69
+ response = sub_agent_responses[0].message
70
+ if response["assessment"] is not None and len(response["assessment"]) == 1:
71
+ return response["assessment"][0]
72
+
73
+ return None
74
+
75
+
76
+ class SubAgentEvaluationService(Evaluation):
77
+ DISPLAY_NAME = "Sub Agents"
78
+
79
+ def __init__(
80
+ self,
81
+ config: SubAgentEvaluationServiceConfig,
82
+ language_model_service: LanguageModelService,
83
+ response_watcher: SubAgentResponseWatcher,
84
+ evaluation_specs: list[SubAgentEvaluationSpec],
85
+ ) -> None:
86
+ super().__init__(EvaluationMetricName.SUB_AGENT)
87
+ self._config = config
88
+
89
+ self._response_watcher = response_watcher
90
+ self._language_model_service = language_model_service
91
+
92
+ self._evaluation_specs: dict[str, SubAgentEvaluationSpec] = {
93
+ spec.assistant_id: spec
94
+ for spec in evaluation_specs
95
+ if spec.config.include_evaluation
96
+ }
97
+
98
+ @override
99
+ def get_assessment_type(self) -> ChatMessageAssessmentType:
100
+ return self._config.assessment_type
101
+
102
+ def _get_included_sub_agent_responses(
103
+ self,
104
+ ) -> dict[str, list[SubAgentResponse]]:
105
+ responses = {}
106
+ for assistant_id, eval_spec in self._evaluation_specs.items():
107
+ sub_agent_responses = self._response_watcher.get_responses(
108
+ eval_spec.assistant_id
109
+ )
110
+ if len(sub_agent_responses) == 0:
111
+ logger.debug(
112
+ "No responses for sub agent %s (%s)",
113
+ eval_spec.display_name,
114
+ eval_spec.assistant_id,
115
+ )
116
+ continue
117
+
118
+ responses_with_assessment = []
119
+ for response in sub_agent_responses:
120
+ assessments = response.message["assessment"]
121
+
122
+ if assessments is None or len(assessments) == 0:
123
+ logger.debug(
124
+ "No assessment for sub agent %s (%s) response with sequence number %s",
125
+ eval_spec.display_name,
126
+ eval_spec.assistant_id,
127
+ response.sequence_number,
128
+ )
129
+ continue
130
+
131
+ assessments = get_valid_assessments(
132
+ assessments=assessments,
133
+ display_name=eval_spec.display_name,
134
+ sequence_number=response.sequence_number,
135
+ )
136
+
137
+ if len(assessments) > 0:
138
+ responses_with_assessment.append(response)
139
+
140
+ responses[assistant_id] = responses_with_assessment
141
+
142
+ return responses
143
+
144
+ @override
145
+ async def run(
146
+ self, loop_response: LanguageModelStreamResponse
147
+ ) -> EvaluationMetricResult:
148
+ logger.info("Running sub agents evaluation")
149
+
150
+ sub_agents_display_data = []
151
+
152
+ responses = self._get_included_sub_agent_responses()
153
+
154
+ # No valid assessments found
155
+ if len(responses) == 0:
156
+ logger.warning("No valid sub agent assessments found")
157
+
158
+ return EvaluationMetricResult(
159
+ name=self.get_name(),
160
+ # This is a trick to be able to indicate to `evaluation_metric_to_assessment`
161
+ # that no valid assessments were found
162
+ value=_NO_ASSESSMENTS_FOUND,
163
+ reason="No sub agents assessments found",
164
+ )
165
+
166
+ single_assessment = _find_single_assessment(responses)
167
+ # Only one valid assessment found, no need to perform summarization
168
+ if single_assessment is not None:
169
+ assistant_id = next(iter(responses))
170
+ explanation = single_assessment["explanation"] or ""
171
+ name = self._evaluation_specs[assistant_id].display_name
172
+ label = single_assessment["label"] or ""
173
+
174
+ return EvaluationMetricResult(
175
+ name=self.get_name(),
176
+ value=label,
177
+ # This is a trick to be able to pass the display name to the UI in `evaluation_metric_to_assessment`
178
+ reason=_format_single_assessment_found(name, explanation),
179
+ is_positive=label == ChatMessageAssessmentLabel.GREEN,
180
+ )
181
+
182
+ sub_agents_display_data = []
183
+
184
+ # Multiple Assessments found
185
+ value = ChatMessageAssessmentLabel.GREEN
186
+ for assistant_id, sub_agent_responses in responses.items():
187
+ display_name = self._evaluation_specs[assistant_id].display_name
188
+
189
+ for response in sub_agent_responses:
190
+ assessments = sort_assessments(response.message["assessment"]) #  type:ignore
191
+ value = get_worst_label(value, assessments[0]["label"]) # type: ignore
192
+
193
+ data = {
194
+ "name": display_name,
195
+ "assessments": assessments,
196
+ }
197
+ if len(sub_agent_responses) > 1:
198
+ data["name"] += f" {response.sequence_number}"
199
+
200
+ sub_agents_display_data.append(data)
201
+
202
+ reason = await self._get_reason(sub_agents_display_data)
203
+
204
+ return EvaluationMetricResult(
205
+ name=self.get_name(),
206
+ value=value,
207
+ reason=reason,
208
+ is_positive=value == ChatMessageAssessmentLabel.GREEN,
209
+ )
210
+
211
+ @override
212
+ async def evaluation_metric_to_assessment(
213
+ self, evaluation_result: EvaluationMetricResult
214
+ ) -> EvaluationAssessmentMessage:
215
+ if evaluation_result.value == _NO_ASSESSMENTS_FOUND:
216
+ return EvaluationAssessmentMessage(
217
+ status=ChatMessageAssessmentStatus.DONE,
218
+ explanation="No valid sub agents assessments found to consolidate.",
219
+ title=self.DISPLAY_NAME,
220
+ label=ChatMessageAssessmentLabel.GREEN,
221
+ type=self.get_assessment_type(),
222
+ )
223
+
224
+ single_assessment_data = _parse_single_assesment_found(evaluation_result.reason)
225
+ if single_assessment_data is not None:
226
+ return EvaluationAssessmentMessage(
227
+ status=ChatMessageAssessmentStatus.DONE,
228
+ explanation=single_assessment_data.explanation,
229
+ title=single_assessment_data.name,
230
+ label=evaluation_result.value, # type: ignore
231
+ type=self.get_assessment_type(),
232
+ )
233
+
234
+ return EvaluationAssessmentMessage(
235
+ status=ChatMessageAssessmentStatus.DONE,
236
+ explanation=evaluation_result.reason,
237
+ title=self.DISPLAY_NAME,
238
+ label=evaluation_result.value, # type: ignore
239
+ type=self.get_assessment_type(),
240
+ )
241
+
242
+ async def _get_reason(self, sub_agents_display_data: list[dict]) -> str:
243
+ messages = (
244
+ MessagesBuilder()
245
+ .system_message_append(self._config.summarization_system_message)
246
+ .user_message_append(
247
+ Template(self._config.summarization_user_message_template).render(
248
+ sub_agents=sub_agents_display_data,
249
+ )
250
+ )
251
+ .build()
252
+ )
253
+
254
+ reason = await self._language_model_service.complete_async(
255
+ messages=messages,
256
+ model_name=self._config.summarization_model.name,
257
+ temperature=0.0,
258
+ )
259
+
260
+ return str(reason.choices[0].message.content)
@@ -0,0 +1,9 @@
1
+ Here are the sub_agent(s) assessments:
2
+
3
+ {% for sub_agent in sub_agents %}
4
+ - Agent name: {{ sub_agent.name }}
5
+ {% for assessment in sub_agent.assessments %}
6
+ - {{ assessment.title }}: {{ assessment.label }}
7
+ {{ assessment.explanation }}
8
+ {% endfor %}
9
+ {% endfor %}
@@ -0,0 +1,55 @@
1
+ from logging import Logger
2
+
3
+ from unique_toolkit.agentic.tools.a2a.response_watcher import SubAgentResponseWatcher
4
+ from unique_toolkit.agentic.tools.a2a.tool import SubAgentTool, SubAgentToolConfig
5
+ from unique_toolkit.agentic.tools.config import ToolBuildConfig
6
+ from unique_toolkit.agentic.tools.tool_progress_reporter import ToolProgressReporter
7
+ from unique_toolkit.app.schemas import ChatEvent
8
+
9
+
10
+ class A2AManager:
11
+ def __init__(
12
+ self,
13
+ logger: Logger,
14
+ tool_progress_reporter: ToolProgressReporter,
15
+ response_watcher: SubAgentResponseWatcher,
16
+ ):
17
+ self._logger = logger
18
+ self._tool_progress_reporter = tool_progress_reporter
19
+ self._response_watcher = response_watcher
20
+
21
+ def get_all_sub_agents(
22
+ self,
23
+ tool_configs: list[ToolBuildConfig],
24
+ event: ChatEvent,
25
+ ) -> tuple[list[ToolBuildConfig], list[SubAgentTool]]:
26
+ sub_agents = []
27
+
28
+ for tool_config in tool_configs:
29
+ if not tool_config.is_sub_agent:
30
+ continue
31
+
32
+ if not isinstance(tool_config.configuration, SubAgentToolConfig):
33
+ self._logger.error(
34
+ "tool_config.configuration must be of type SubAgentToolConfig"
35
+ )
36
+ continue
37
+
38
+ sub_agent_tool_config = tool_config.configuration
39
+
40
+ sub_agents.append(
41
+ SubAgentTool(
42
+ configuration=sub_agent_tool_config,
43
+ event=event,
44
+ tool_progress_reporter=self._tool_progress_reporter,
45
+ name=tool_config.name,
46
+ display_name=tool_config.display_name,
47
+ response_watcher=self._response_watcher,
48
+ )
49
+ )
50
+
51
+ filtered_tool_config = [
52
+ tool_config for tool_config in tool_configs if not tool_config.is_sub_agent
53
+ ]
54
+
55
+ return filtered_tool_config, sub_agents
@@ -0,0 +1,21 @@
1
+ from unique_toolkit.agentic.tools.a2a.postprocessing.config import (
2
+ SubAgentDisplayConfig,
3
+ SubAgentResponseDisplayMode,
4
+ )
5
+ from unique_toolkit.agentic.tools.a2a.postprocessing.display import (
6
+ SubAgentDisplaySpec,
7
+ SubAgentResponsesDisplayPostprocessor,
8
+ SubAgentResponsesPostprocessorConfig,
9
+ )
10
+ from unique_toolkit.agentic.tools.a2a.postprocessing.references import (
11
+ SubAgentReferencesPostprocessor,
12
+ )
13
+
14
+ __all__ = [
15
+ "SubAgentResponsesDisplayPostprocessor",
16
+ "SubAgentResponsesPostprocessorConfig",
17
+ "SubAgentDisplaySpec",
18
+ "SubAgentResponseDisplayMode",
19
+ "SubAgentDisplayConfig",
20
+ "SubAgentReferencesPostprocessor",
21
+ ]
@@ -0,0 +1,240 @@
1
+ import re
2
+ from typing import Literal, NamedTuple
3
+
4
+ from unique_toolkit._common.utils.jinja.render import render_template
5
+ from unique_toolkit.agentic.tools.a2a.postprocessing.config import (
6
+ SubAgentDisplayConfig,
7
+ SubAgentResponseDisplayMode,
8
+ )
9
+
10
+
11
+ def _wrap_text(text: str, start_text: str, end_text: str) -> str:
12
+ text = text.strip()
13
+ start_text = start_text.strip()
14
+ end_text = end_text.strip()
15
+
16
+ if start_text != "":
17
+ start_text = f"{start_text}\n"
18
+
19
+ if end_text != "":
20
+ end_text = f"\n{end_text}"
21
+
22
+ return f"{start_text}{text}{end_text}"
23
+
24
+
25
+ def _join_text_blocks(*blocks: str, sep: str = "\n") -> str:
26
+ return sep.join(block.strip() for block in blocks)
27
+
28
+
29
+ def _wrap_with_details_tag(
30
+ text, mode: Literal["open", "closed"], summary_name: str | None = None
31
+ ) -> str:
32
+ if summary_name is not None:
33
+ summary_tag = _wrap_text(summary_name, "<summary>", "</summary>")
34
+ text = _join_text_blocks(summary_tag, text)
35
+
36
+ if mode == "open":
37
+ text = _wrap_text(text, "<details open>", "</details>")
38
+ else:
39
+ text = _wrap_text(text, "<details>", "</details>")
40
+
41
+ return text
42
+
43
+
44
+ _BLOCK_BORDER_STYLE = (
45
+ "overflow-y: auto; border: 1px solid #ccc; padding: 8px; margin-top: 8px;"
46
+ )
47
+
48
+
49
+ def _wrap_with_block_border(text: str) -> str:
50
+ return _wrap_text(text, f"<div style='{_BLOCK_BORDER_STYLE}'>", "</div>")
51
+
52
+
53
+ _QUOTE_BORDER_STYLE = (
54
+ "margin-left: 20px; border-left: 2px solid #ccc; padding-left: 10px;"
55
+ )
56
+
57
+
58
+ def _wrap_with_quote_border(text: str) -> str:
59
+ return _wrap_text(text, f"<div style='{_QUOTE_BORDER_STYLE}'>", "</div>")
60
+
61
+
62
+ def _wrap_strong(text: str) -> str:
63
+ return _wrap_text(text, "<strong>", "</strong>")
64
+
65
+
66
+ def _wrap_hidden_div(text: str) -> str:
67
+ return _wrap_text(text, '<div style="display: none;">', "</div>")
68
+
69
+
70
+ def _add_line_break(text: str, before: bool = True, after: bool = True) -> str:
71
+ start_tag = ""
72
+ if before:
73
+ start_tag = "<br>"
74
+
75
+ end_tag = ""
76
+ if after:
77
+ end_tag = "<br>"
78
+
79
+ return _wrap_text(text, start_tag, end_tag)
80
+
81
+
82
+ def _prepare_title_template(
83
+ display_title_template: str, display_name_placeholder: str
84
+ ) -> str:
85
+ return display_title_template.replace("{}", "{%s}" % display_name_placeholder)
86
+
87
+
88
+ def _clean_linebreaks(text: str) -> str:
89
+ text = text.strip()
90
+ text = re.sub(r"^(<br>)*|(<br>)*$", "", text)
91
+ return text
92
+
93
+
94
+ def _get_display_template(
95
+ mode: SubAgentResponseDisplayMode,
96
+ add_quote_border: bool,
97
+ add_block_border: bool,
98
+ display_title_template: str,
99
+ answer_placeholder: str = "answer",
100
+ assistant_id_placeholder: str = "assistant_id",
101
+ display_name_placeholder: str = "display_name",
102
+ ) -> str:
103
+ if mode == SubAgentResponseDisplayMode.HIDDEN:
104
+ return ""
105
+
106
+ assistant_id_placeholder = _wrap_hidden_div("{%s}" % assistant_id_placeholder)
107
+ title_template = _prepare_title_template(
108
+ display_title_template, display_name_placeholder
109
+ )
110
+ template = _join_text_blocks(
111
+ assistant_id_placeholder, "{%s}" % answer_placeholder, sep="\n\n"
112
+ ) # Double line break is needed for markdown formatting
113
+
114
+ template = _add_line_break(template, before=True, after=False)
115
+
116
+ if add_quote_border:
117
+ template = _wrap_with_quote_border(template)
118
+
119
+ match mode:
120
+ case SubAgentResponseDisplayMode.DETAILS_OPEN:
121
+ template = _wrap_with_details_tag(
122
+ template,
123
+ "open",
124
+ title_template,
125
+ )
126
+ case SubAgentResponseDisplayMode.DETAILS_CLOSED:
127
+ template = _wrap_with_details_tag(template, "closed", title_template)
128
+ case SubAgentResponseDisplayMode.PLAIN:
129
+ # Add a hidden block border to seperate sub agent answers from the rest of the text.
130
+ hidden_block_border = _wrap_hidden_div("sub_agent_answer_block")
131
+ template = _join_text_blocks(title_template, template, hidden_block_border)
132
+
133
+ if add_block_border:
134
+ template = _wrap_with_block_border(template)
135
+
136
+ return _clean_linebreaks(template)
137
+
138
+
139
+ def _get_display_removal_re(
140
+ assistant_id: str,
141
+ mode: SubAgentResponseDisplayMode,
142
+ add_quote_border: bool,
143
+ add_block_border: bool,
144
+ display_title_template: str,
145
+ ) -> re.Pattern[str]:
146
+ template = _get_display_template(
147
+ mode=mode,
148
+ add_quote_border=add_quote_border,
149
+ add_block_border=add_block_border,
150
+ display_title_template=display_title_template,
151
+ )
152
+
153
+ pattern = template.format(
154
+ assistant_id=re.escape(assistant_id), answer=r"(.*?)", display_name=r"(.*?)"
155
+ )
156
+
157
+ return re.compile(pattern, flags=re.DOTALL)
158
+
159
+
160
+ class SubAgentAnswerPart(NamedTuple):
161
+ matching_text: str # Matching text as found in the answer
162
+ formatted_text: str # Formatted text to be displayed
163
+
164
+
165
+ def get_sub_agent_answer_parts(
166
+ answer: str,
167
+ display_config: SubAgentDisplayConfig,
168
+ ) -> list[SubAgentAnswerPart]:
169
+ if display_config.mode == SubAgentResponseDisplayMode.HIDDEN:
170
+ return []
171
+
172
+ if len(display_config.answer_substrings_config) == 0:
173
+ return [SubAgentAnswerPart(matching_text=answer, formatted_text=answer)]
174
+
175
+ substrings = []
176
+ for config in display_config.answer_substrings_config:
177
+ for match in config.regexp.finditer(answer):
178
+ text = match.group(0)
179
+ substrings.append(
180
+ SubAgentAnswerPart(
181
+ matching_text=text,
182
+ formatted_text=config.display_template.format(text),
183
+ )
184
+ )
185
+
186
+ return substrings
187
+
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
+
201
+ def get_sub_agent_answer_display(
202
+ display_name: str,
203
+ display_config: SubAgentDisplayConfig,
204
+ answer: str | list[SubAgentAnswerPart],
205
+ assistant_id: str,
206
+ ) -> str:
207
+ template = _get_display_template(
208
+ mode=display_config.mode,
209
+ add_quote_border=display_config.add_quote_border,
210
+ add_block_border=display_config.add_block_border,
211
+ display_title_template=display_config.display_title_template,
212
+ )
213
+
214
+ if isinstance(answer, list):
215
+ answer = get_sub_agent_answer_from_parts(
216
+ answer_parts=answer,
217
+ config=display_config,
218
+ )
219
+
220
+ return template.format(
221
+ display_name=display_name, answer=answer, assistant_id=assistant_id
222
+ )
223
+
224
+
225
+ def remove_sub_agent_answer_from_text(
226
+ display_config: SubAgentDisplayConfig,
227
+ text: str,
228
+ assistant_id: str,
229
+ ) -> str:
230
+ if not display_config.remove_from_history:
231
+ return text
232
+
233
+ pattern = _get_display_removal_re(
234
+ assistant_id=assistant_id,
235
+ mode=display_config.mode,
236
+ add_quote_border=display_config.add_quote_border,
237
+ add_block_border=display_config.add_block_border,
238
+ display_title_template=display_config.display_title_template,
239
+ )
240
+ return re.sub(pattern, "", text)
@@ -0,0 +1,84 @@
1
+ import re
2
+ from typing import Callable, Iterable, Mapping, Sequence
3
+
4
+ from unique_toolkit._common.referencing import get_reference_pattern
5
+ from unique_toolkit._common.string_utilities import replace_in_text
6
+ from unique_toolkit.content import ContentReference
7
+
8
+ SourceId = str
9
+ SequenceNumber = int
10
+
11
+
12
+ def _add_source_ids(
13
+ existing_refs: Mapping[SourceId, SequenceNumber],
14
+ new_refs: Iterable[SourceId],
15
+ ) -> dict[SourceId, SequenceNumber]:
16
+ next_seq_num = max(existing_refs.values(), default=0) + 1
17
+ new_seq_nums: dict[SourceId, SequenceNumber] = {}
18
+
19
+ for source_id in new_refs:
20
+ seq_num = existing_refs.get(source_id, None) or new_seq_nums.get(
21
+ source_id, None
22
+ )
23
+ if seq_num is None:
24
+ new_seq_nums[source_id] = next_seq_num
25
+ next_seq_num += 1
26
+
27
+ return new_seq_nums
28
+
29
+
30
+ def add_content_refs(
31
+ message_refs: Sequence[ContentReference],
32
+ new_refs: Sequence[ContentReference],
33
+ ) -> list[ContentReference]:
34
+ message_refs = list(message_refs)
35
+
36
+ if len(new_refs) == 0:
37
+ return message_refs
38
+
39
+ existing_refs = {ref.source_id: ref.sequence_number for ref in message_refs}
40
+ new_refs_by_source_id = {
41
+ ref.source_id: ref for ref in sorted(new_refs, key=lambda x: x.sequence_number)
42
+ }
43
+ new_seq_nums = _add_source_ids(existing_refs, new_refs_by_source_id.keys())
44
+
45
+ for source_id, seq_num in new_seq_nums.items():
46
+ ref = new_refs_by_source_id[source_id]
47
+ message_refs.append(
48
+ ref.model_copy(update={"sequence_number": seq_num}, deep=True)
49
+ )
50
+
51
+ return message_refs
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
+
64
+ def add_content_refs_and_replace_in_text(
65
+ message_text: str,
66
+ message_refs: Sequence[ContentReference],
67
+ new_refs: Sequence[ContentReference],
68
+ ref_pattern_f: Callable[[int], str] = get_reference_pattern,
69
+ ref_replacement_f: Callable[[int], str] = get_reference_pattern,
70
+ ) -> tuple[str, list[ContentReference]]:
71
+ if len(new_refs) == 0:
72
+ return message_text, list(message_refs)
73
+
74
+ references = add_content_refs(message_refs, new_refs)
75
+ seq_num_for_source_id = {ref.source_id: ref.sequence_number for ref in references}
76
+ ref_map = []
77
+
78
+ for ref in new_refs:
79
+ old_seq_num = ref.sequence_number
80
+ new_seq_num = seq_num_for_source_id[ref.source_id]
81
+
82
+ ref_map.append((ref_pattern_f(old_seq_num), ref_replacement_f(new_seq_num)))
83
+
84
+ return replace_in_text(message_text, ref_map), references