unique_toolkit 0.7.7__py3-none-any.whl → 1.23.0__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.

Potentially problematic release.


This version of unique_toolkit might be problematic. Click here for more details.

Files changed (166) hide show
  1. unique_toolkit/__init__.py +28 -1
  2. unique_toolkit/_common/api_calling/human_verification_manager.py +343 -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 +252 -0
  14. unique_toolkit/_common/docx_generator/template/Doc Template.docx +0 -0
  15. unique_toolkit/_common/endpoint_builder.py +305 -0
  16. unique_toolkit/_common/endpoint_requestor.py +430 -0
  17. unique_toolkit/_common/exception.py +24 -0
  18. unique_toolkit/_common/feature_flags/schema.py +9 -0
  19. unique_toolkit/_common/pydantic/rjsf_tags.py +936 -0
  20. unique_toolkit/_common/pydantic_helpers.py +154 -0
  21. unique_toolkit/_common/referencing.py +53 -0
  22. unique_toolkit/_common/string_utilities.py +140 -0
  23. unique_toolkit/_common/tests/test_referencing.py +521 -0
  24. unique_toolkit/_common/tests/test_string_utilities.py +506 -0
  25. unique_toolkit/_common/token/image_token_counting.py +67 -0
  26. unique_toolkit/_common/token/token_counting.py +204 -0
  27. unique_toolkit/_common/utils/__init__.py +1 -0
  28. unique_toolkit/_common/utils/files.py +43 -0
  29. unique_toolkit/_common/utils/structured_output/__init__.py +1 -0
  30. unique_toolkit/_common/utils/structured_output/schema.py +5 -0
  31. unique_toolkit/_common/utils/write_configuration.py +51 -0
  32. unique_toolkit/_common/validators.py +101 -4
  33. unique_toolkit/agentic/__init__.py +1 -0
  34. unique_toolkit/agentic/debug_info_manager/debug_info_manager.py +28 -0
  35. unique_toolkit/agentic/debug_info_manager/test/test_debug_info_manager.py +278 -0
  36. unique_toolkit/agentic/evaluation/config.py +36 -0
  37. unique_toolkit/{evaluators → agentic/evaluation}/context_relevancy/prompts.py +25 -0
  38. unique_toolkit/agentic/evaluation/context_relevancy/schema.py +80 -0
  39. unique_toolkit/agentic/evaluation/context_relevancy/service.py +273 -0
  40. unique_toolkit/agentic/evaluation/evaluation_manager.py +218 -0
  41. unique_toolkit/agentic/evaluation/hallucination/constants.py +61 -0
  42. unique_toolkit/agentic/evaluation/hallucination/hallucination_evaluation.py +111 -0
  43. unique_toolkit/{evaluators → agentic/evaluation}/hallucination/prompts.py +1 -1
  44. unique_toolkit/{evaluators → agentic/evaluation}/hallucination/service.py +16 -15
  45. unique_toolkit/{evaluators → agentic/evaluation}/hallucination/utils.py +30 -20
  46. unique_toolkit/{evaluators → agentic/evaluation}/output_parser.py +20 -2
  47. unique_toolkit/{evaluators → agentic/evaluation}/schemas.py +27 -7
  48. unique_toolkit/agentic/evaluation/tests/test_context_relevancy_service.py +253 -0
  49. unique_toolkit/agentic/evaluation/tests/test_output_parser.py +87 -0
  50. unique_toolkit/agentic/history_manager/history_construction_with_contents.py +297 -0
  51. unique_toolkit/agentic/history_manager/history_manager.py +242 -0
  52. unique_toolkit/agentic/history_manager/loop_token_reducer.py +484 -0
  53. unique_toolkit/agentic/history_manager/utils.py +96 -0
  54. unique_toolkit/agentic/postprocessor/postprocessor_manager.py +212 -0
  55. unique_toolkit/agentic/reference_manager/reference_manager.py +103 -0
  56. unique_toolkit/agentic/responses_api/__init__.py +19 -0
  57. unique_toolkit/agentic/responses_api/postprocessors/code_display.py +63 -0
  58. unique_toolkit/agentic/responses_api/postprocessors/generated_files.py +145 -0
  59. unique_toolkit/agentic/responses_api/stream_handler.py +15 -0
  60. unique_toolkit/agentic/short_term_memory_manager/persistent_short_term_memory_manager.py +141 -0
  61. unique_toolkit/agentic/thinking_manager/thinking_manager.py +103 -0
  62. unique_toolkit/agentic/tools/__init__.py +1 -0
  63. unique_toolkit/agentic/tools/a2a/__init__.py +36 -0
  64. unique_toolkit/agentic/tools/a2a/config.py +17 -0
  65. unique_toolkit/agentic/tools/a2a/evaluation/__init__.py +15 -0
  66. unique_toolkit/agentic/tools/a2a/evaluation/_utils.py +66 -0
  67. unique_toolkit/agentic/tools/a2a/evaluation/config.py +55 -0
  68. unique_toolkit/agentic/tools/a2a/evaluation/evaluator.py +260 -0
  69. unique_toolkit/agentic/tools/a2a/evaluation/summarization_user_message.j2 +9 -0
  70. unique_toolkit/agentic/tools/a2a/manager.py +55 -0
  71. unique_toolkit/agentic/tools/a2a/postprocessing/__init__.py +21 -0
  72. unique_toolkit/agentic/tools/a2a/postprocessing/_display_utils.py +185 -0
  73. unique_toolkit/agentic/tools/a2a/postprocessing/_ref_utils.py +73 -0
  74. unique_toolkit/agentic/tools/a2a/postprocessing/config.py +45 -0
  75. unique_toolkit/agentic/tools/a2a/postprocessing/display.py +180 -0
  76. unique_toolkit/agentic/tools/a2a/postprocessing/references.py +101 -0
  77. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display_utils.py +1335 -0
  78. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_ref_utils.py +603 -0
  79. unique_toolkit/agentic/tools/a2a/prompts.py +46 -0
  80. unique_toolkit/agentic/tools/a2a/response_watcher/__init__.py +6 -0
  81. unique_toolkit/agentic/tools/a2a/response_watcher/service.py +91 -0
  82. unique_toolkit/agentic/tools/a2a/tool/__init__.py +4 -0
  83. unique_toolkit/agentic/tools/a2a/tool/_memory.py +26 -0
  84. unique_toolkit/agentic/tools/a2a/tool/_schema.py +9 -0
  85. unique_toolkit/agentic/tools/a2a/tool/config.py +73 -0
  86. unique_toolkit/agentic/tools/a2a/tool/service.py +306 -0
  87. unique_toolkit/agentic/tools/agent_chunks_hanlder.py +65 -0
  88. unique_toolkit/agentic/tools/config.py +167 -0
  89. unique_toolkit/agentic/tools/factory.py +44 -0
  90. unique_toolkit/agentic/tools/mcp/__init__.py +4 -0
  91. unique_toolkit/agentic/tools/mcp/manager.py +71 -0
  92. unique_toolkit/agentic/tools/mcp/models.py +28 -0
  93. unique_toolkit/agentic/tools/mcp/tool_wrapper.py +234 -0
  94. unique_toolkit/agentic/tools/openai_builtin/__init__.py +11 -0
  95. unique_toolkit/agentic/tools/openai_builtin/base.py +30 -0
  96. unique_toolkit/agentic/tools/openai_builtin/code_interpreter/__init__.py +8 -0
  97. unique_toolkit/agentic/tools/openai_builtin/code_interpreter/config.py +57 -0
  98. unique_toolkit/agentic/tools/openai_builtin/code_interpreter/service.py +230 -0
  99. unique_toolkit/agentic/tools/openai_builtin/manager.py +62 -0
  100. unique_toolkit/agentic/tools/schemas.py +141 -0
  101. unique_toolkit/agentic/tools/test/test_mcp_manager.py +536 -0
  102. unique_toolkit/agentic/tools/test/test_tool_progress_reporter.py +445 -0
  103. unique_toolkit/agentic/tools/tool.py +183 -0
  104. unique_toolkit/agentic/tools/tool_manager.py +523 -0
  105. unique_toolkit/agentic/tools/tool_progress_reporter.py +285 -0
  106. unique_toolkit/agentic/tools/utils/__init__.py +19 -0
  107. unique_toolkit/agentic/tools/utils/execution/__init__.py +1 -0
  108. unique_toolkit/agentic/tools/utils/execution/execution.py +286 -0
  109. unique_toolkit/agentic/tools/utils/source_handling/__init__.py +0 -0
  110. unique_toolkit/agentic/tools/utils/source_handling/schema.py +21 -0
  111. unique_toolkit/agentic/tools/utils/source_handling/source_formatting.py +207 -0
  112. unique_toolkit/agentic/tools/utils/source_handling/tests/test_source_formatting.py +216 -0
  113. unique_toolkit/app/__init__.py +6 -0
  114. unique_toolkit/app/dev_util.py +180 -0
  115. unique_toolkit/app/init_sdk.py +32 -1
  116. unique_toolkit/app/schemas.py +198 -31
  117. unique_toolkit/app/unique_settings.py +367 -0
  118. unique_toolkit/chat/__init__.py +8 -1
  119. unique_toolkit/chat/deprecated/service.py +232 -0
  120. unique_toolkit/chat/functions.py +642 -77
  121. unique_toolkit/chat/rendering.py +34 -0
  122. unique_toolkit/chat/responses_api.py +461 -0
  123. unique_toolkit/chat/schemas.py +133 -2
  124. unique_toolkit/chat/service.py +115 -767
  125. unique_toolkit/content/functions.py +153 -4
  126. unique_toolkit/content/schemas.py +122 -15
  127. unique_toolkit/content/service.py +278 -44
  128. unique_toolkit/content/smart_rules.py +301 -0
  129. unique_toolkit/content/utils.py +8 -3
  130. unique_toolkit/embedding/service.py +102 -11
  131. unique_toolkit/framework_utilities/__init__.py +1 -0
  132. unique_toolkit/framework_utilities/langchain/client.py +71 -0
  133. unique_toolkit/framework_utilities/langchain/history.py +19 -0
  134. unique_toolkit/framework_utilities/openai/__init__.py +6 -0
  135. unique_toolkit/framework_utilities/openai/client.py +83 -0
  136. unique_toolkit/framework_utilities/openai/message_builder.py +229 -0
  137. unique_toolkit/framework_utilities/utils.py +23 -0
  138. unique_toolkit/language_model/__init__.py +3 -0
  139. unique_toolkit/language_model/builder.py +27 -11
  140. unique_toolkit/language_model/default_language_model.py +3 -0
  141. unique_toolkit/language_model/functions.py +327 -43
  142. unique_toolkit/language_model/infos.py +992 -50
  143. unique_toolkit/language_model/reference.py +242 -0
  144. unique_toolkit/language_model/schemas.py +475 -48
  145. unique_toolkit/language_model/service.py +228 -27
  146. unique_toolkit/protocols/support.py +145 -0
  147. unique_toolkit/services/__init__.py +7 -0
  148. unique_toolkit/services/chat_service.py +1630 -0
  149. unique_toolkit/services/knowledge_base.py +861 -0
  150. unique_toolkit/short_term_memory/service.py +178 -41
  151. unique_toolkit/smart_rules/__init__.py +0 -0
  152. unique_toolkit/smart_rules/compile.py +56 -0
  153. unique_toolkit/test_utilities/events.py +197 -0
  154. {unique_toolkit-0.7.7.dist-info → unique_toolkit-1.23.0.dist-info}/METADATA +606 -7
  155. unique_toolkit-1.23.0.dist-info/RECORD +182 -0
  156. unique_toolkit/evaluators/__init__.py +0 -1
  157. unique_toolkit/evaluators/config.py +0 -35
  158. unique_toolkit/evaluators/constants.py +0 -1
  159. unique_toolkit/evaluators/context_relevancy/constants.py +0 -32
  160. unique_toolkit/evaluators/context_relevancy/service.py +0 -53
  161. unique_toolkit/evaluators/context_relevancy/utils.py +0 -142
  162. unique_toolkit/evaluators/hallucination/constants.py +0 -41
  163. unique_toolkit-0.7.7.dist-info/RECORD +0 -64
  164. /unique_toolkit/{evaluators → agentic/evaluation}/exception.py +0 -0
  165. {unique_toolkit-0.7.7.dist-info → unique_toolkit-1.23.0.dist-info}/LICENSE +0 -0
  166. {unique_toolkit-0.7.7.dist-info → unique_toolkit-1.23.0.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,185 @@
1
+ import re
2
+ from typing import Literal
3
+
4
+ from unique_toolkit.agentic.tools.a2a.postprocessing.config import (
5
+ SubAgentDisplayConfig,
6
+ SubAgentResponseDisplayMode,
7
+ )
8
+
9
+
10
+ def _wrap_text(text: str, start_text: str, end_text: str) -> str:
11
+ text = text.strip()
12
+ start_text = start_text.strip()
13
+ end_text = end_text.strip()
14
+
15
+ if start_text != "":
16
+ start_text = f"{start_text}\n"
17
+
18
+ if end_text != "":
19
+ end_text = f"\n{end_text}"
20
+
21
+ return f"{start_text}{text}{end_text}"
22
+
23
+
24
+ def _join_text_blocks(*blocks: str, sep: str = "\n") -> str:
25
+ return sep.join(block.strip() for block in blocks)
26
+
27
+
28
+ def _wrap_with_details_tag(
29
+ text, mode: Literal["open", "closed"], summary_name: str | None = None
30
+ ) -> str:
31
+ if summary_name is not None:
32
+ summary_tag = _wrap_text(summary_name, "<summary>", "</summary>")
33
+ text = _join_text_blocks(summary_tag, text)
34
+
35
+ if mode == "open":
36
+ text = _wrap_text(text, "<details open>", "</details>")
37
+ else:
38
+ text = _wrap_text(text, "<details>", "</details>")
39
+
40
+ return text
41
+
42
+
43
+ _BLOCK_BORDER_STYLE = (
44
+ "overflow-y: auto; border: 1px solid #ccc; padding: 8px; margin-top: 8px;"
45
+ )
46
+
47
+
48
+ def _wrap_with_block_border(text: str) -> str:
49
+ return _wrap_text(text, f"<div style='{_BLOCK_BORDER_STYLE}'>", "</div>")
50
+
51
+
52
+ _QUOTE_BORDER_STYLE = (
53
+ "margin-left: 20px; border-left: 2px solid #ccc; padding-left: 10px;"
54
+ )
55
+
56
+
57
+ def _wrap_with_quote_border(text: str) -> str:
58
+ return _wrap_text(text, f"<div style='{_QUOTE_BORDER_STYLE}'>", "</div>")
59
+
60
+
61
+ def _wrap_strong(text: str) -> str:
62
+ return _wrap_text(text, "<strong>", "</strong>")
63
+
64
+
65
+ def _wrap_hidden_div(text: str) -> str:
66
+ return _wrap_text(text, '<div style="display: none;">', "</div>")
67
+
68
+
69
+ def _add_line_break(text: str, before: bool = True, after: bool = True) -> str:
70
+ start_tag = ""
71
+ if before:
72
+ start_tag = "<br>"
73
+
74
+ end_tag = ""
75
+ if after:
76
+ end_tag = "<br>"
77
+
78
+ return _wrap_text(text, start_tag, end_tag)
79
+
80
+
81
+ def _prepare_title_template(
82
+ display_title_template: str, display_name_placeholder: str
83
+ ) -> str:
84
+ return display_title_template.replace("{}", "{%s}" % display_name_placeholder)
85
+
86
+
87
+ def _get_display_template(
88
+ mode: SubAgentResponseDisplayMode,
89
+ add_quote_border: bool,
90
+ add_block_border: bool,
91
+ display_title_template: str,
92
+ answer_placeholder: str = "answer",
93
+ assistant_id_placeholder: str = "assistant_id",
94
+ display_name_placeholder: str = "display_name",
95
+ ) -> str:
96
+ if mode == SubAgentResponseDisplayMode.HIDDEN:
97
+ return ""
98
+
99
+ assistant_id_placeholder = _wrap_hidden_div("{%s}" % assistant_id_placeholder)
100
+ title_template = _prepare_title_template(
101
+ display_title_template, display_name_placeholder
102
+ )
103
+ template = _join_text_blocks(
104
+ assistant_id_placeholder, "{%s}" % answer_placeholder, sep="\n\n"
105
+ ) # Double line break is needed for markdown formatting
106
+
107
+ template = _add_line_break(template, before=True, after=False)
108
+
109
+ if add_quote_border:
110
+ template = _wrap_with_quote_border(template)
111
+
112
+ match mode:
113
+ case SubAgentResponseDisplayMode.DETAILS_OPEN:
114
+ template = _wrap_with_details_tag(
115
+ template,
116
+ "open",
117
+ title_template,
118
+ )
119
+ case SubAgentResponseDisplayMode.DETAILS_CLOSED:
120
+ template = _wrap_with_details_tag(template, "closed", title_template)
121
+ case SubAgentResponseDisplayMode.PLAIN:
122
+ # Add a hidden block border to seperate sub agent answers from the rest of the text.
123
+ hidden_block_border = _wrap_hidden_div("sub_agent_answer_block")
124
+ template = _join_text_blocks(title_template, template, hidden_block_border)
125
+
126
+ if add_block_border:
127
+ template = _wrap_with_block_border(template)
128
+
129
+ return template
130
+
131
+
132
+ def _get_display_removal_re(
133
+ assistant_id: str,
134
+ mode: SubAgentResponseDisplayMode,
135
+ add_quote_border: bool,
136
+ add_block_border: bool,
137
+ display_title_template: str,
138
+ ) -> re.Pattern[str]:
139
+ template = _get_display_template(
140
+ mode=mode,
141
+ add_quote_border=add_quote_border,
142
+ add_block_border=add_block_border,
143
+ display_title_template=display_title_template,
144
+ )
145
+
146
+ pattern = template.format(
147
+ assistant_id=re.escape(assistant_id), answer=r"(.*?)", display_name=r"(.*?)"
148
+ )
149
+
150
+ return re.compile(pattern, flags=re.DOTALL)
151
+
152
+
153
+ def get_sub_agent_answer_display(
154
+ display_name: str,
155
+ display_config: SubAgentDisplayConfig,
156
+ answer: str,
157
+ assistant_id: str,
158
+ ) -> str:
159
+ template = _get_display_template(
160
+ mode=display_config.mode,
161
+ add_quote_border=display_config.add_quote_border,
162
+ add_block_border=display_config.add_block_border,
163
+ display_title_template=display_config.display_title_template,
164
+ )
165
+ return template.format(
166
+ display_name=display_name, answer=answer, assistant_id=assistant_id
167
+ )
168
+
169
+
170
+ def remove_sub_agent_answer_from_text(
171
+ display_config: SubAgentDisplayConfig,
172
+ text: str,
173
+ assistant_id: str,
174
+ ) -> str:
175
+ if not display_config.remove_from_history:
176
+ return text
177
+
178
+ pattern = _get_display_removal_re(
179
+ assistant_id=assistant_id,
180
+ mode=display_config.mode,
181
+ add_quote_border=display_config.add_quote_border,
182
+ add_block_border=display_config.add_block_border,
183
+ display_title_template=display_config.display_title_template,
184
+ )
185
+ return re.sub(pattern, "", text)
@@ -0,0 +1,73 @@
1
+ from typing import Callable, Iterable, Mapping, Sequence
2
+
3
+ from unique_toolkit._common.referencing import get_reference_pattern
4
+ from unique_toolkit._common.string_utilities import replace_in_text
5
+ from unique_toolkit.content import ContentReference
6
+
7
+ SourceId = str
8
+ SequenceNumber = int
9
+
10
+
11
+ def _add_source_ids(
12
+ existing_refs: Mapping[SourceId, SequenceNumber],
13
+ new_refs: Iterable[SourceId],
14
+ ) -> dict[SourceId, SequenceNumber]:
15
+ next_seq_num = max(existing_refs.values(), default=0) + 1
16
+ new_seq_nums: dict[SourceId, SequenceNumber] = {}
17
+
18
+ for source_id in new_refs:
19
+ seq_num = existing_refs.get(source_id, None) or new_seq_nums.get(
20
+ source_id, None
21
+ )
22
+ if seq_num is None:
23
+ new_seq_nums[source_id] = next_seq_num
24
+ next_seq_num += 1
25
+
26
+ return new_seq_nums
27
+
28
+
29
+ def add_content_refs(
30
+ message_refs: Sequence[ContentReference],
31
+ new_refs: Sequence[ContentReference],
32
+ ) -> list[ContentReference]:
33
+ message_refs = list(message_refs)
34
+
35
+ if len(new_refs) == 0:
36
+ return message_refs
37
+
38
+ existing_refs = {ref.source_id: ref.sequence_number for ref in message_refs}
39
+ new_refs_by_source_id = {
40
+ ref.source_id: ref for ref in sorted(new_refs, key=lambda x: x.sequence_number)
41
+ }
42
+ new_seq_nums = _add_source_ids(existing_refs, new_refs_by_source_id.keys())
43
+
44
+ for source_id, seq_num in new_seq_nums.items():
45
+ ref = new_refs_by_source_id[source_id]
46
+ message_refs.append(
47
+ ref.model_copy(update={"sequence_number": seq_num}, deep=True)
48
+ )
49
+
50
+ return message_refs
51
+
52
+
53
+ def add_content_refs_and_replace_in_text(
54
+ message_text: str,
55
+ message_refs: Sequence[ContentReference],
56
+ new_refs: Sequence[ContentReference],
57
+ ref_pattern_f: Callable[[int], str] = get_reference_pattern,
58
+ ref_replacement_f: Callable[[int], str] = get_reference_pattern,
59
+ ) -> tuple[str, list[ContentReference]]:
60
+ if len(new_refs) == 0:
61
+ return message_text, list(message_refs)
62
+
63
+ references = add_content_refs(message_refs, new_refs)
64
+ seq_num_for_source_id = {ref.source_id: ref.sequence_number for ref in references}
65
+ ref_map = []
66
+
67
+ for ref in new_refs:
68
+ old_seq_num = ref.sequence_number
69
+ new_seq_num = seq_num_for_source_id[ref.source_id]
70
+
71
+ ref_map.append((ref_pattern_f(old_seq_num), ref_replacement_f(new_seq_num)))
72
+
73
+ return replace_in_text(message_text, ref_map), references
@@ -0,0 +1,45 @@
1
+ from enum import StrEnum
2
+ from typing import Literal
3
+
4
+ from pydantic import BaseModel, Field
5
+
6
+ from unique_toolkit._common.pydantic_helpers import get_configuration_dict
7
+
8
+
9
+ class SubAgentResponseDisplayMode(StrEnum):
10
+ HIDDEN = "hidden"
11
+ DETAILS_OPEN = "details_open"
12
+ DETAILS_CLOSED = "details_closed"
13
+ PLAIN = "plain"
14
+
15
+
16
+ class SubAgentDisplayConfig(BaseModel):
17
+ model_config = get_configuration_dict()
18
+
19
+ mode: SubAgentResponseDisplayMode = Field(
20
+ default=SubAgentResponseDisplayMode.HIDDEN,
21
+ description="Controls how to display the sub agent response.",
22
+ )
23
+ remove_from_history: bool = Field(
24
+ default=True,
25
+ description="If set, sub agent responses will be removed from the history on subsequent calls to the assistant.",
26
+ )
27
+ add_quote_border: bool = Field(
28
+ default=True,
29
+ description="If set, a quote border is added to the left of the sub agent response.",
30
+ )
31
+ add_block_border: bool = Field(
32
+ default=False,
33
+ description="If set, a block border is added around the sub agent response.",
34
+ )
35
+ display_title_template: str = Field(
36
+ default="Answer from <strong>{}</strong>",
37
+ description=(
38
+ "The template to use for the display title of the sub agent response."
39
+ "If a placeholder '{}' is present, it will be replaced with the display name of the sub agent."
40
+ ),
41
+ )
42
+ position: Literal["before", "after"] = Field(
43
+ default="before",
44
+ description="The position of the sub agent response in the main agent response.",
45
+ )