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.
- unique_toolkit/__init__.py +28 -1
- unique_toolkit/_common/api_calling/human_verification_manager.py +343 -0
- unique_toolkit/_common/base_model_type_attribute.py +303 -0
- unique_toolkit/_common/chunk_relevancy_sorter/config.py +49 -0
- unique_toolkit/_common/chunk_relevancy_sorter/exception.py +5 -0
- unique_toolkit/_common/chunk_relevancy_sorter/schemas.py +46 -0
- unique_toolkit/_common/chunk_relevancy_sorter/service.py +374 -0
- unique_toolkit/_common/chunk_relevancy_sorter/tests/test_service.py +275 -0
- unique_toolkit/_common/default_language_model.py +12 -0
- unique_toolkit/_common/docx_generator/__init__.py +7 -0
- unique_toolkit/_common/docx_generator/config.py +12 -0
- unique_toolkit/_common/docx_generator/schemas.py +80 -0
- unique_toolkit/_common/docx_generator/service.py +252 -0
- unique_toolkit/_common/docx_generator/template/Doc Template.docx +0 -0
- unique_toolkit/_common/endpoint_builder.py +305 -0
- unique_toolkit/_common/endpoint_requestor.py +430 -0
- unique_toolkit/_common/exception.py +24 -0
- unique_toolkit/_common/feature_flags/schema.py +9 -0
- unique_toolkit/_common/pydantic/rjsf_tags.py +936 -0
- unique_toolkit/_common/pydantic_helpers.py +154 -0
- unique_toolkit/_common/referencing.py +53 -0
- unique_toolkit/_common/string_utilities.py +140 -0
- unique_toolkit/_common/tests/test_referencing.py +521 -0
- unique_toolkit/_common/tests/test_string_utilities.py +506 -0
- unique_toolkit/_common/token/image_token_counting.py +67 -0
- unique_toolkit/_common/token/token_counting.py +204 -0
- unique_toolkit/_common/utils/__init__.py +1 -0
- unique_toolkit/_common/utils/files.py +43 -0
- unique_toolkit/_common/utils/structured_output/__init__.py +1 -0
- unique_toolkit/_common/utils/structured_output/schema.py +5 -0
- unique_toolkit/_common/utils/write_configuration.py +51 -0
- unique_toolkit/_common/validators.py +101 -4
- unique_toolkit/agentic/__init__.py +1 -0
- unique_toolkit/agentic/debug_info_manager/debug_info_manager.py +28 -0
- unique_toolkit/agentic/debug_info_manager/test/test_debug_info_manager.py +278 -0
- unique_toolkit/agentic/evaluation/config.py +36 -0
- unique_toolkit/{evaluators → agentic/evaluation}/context_relevancy/prompts.py +25 -0
- unique_toolkit/agentic/evaluation/context_relevancy/schema.py +80 -0
- unique_toolkit/agentic/evaluation/context_relevancy/service.py +273 -0
- unique_toolkit/agentic/evaluation/evaluation_manager.py +218 -0
- unique_toolkit/agentic/evaluation/hallucination/constants.py +61 -0
- unique_toolkit/agentic/evaluation/hallucination/hallucination_evaluation.py +111 -0
- unique_toolkit/{evaluators → agentic/evaluation}/hallucination/prompts.py +1 -1
- unique_toolkit/{evaluators → agentic/evaluation}/hallucination/service.py +16 -15
- unique_toolkit/{evaluators → agentic/evaluation}/hallucination/utils.py +30 -20
- unique_toolkit/{evaluators → agentic/evaluation}/output_parser.py +20 -2
- unique_toolkit/{evaluators → agentic/evaluation}/schemas.py +27 -7
- unique_toolkit/agentic/evaluation/tests/test_context_relevancy_service.py +253 -0
- unique_toolkit/agentic/evaluation/tests/test_output_parser.py +87 -0
- unique_toolkit/agentic/history_manager/history_construction_with_contents.py +297 -0
- unique_toolkit/agentic/history_manager/history_manager.py +242 -0
- unique_toolkit/agentic/history_manager/loop_token_reducer.py +484 -0
- unique_toolkit/agentic/history_manager/utils.py +96 -0
- unique_toolkit/agentic/postprocessor/postprocessor_manager.py +212 -0
- unique_toolkit/agentic/reference_manager/reference_manager.py +103 -0
- unique_toolkit/agentic/responses_api/__init__.py +19 -0
- unique_toolkit/agentic/responses_api/postprocessors/code_display.py +63 -0
- unique_toolkit/agentic/responses_api/postprocessors/generated_files.py +145 -0
- unique_toolkit/agentic/responses_api/stream_handler.py +15 -0
- unique_toolkit/agentic/short_term_memory_manager/persistent_short_term_memory_manager.py +141 -0
- unique_toolkit/agentic/thinking_manager/thinking_manager.py +103 -0
- unique_toolkit/agentic/tools/__init__.py +1 -0
- unique_toolkit/agentic/tools/a2a/__init__.py +36 -0
- unique_toolkit/agentic/tools/a2a/config.py +17 -0
- unique_toolkit/agentic/tools/a2a/evaluation/__init__.py +15 -0
- unique_toolkit/agentic/tools/a2a/evaluation/_utils.py +66 -0
- unique_toolkit/agentic/tools/a2a/evaluation/config.py +55 -0
- unique_toolkit/agentic/tools/a2a/evaluation/evaluator.py +260 -0
- unique_toolkit/agentic/tools/a2a/evaluation/summarization_user_message.j2 +9 -0
- unique_toolkit/agentic/tools/a2a/manager.py +55 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/__init__.py +21 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/_display_utils.py +185 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/_ref_utils.py +73 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/config.py +45 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/display.py +180 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/references.py +101 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display_utils.py +1335 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/test/test_ref_utils.py +603 -0
- unique_toolkit/agentic/tools/a2a/prompts.py +46 -0
- unique_toolkit/agentic/tools/a2a/response_watcher/__init__.py +6 -0
- unique_toolkit/agentic/tools/a2a/response_watcher/service.py +91 -0
- unique_toolkit/agentic/tools/a2a/tool/__init__.py +4 -0
- unique_toolkit/agentic/tools/a2a/tool/_memory.py +26 -0
- unique_toolkit/agentic/tools/a2a/tool/_schema.py +9 -0
- unique_toolkit/agentic/tools/a2a/tool/config.py +73 -0
- unique_toolkit/agentic/tools/a2a/tool/service.py +306 -0
- unique_toolkit/agentic/tools/agent_chunks_hanlder.py +65 -0
- unique_toolkit/agentic/tools/config.py +167 -0
- unique_toolkit/agentic/tools/factory.py +44 -0
- unique_toolkit/agentic/tools/mcp/__init__.py +4 -0
- unique_toolkit/agentic/tools/mcp/manager.py +71 -0
- unique_toolkit/agentic/tools/mcp/models.py +28 -0
- unique_toolkit/agentic/tools/mcp/tool_wrapper.py +234 -0
- unique_toolkit/agentic/tools/openai_builtin/__init__.py +11 -0
- unique_toolkit/agentic/tools/openai_builtin/base.py +30 -0
- unique_toolkit/agentic/tools/openai_builtin/code_interpreter/__init__.py +8 -0
- unique_toolkit/agentic/tools/openai_builtin/code_interpreter/config.py +57 -0
- unique_toolkit/agentic/tools/openai_builtin/code_interpreter/service.py +230 -0
- unique_toolkit/agentic/tools/openai_builtin/manager.py +62 -0
- unique_toolkit/agentic/tools/schemas.py +141 -0
- unique_toolkit/agentic/tools/test/test_mcp_manager.py +536 -0
- unique_toolkit/agentic/tools/test/test_tool_progress_reporter.py +445 -0
- unique_toolkit/agentic/tools/tool.py +183 -0
- unique_toolkit/agentic/tools/tool_manager.py +523 -0
- unique_toolkit/agentic/tools/tool_progress_reporter.py +285 -0
- unique_toolkit/agentic/tools/utils/__init__.py +19 -0
- unique_toolkit/agentic/tools/utils/execution/__init__.py +1 -0
- unique_toolkit/agentic/tools/utils/execution/execution.py +286 -0
- unique_toolkit/agentic/tools/utils/source_handling/__init__.py +0 -0
- unique_toolkit/agentic/tools/utils/source_handling/schema.py +21 -0
- unique_toolkit/agentic/tools/utils/source_handling/source_formatting.py +207 -0
- unique_toolkit/agentic/tools/utils/source_handling/tests/test_source_formatting.py +216 -0
- unique_toolkit/app/__init__.py +6 -0
- unique_toolkit/app/dev_util.py +180 -0
- unique_toolkit/app/init_sdk.py +32 -1
- unique_toolkit/app/schemas.py +198 -31
- unique_toolkit/app/unique_settings.py +367 -0
- unique_toolkit/chat/__init__.py +8 -1
- unique_toolkit/chat/deprecated/service.py +232 -0
- unique_toolkit/chat/functions.py +642 -77
- unique_toolkit/chat/rendering.py +34 -0
- unique_toolkit/chat/responses_api.py +461 -0
- unique_toolkit/chat/schemas.py +133 -2
- unique_toolkit/chat/service.py +115 -767
- unique_toolkit/content/functions.py +153 -4
- unique_toolkit/content/schemas.py +122 -15
- unique_toolkit/content/service.py +278 -44
- unique_toolkit/content/smart_rules.py +301 -0
- unique_toolkit/content/utils.py +8 -3
- unique_toolkit/embedding/service.py +102 -11
- unique_toolkit/framework_utilities/__init__.py +1 -0
- unique_toolkit/framework_utilities/langchain/client.py +71 -0
- unique_toolkit/framework_utilities/langchain/history.py +19 -0
- unique_toolkit/framework_utilities/openai/__init__.py +6 -0
- unique_toolkit/framework_utilities/openai/client.py +83 -0
- unique_toolkit/framework_utilities/openai/message_builder.py +229 -0
- unique_toolkit/framework_utilities/utils.py +23 -0
- unique_toolkit/language_model/__init__.py +3 -0
- unique_toolkit/language_model/builder.py +27 -11
- unique_toolkit/language_model/default_language_model.py +3 -0
- unique_toolkit/language_model/functions.py +327 -43
- unique_toolkit/language_model/infos.py +992 -50
- unique_toolkit/language_model/reference.py +242 -0
- unique_toolkit/language_model/schemas.py +475 -48
- unique_toolkit/language_model/service.py +228 -27
- unique_toolkit/protocols/support.py +145 -0
- unique_toolkit/services/__init__.py +7 -0
- unique_toolkit/services/chat_service.py +1630 -0
- unique_toolkit/services/knowledge_base.py +861 -0
- unique_toolkit/short_term_memory/service.py +178 -41
- unique_toolkit/smart_rules/__init__.py +0 -0
- unique_toolkit/smart_rules/compile.py +56 -0
- unique_toolkit/test_utilities/events.py +197 -0
- {unique_toolkit-0.7.7.dist-info → unique_toolkit-1.23.0.dist-info}/METADATA +606 -7
- unique_toolkit-1.23.0.dist-info/RECORD +182 -0
- unique_toolkit/evaluators/__init__.py +0 -1
- unique_toolkit/evaluators/config.py +0 -35
- unique_toolkit/evaluators/constants.py +0 -1
- unique_toolkit/evaluators/context_relevancy/constants.py +0 -32
- unique_toolkit/evaluators/context_relevancy/service.py +0 -53
- unique_toolkit/evaluators/context_relevancy/utils.py +0 -142
- unique_toolkit/evaluators/hallucination/constants.py +0 -41
- unique_toolkit-0.7.7.dist-info/RECORD +0 -64
- /unique_toolkit/{evaluators → agentic/evaluation}/exception.py +0 -0
- {unique_toolkit-0.7.7.dist-info → unique_toolkit-1.23.0.dist-info}/LICENSE +0 -0
- {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
|
+
)
|