unique_toolkit 1.1.8__py3-none-any.whl → 1.2.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.
- unique_toolkit/agentic/postprocessor/postprocessor_manager.py +3 -2
- unique_toolkit/agentic/tools/a2a/config.py +20 -2
- unique_toolkit/agentic/tools/a2a/manager.py +4 -4
- unique_toolkit/agentic/tools/a2a/postprocessing/__init__.py +5 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/display.py +113 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/postprocessor.py +204 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display.py +412 -0
- unique_toolkit/agentic/tools/a2a/service.py +94 -25
- unique_toolkit/agentic/tools/config.py +10 -1
- unique_toolkit/agentic/tools/tool_manager.py +7 -1
- unique_toolkit/language_model/schemas.py +4 -3
- {unique_toolkit-1.1.8.dist-info → unique_toolkit-1.2.0.dist-info}/METADATA +8 -1
- {unique_toolkit-1.1.8.dist-info → unique_toolkit-1.2.0.dist-info}/RECORD +15 -11
- {unique_toolkit-1.1.8.dist-info → unique_toolkit-1.2.0.dist-info}/LICENSE +0 -0
- {unique_toolkit-1.1.8.dist-info → unique_toolkit-1.2.0.dist-info}/WHEEL +0 -0
@@ -16,10 +16,10 @@ class Postprocessor(ABC):
|
|
16
16
|
def get_name(self) -> str:
|
17
17
|
return self.name
|
18
18
|
|
19
|
-
async def run(self, loop_response: LanguageModelStreamResponse) ->
|
19
|
+
async def run(self, loop_response: LanguageModelStreamResponse) -> None:
|
20
20
|
raise NotImplementedError("Subclasses must implement this method.")
|
21
21
|
|
22
|
-
|
22
|
+
def apply_postprocessing_to_response(
|
23
23
|
self, loop_response: LanguageModelStreamResponse
|
24
24
|
) -> bool:
|
25
25
|
raise NotImplementedError(
|
@@ -102,6 +102,7 @@ class PostprocessorManager:
|
|
102
102
|
self._chat_service.modify_assistant_message(
|
103
103
|
content=loop_response.message.text,
|
104
104
|
message_id=loop_response.message.id,
|
105
|
+
references=loop_response.message.references,
|
105
106
|
)
|
106
107
|
|
107
108
|
async def execute_postprocessors(
|
@@ -1,3 +1,7 @@
|
|
1
|
+
from enum import StrEnum
|
2
|
+
|
3
|
+
from pydantic import BaseModel
|
4
|
+
|
1
5
|
from unique_toolkit.agentic.tools.config import get_configuration_dict
|
2
6
|
from unique_toolkit.agentic.tools.schemas import BaseToolConfig
|
3
7
|
|
@@ -6,22 +10,36 @@ This is the message that will be sent to the sub-agent.
|
|
6
10
|
""".strip()
|
7
11
|
|
8
12
|
|
13
|
+
class ResponseDisplayMode(StrEnum):
|
14
|
+
HIDDEN = "hidden"
|
15
|
+
DETAILS_OPEN = "details_open"
|
16
|
+
DETAILS_CLOSED = "details_closed"
|
17
|
+
|
18
|
+
|
19
|
+
class SubAgentToolDisplayConfig(BaseModel):
|
20
|
+
model_config = get_configuration_dict()
|
21
|
+
|
22
|
+
mode: ResponseDisplayMode = ResponseDisplayMode.HIDDEN
|
23
|
+
remove_from_history: bool = True
|
24
|
+
|
25
|
+
|
9
26
|
class SubAgentToolConfig(BaseToolConfig):
|
10
27
|
model_config = get_configuration_dict()
|
11
28
|
|
12
|
-
name: str = "default_name"
|
13
29
|
assistant_id: str = ""
|
14
30
|
chat_id: str | None = None
|
15
31
|
reuse_chat: bool = True
|
32
|
+
|
16
33
|
tool_description_for_system_prompt: str = ""
|
17
34
|
tool_description: str = ""
|
18
35
|
param_description_sub_agent_user_message: str = (
|
19
36
|
DEFAULT_PARAM_DESCRIPTION_SUB_AGENT_USER_MESSAGE
|
20
37
|
)
|
21
38
|
tool_format_information_for_system_prompt: str = ""
|
22
|
-
|
23
39
|
tool_description_for_user_prompt: str = ""
|
24
40
|
tool_format_information_for_user_prompt: str = ""
|
25
41
|
|
26
42
|
poll_interval: float = 1.0
|
27
43
|
max_wait: float = 120.0
|
44
|
+
|
45
|
+
response_display_config: SubAgentToolDisplayConfig = SubAgentToolDisplayConfig()
|
@@ -3,8 +3,6 @@ from logging import Logger
|
|
3
3
|
from unique_toolkit.agentic.tools.a2a.config import SubAgentToolConfig
|
4
4
|
from unique_toolkit.agentic.tools.a2a.service import SubAgentTool, ToolProgressReporter
|
5
5
|
from unique_toolkit.agentic.tools.config import ToolBuildConfig
|
6
|
-
from unique_toolkit.agentic.tools.schemas import BaseToolConfig
|
7
|
-
from unique_toolkit.agentic.tools.tool import Tool
|
8
6
|
from unique_toolkit.app.schemas import ChatEvent
|
9
7
|
|
10
8
|
|
@@ -19,7 +17,7 @@ class A2AManager:
|
|
19
17
|
|
20
18
|
def get_all_sub_agents(
|
21
19
|
self, tool_configs: list[ToolBuildConfig], event: ChatEvent
|
22
|
-
) -> tuple[list[ToolBuildConfig], list[
|
20
|
+
) -> tuple[list[ToolBuildConfig], list[SubAgentTool]]:
|
23
21
|
sub_agents = []
|
24
22
|
|
25
23
|
for tool_config in tool_configs:
|
@@ -32,13 +30,15 @@ class A2AManager:
|
|
32
30
|
)
|
33
31
|
continue
|
34
32
|
|
35
|
-
sub_agent_tool_config
|
33
|
+
sub_agent_tool_config = tool_config.configuration
|
36
34
|
|
37
35
|
sub_agents.append(
|
38
36
|
SubAgentTool(
|
39
37
|
configuration=sub_agent_tool_config,
|
40
38
|
event=event,
|
41
39
|
tool_progress_reporter=self._tool_progress_reporter,
|
40
|
+
name=tool_config.name,
|
41
|
+
display_name=tool_config.display_name,
|
42
42
|
)
|
43
43
|
)
|
44
44
|
|
@@ -0,0 +1,113 @@
|
|
1
|
+
import re
|
2
|
+
from abc import ABC, abstractmethod
|
3
|
+
from typing import Literal, override
|
4
|
+
|
5
|
+
from unique_toolkit.agentic.tools.a2a.config import ResponseDisplayMode
|
6
|
+
|
7
|
+
|
8
|
+
class _ResponseDisplayHandler(ABC):
|
9
|
+
@abstractmethod
|
10
|
+
def build_response_display(
|
11
|
+
self, display_name: str, assistant_id: str, answer: str
|
12
|
+
) -> str:
|
13
|
+
raise NotImplementedError()
|
14
|
+
|
15
|
+
@abstractmethod
|
16
|
+
def remove_response_display(self, assistant_id: str, text: str) -> str:
|
17
|
+
raise NotImplementedError()
|
18
|
+
|
19
|
+
|
20
|
+
class _DetailsResponseDisplayHandler(_ResponseDisplayHandler):
|
21
|
+
def __init__(self, mode: Literal["open", "closed"]) -> None:
|
22
|
+
self._mode = mode
|
23
|
+
|
24
|
+
DETAILS_CLOSED_TEMPLATE = (
|
25
|
+
"<details><summary>{display_name}</summary>\n"
|
26
|
+
"\n"
|
27
|
+
'<div style="display: none;">{assistant_id}</div>\n'
|
28
|
+
"\n"
|
29
|
+
"{answer}\n"
|
30
|
+
"</details>\n"
|
31
|
+
"<br>\n"
|
32
|
+
"\n"
|
33
|
+
)
|
34
|
+
|
35
|
+
DETAILS_OPEN_TEMPLATE = (
|
36
|
+
"<details open><summary>{display_name}</summary>\n"
|
37
|
+
"\n"
|
38
|
+
'<div style="display: none;">{assistant_id}</div>\n'
|
39
|
+
"\n"
|
40
|
+
"{answer}\n"
|
41
|
+
"\n"
|
42
|
+
"</details>\n"
|
43
|
+
"<br>\n"
|
44
|
+
"\n"
|
45
|
+
)
|
46
|
+
|
47
|
+
def _get_detect_re(self, assistant_id: str) -> str:
|
48
|
+
if self._mode == "open":
|
49
|
+
return (
|
50
|
+
r"(?s)<details open>\s*"
|
51
|
+
r"<summary>(.*?)</summary>\s*"
|
52
|
+
rf"<div style=\"display: none;\">{re.escape(assistant_id)}</div>\s*"
|
53
|
+
r"(.*?)\s*"
|
54
|
+
r"</details>\s*"
|
55
|
+
r"<br>\s*"
|
56
|
+
)
|
57
|
+
else:
|
58
|
+
return (
|
59
|
+
r"(?s)<details>\s*"
|
60
|
+
r"<summary>(.*?)</summary>\s*"
|
61
|
+
rf"<div style=\"display: none;\">{re.escape(assistant_id)}</div>\s*"
|
62
|
+
r"(.*?)\s*"
|
63
|
+
r"</details>\s*"
|
64
|
+
r"<br>\s*"
|
65
|
+
)
|
66
|
+
|
67
|
+
def _get_template(self) -> str:
|
68
|
+
if self._mode == "open":
|
69
|
+
return self.DETAILS_OPEN_TEMPLATE
|
70
|
+
else:
|
71
|
+
return self.DETAILS_CLOSED_TEMPLATE
|
72
|
+
|
73
|
+
@override
|
74
|
+
def build_response_display(
|
75
|
+
self, display_name: str, assistant_id: str, answer: str
|
76
|
+
) -> str:
|
77
|
+
return self._get_template().format(
|
78
|
+
assistant_id=assistant_id, display_name=display_name, answer=answer
|
79
|
+
)
|
80
|
+
|
81
|
+
@override
|
82
|
+
def remove_response_display(self, assistant_id: str, text: str) -> str:
|
83
|
+
return re.sub(self._get_detect_re(assistant_id=assistant_id), "", text)
|
84
|
+
|
85
|
+
|
86
|
+
_DISPLAY_HANDLERS = {
|
87
|
+
ResponseDisplayMode.DETAILS_OPEN: _DetailsResponseDisplayHandler(mode="open"),
|
88
|
+
ResponseDisplayMode.DETAILS_CLOSED: _DetailsResponseDisplayHandler(mode="closed"),
|
89
|
+
}
|
90
|
+
|
91
|
+
|
92
|
+
def build_sub_agent_answer_display(
|
93
|
+
display_name: str, display_mode: ResponseDisplayMode, answer: str, assistant_id: str
|
94
|
+
) -> str:
|
95
|
+
if display_mode not in _DISPLAY_HANDLERS:
|
96
|
+
return ""
|
97
|
+
|
98
|
+
display_f = _DISPLAY_HANDLERS[display_mode]
|
99
|
+
|
100
|
+
return display_f.build_response_display(
|
101
|
+
display_name=display_name, answer=answer, assistant_id=assistant_id
|
102
|
+
)
|
103
|
+
|
104
|
+
|
105
|
+
def remove_sub_agent_answer_from_text(
|
106
|
+
display_mode: ResponseDisplayMode, text: str, assistant_id: str
|
107
|
+
) -> str:
|
108
|
+
if display_mode not in _DISPLAY_HANDLERS:
|
109
|
+
return text
|
110
|
+
|
111
|
+
display_f = _DISPLAY_HANDLERS[display_mode]
|
112
|
+
|
113
|
+
return display_f.remove_response_display(assistant_id=assistant_id, text=text)
|
@@ -0,0 +1,204 @@
|
|
1
|
+
import logging
|
2
|
+
import re
|
3
|
+
from typing import NotRequired, TypedDict, override
|
4
|
+
|
5
|
+
import unique_sdk
|
6
|
+
|
7
|
+
from unique_toolkit.agentic.postprocessor.postprocessor_manager import Postprocessor
|
8
|
+
from unique_toolkit.agentic.tools.a2a.config import (
|
9
|
+
ResponseDisplayMode,
|
10
|
+
SubAgentToolDisplayConfig,
|
11
|
+
)
|
12
|
+
from unique_toolkit.agentic.tools.a2a.postprocessing.display import (
|
13
|
+
build_sub_agent_answer_display,
|
14
|
+
remove_sub_agent_answer_from_text,
|
15
|
+
)
|
16
|
+
from unique_toolkit.agentic.tools.a2a.service import SubAgentTool
|
17
|
+
from unique_toolkit.content.schemas import ContentReference
|
18
|
+
from unique_toolkit.language_model.schemas import LanguageModelStreamResponse
|
19
|
+
|
20
|
+
logger = logging.getLogger(__name__)
|
21
|
+
|
22
|
+
SpaceMessage = unique_sdk.Space.Message
|
23
|
+
|
24
|
+
|
25
|
+
class _SubAgentMessageInfo(TypedDict):
|
26
|
+
text: str | None
|
27
|
+
references: list[unique_sdk.Space.Reference]
|
28
|
+
|
29
|
+
|
30
|
+
class _SubAgentToolInfo(TypedDict):
|
31
|
+
display_name: str
|
32
|
+
display_config: SubAgentToolDisplayConfig
|
33
|
+
response: NotRequired[_SubAgentMessageInfo]
|
34
|
+
|
35
|
+
|
36
|
+
class SubAgentResponsesPostprocessor(Postprocessor):
|
37
|
+
def __init__(
|
38
|
+
self,
|
39
|
+
user_id: str,
|
40
|
+
company_id: str,
|
41
|
+
agent_chat_id: str,
|
42
|
+
sub_agent_tools: list[SubAgentTool],
|
43
|
+
):
|
44
|
+
super().__init__(name=self.__class__.__name__)
|
45
|
+
|
46
|
+
self._user_id = user_id
|
47
|
+
self._company_id = company_id
|
48
|
+
|
49
|
+
self._agent_chat_id = agent_chat_id
|
50
|
+
|
51
|
+
self._assistant_id_to_tool_info: dict[str, _SubAgentToolInfo] = {}
|
52
|
+
|
53
|
+
for sub_agent_tool in sub_agent_tools:
|
54
|
+
sub_agent_tool.subscribe(self)
|
55
|
+
|
56
|
+
self._assistant_id_to_tool_info[sub_agent_tool.config.assistant_id] = (
|
57
|
+
_SubAgentToolInfo(
|
58
|
+
display_config=sub_agent_tool.config.response_display_config,
|
59
|
+
display_name=sub_agent_tool.display_name(),
|
60
|
+
)
|
61
|
+
)
|
62
|
+
|
63
|
+
self._sub_agent_message = None
|
64
|
+
|
65
|
+
@override
|
66
|
+
async def run(self, loop_response: LanguageModelStreamResponse) -> None:
|
67
|
+
self._sub_agent_message = await unique_sdk.Space.get_latest_message_async(
|
68
|
+
user_id=self._user_id,
|
69
|
+
company_id=self._company_id,
|
70
|
+
chat_id=self._agent_chat_id,
|
71
|
+
)
|
72
|
+
|
73
|
+
@override
|
74
|
+
def apply_postprocessing_to_response(
|
75
|
+
self, loop_response: LanguageModelStreamResponse
|
76
|
+
) -> bool:
|
77
|
+
logger.info("Adding sub agent responses to the response")
|
78
|
+
|
79
|
+
# Get responses to display
|
80
|
+
displayed = {}
|
81
|
+
for assistant_id, tool_info in self._assistant_id_to_tool_info.items():
|
82
|
+
display_mode = tool_info["display_config"].mode
|
83
|
+
|
84
|
+
if "response" not in tool_info:
|
85
|
+
logger.warning(
|
86
|
+
"No response from assistant %s",
|
87
|
+
assistant_id,
|
88
|
+
)
|
89
|
+
continue
|
90
|
+
|
91
|
+
if display_mode != ResponseDisplayMode.HIDDEN:
|
92
|
+
displayed[assistant_id] = tool_info["response"]
|
93
|
+
|
94
|
+
existing_refs = {
|
95
|
+
ref.source_id: ref.sequence_number
|
96
|
+
for ref in loop_response.message.references
|
97
|
+
}
|
98
|
+
_consolidate_references_in_place(displayed, existing_refs)
|
99
|
+
|
100
|
+
modified = len(displayed) > 0
|
101
|
+
for assistant_id, message in reversed(displayed.items()):
|
102
|
+
tool_info = self._assistant_id_to_tool_info[assistant_id]
|
103
|
+
display_mode = tool_info["display_config"].mode
|
104
|
+
display_name = tool_info["display_name"]
|
105
|
+
loop_response.message.text = (
|
106
|
+
build_sub_agent_answer_display(
|
107
|
+
display_name=display_name,
|
108
|
+
assistant_id=assistant_id,
|
109
|
+
display_mode=display_mode,
|
110
|
+
answer=message["text"],
|
111
|
+
)
|
112
|
+
+ loop_response.message.text
|
113
|
+
)
|
114
|
+
|
115
|
+
assert self._sub_agent_message is not None
|
116
|
+
|
117
|
+
loop_response.message.references.extend(
|
118
|
+
ContentReference(
|
119
|
+
message_id=self._sub_agent_message["id"],
|
120
|
+
source_id=ref["sourceId"],
|
121
|
+
url=ref["url"],
|
122
|
+
source=ref["source"],
|
123
|
+
name=ref["name"],
|
124
|
+
sequence_number=ref["sequenceNumber"],
|
125
|
+
)
|
126
|
+
for ref in message["references"]
|
127
|
+
)
|
128
|
+
|
129
|
+
return modified
|
130
|
+
|
131
|
+
@override
|
132
|
+
async def remove_from_text(self, text) -> str:
|
133
|
+
for assistant_id, tool_info in self._assistant_id_to_tool_info.items():
|
134
|
+
display_config = tool_info["display_config"]
|
135
|
+
if display_config.remove_from_history:
|
136
|
+
text = remove_sub_agent_answer_from_text(
|
137
|
+
display_mode=display_config.mode,
|
138
|
+
text=text,
|
139
|
+
assistant_id=assistant_id,
|
140
|
+
)
|
141
|
+
return text
|
142
|
+
|
143
|
+
def notify_sub_agent_response(
|
144
|
+
self, sub_agent_assistant_id: str, response: SpaceMessage
|
145
|
+
) -> None:
|
146
|
+
if sub_agent_assistant_id not in self._assistant_id_to_tool_info:
|
147
|
+
logger.warning(
|
148
|
+
"Unknown assistant id %s received, message will be ignored.",
|
149
|
+
sub_agent_assistant_id,
|
150
|
+
)
|
151
|
+
return
|
152
|
+
|
153
|
+
self._assistant_id_to_tool_info[sub_agent_assistant_id]["response"] = {
|
154
|
+
"text": response["text"],
|
155
|
+
"references": [
|
156
|
+
{
|
157
|
+
"name": ref["name"],
|
158
|
+
"url": ref["url"],
|
159
|
+
"sequenceNumber": ref["sequenceNumber"],
|
160
|
+
"originalIndex": [],
|
161
|
+
"sourceId": ref["sourceId"],
|
162
|
+
"source": ref["source"],
|
163
|
+
}
|
164
|
+
for ref in response["references"] or []
|
165
|
+
],
|
166
|
+
}
|
167
|
+
|
168
|
+
|
169
|
+
def _consolidate_references_in_place(
|
170
|
+
messages: dict[str, _SubAgentMessageInfo], existing_refs: dict[str, int]
|
171
|
+
) -> None:
|
172
|
+
start_index = max(existing_refs.values(), default=0) + 1
|
173
|
+
|
174
|
+
for assistant_id, message in messages.items():
|
175
|
+
references = message["references"]
|
176
|
+
if len(references) == 0 or message["text"] is None:
|
177
|
+
logger.info(
|
178
|
+
"Message from assistant %s does not contain any references",
|
179
|
+
assistant_id,
|
180
|
+
)
|
181
|
+
continue
|
182
|
+
|
183
|
+
references = list(sorted(references, key=lambda ref: ref["sequenceNumber"]))
|
184
|
+
|
185
|
+
message_new_refs = []
|
186
|
+
for reference in references:
|
187
|
+
source_id = reference["sourceId"]
|
188
|
+
|
189
|
+
if source_id not in existing_refs:
|
190
|
+
message_new_refs.append(reference)
|
191
|
+
existing_refs[source_id] = start_index
|
192
|
+
start_index += 1
|
193
|
+
|
194
|
+
reference_num = existing_refs[source_id]
|
195
|
+
|
196
|
+
seq_num = reference["sequenceNumber"]
|
197
|
+
message["text"] = re.sub(
|
198
|
+
rf"<sup>{seq_num}</sup>",
|
199
|
+
f"<sup>{reference_num}</sup>",
|
200
|
+
message["text"],
|
201
|
+
)
|
202
|
+
reference["sequenceNumber"] = reference_num
|
203
|
+
|
204
|
+
message["references"] = message_new_refs
|
@@ -0,0 +1,412 @@
|
|
1
|
+
import re
|
2
|
+
|
3
|
+
import pytest
|
4
|
+
|
5
|
+
from unique_toolkit.agentic.tools.a2a.config import ResponseDisplayMode
|
6
|
+
from unique_toolkit.agentic.tools.a2a.postprocessing.display import (
|
7
|
+
_DetailsResponseDisplayHandler,
|
8
|
+
build_sub_agent_answer_display,
|
9
|
+
remove_sub_agent_answer_from_text,
|
10
|
+
)
|
11
|
+
|
12
|
+
|
13
|
+
class TestDetailsResponseDisplayHandler:
|
14
|
+
"""Test suite for DetailsResponseDisplayHandler class."""
|
15
|
+
|
16
|
+
@pytest.fixture
|
17
|
+
def open_handler(self):
|
18
|
+
"""Create a handler with open mode."""
|
19
|
+
return _DetailsResponseDisplayHandler(mode="open")
|
20
|
+
|
21
|
+
@pytest.fixture
|
22
|
+
def closed_handler(self):
|
23
|
+
"""Create a handler with closed mode."""
|
24
|
+
return _DetailsResponseDisplayHandler(mode="closed")
|
25
|
+
|
26
|
+
@pytest.fixture
|
27
|
+
def sample_data(self):
|
28
|
+
"""Sample data for testing."""
|
29
|
+
return {
|
30
|
+
"display_name": "Test Assistant",
|
31
|
+
"assistant_id": "test_assistant_123",
|
32
|
+
"answer": "This is a test answer with multiple lines.\nSecond line here.",
|
33
|
+
}
|
34
|
+
|
35
|
+
def test_build_response_display_open_mode(self, open_handler, sample_data):
|
36
|
+
"""Test building response display in open mode."""
|
37
|
+
result = open_handler.build_response_display(
|
38
|
+
display_name=sample_data["display_name"],
|
39
|
+
assistant_id=sample_data["assistant_id"],
|
40
|
+
answer=sample_data["answer"],
|
41
|
+
)
|
42
|
+
|
43
|
+
assert "<details open>" in result
|
44
|
+
assert (
|
45
|
+
f'<div style="display: none;">{sample_data["assistant_id"]}</div>' in result
|
46
|
+
)
|
47
|
+
assert f"<summary>{sample_data['display_name']}</summary>" in result
|
48
|
+
assert sample_data["answer"] in result
|
49
|
+
assert "</details>" in result
|
50
|
+
|
51
|
+
def test_build_response_display_closed_mode(self, closed_handler, sample_data):
|
52
|
+
"""Test building response display in closed mode."""
|
53
|
+
result = closed_handler.build_response_display(
|
54
|
+
display_name=sample_data["display_name"],
|
55
|
+
assistant_id=sample_data["assistant_id"],
|
56
|
+
answer=sample_data["answer"],
|
57
|
+
)
|
58
|
+
|
59
|
+
assert "<details>" in result
|
60
|
+
assert "<details open>" not in result
|
61
|
+
assert (
|
62
|
+
f'<div style="display: none;">{sample_data["assistant_id"]}</div>' in result
|
63
|
+
)
|
64
|
+
assert f"<summary>{sample_data['display_name']}</summary>" in result
|
65
|
+
assert sample_data["answer"] in result
|
66
|
+
assert "</details>" in result
|
67
|
+
|
68
|
+
def test_build_response_display_with_special_characters(self, open_handler):
|
69
|
+
"""Test building response display with special characters in content."""
|
70
|
+
result = open_handler.build_response_display(
|
71
|
+
display_name="Test & Co.",
|
72
|
+
assistant_id="test<>123",
|
73
|
+
answer="Answer with <tags> & symbols",
|
74
|
+
)
|
75
|
+
|
76
|
+
assert "Test & Co." in result
|
77
|
+
assert "test<>123" in result
|
78
|
+
assert "Answer with <tags> & symbols" in result
|
79
|
+
|
80
|
+
def test_remove_response_display_open_mode(self, open_handler, sample_data):
|
81
|
+
"""Test removing response display from text in open mode."""
|
82
|
+
# First build the display
|
83
|
+
display_html = open_handler.build_response_display(
|
84
|
+
display_name=sample_data["display_name"],
|
85
|
+
assistant_id=sample_data["assistant_id"],
|
86
|
+
answer=sample_data["answer"],
|
87
|
+
)
|
88
|
+
|
89
|
+
# Create text with the display embedded
|
90
|
+
text_with_display = f"Some text before\n{display_html}\nSome text after"
|
91
|
+
|
92
|
+
# Remove the display
|
93
|
+
result = open_handler.remove_response_display(
|
94
|
+
assistant_id=sample_data["assistant_id"], text=text_with_display
|
95
|
+
)
|
96
|
+
|
97
|
+
assert "Some text before" in result
|
98
|
+
assert "Some text after" in result
|
99
|
+
assert sample_data["display_name"] not in result
|
100
|
+
assert sample_data["answer"] not in result
|
101
|
+
|
102
|
+
def test_remove_response_display_closed_mode(self, closed_handler, sample_data):
|
103
|
+
"""Test removing response display from text in closed mode."""
|
104
|
+
# First build the display
|
105
|
+
display_html = closed_handler.build_response_display(
|
106
|
+
display_name=sample_data["display_name"],
|
107
|
+
assistant_id=sample_data["assistant_id"],
|
108
|
+
answer=sample_data["answer"],
|
109
|
+
)
|
110
|
+
|
111
|
+
# Create text with the display embedded
|
112
|
+
text_with_display = f"Some text before\n{display_html}\nSome text after"
|
113
|
+
|
114
|
+
# Remove the display
|
115
|
+
result = closed_handler.remove_response_display(
|
116
|
+
assistant_id=sample_data["assistant_id"], text=text_with_display
|
117
|
+
)
|
118
|
+
|
119
|
+
assert "Some text before" in result
|
120
|
+
assert "Some text after" in result
|
121
|
+
assert sample_data["display_name"] not in result
|
122
|
+
assert sample_data["answer"] not in result
|
123
|
+
|
124
|
+
def test_remove_response_display_multiple_instances(self, open_handler):
|
125
|
+
"""Test removing multiple instances of response display."""
|
126
|
+
assistant_id = "test_123"
|
127
|
+
|
128
|
+
display1 = open_handler.build_response_display(
|
129
|
+
display_name="First", assistant_id=assistant_id, answer="First answer"
|
130
|
+
)
|
131
|
+
|
132
|
+
display2 = open_handler.build_response_display(
|
133
|
+
display_name="Second", assistant_id=assistant_id, answer="Second answer"
|
134
|
+
)
|
135
|
+
|
136
|
+
text_with_displays = f"Start\n{display1}\nMiddle\n{display2}\nEnd"
|
137
|
+
|
138
|
+
result = open_handler.remove_response_display(
|
139
|
+
assistant_id=assistant_id, text=text_with_displays
|
140
|
+
)
|
141
|
+
|
142
|
+
assert "Start" in result
|
143
|
+
assert "Middle" in result
|
144
|
+
assert "End" in result
|
145
|
+
assert "First answer" not in result
|
146
|
+
assert "Second answer" not in result
|
147
|
+
|
148
|
+
def test_remove_response_display_no_match(self, open_handler):
|
149
|
+
"""Test removing response display when no match exists."""
|
150
|
+
text = "This is some text without any displays"
|
151
|
+
result = open_handler.remove_response_display(
|
152
|
+
assistant_id="nonexistent", text=text
|
153
|
+
)
|
154
|
+
assert result == text
|
155
|
+
|
156
|
+
def test_remove_response_display_with_regex_special_chars(self, open_handler):
|
157
|
+
"""Test removing response display with regex special characters in assistant_id."""
|
158
|
+
assistant_id = "test.+*?[]{}()^$|"
|
159
|
+
|
160
|
+
display_html = open_handler.build_response_display(
|
161
|
+
display_name="Test", assistant_id=assistant_id, answer="Test answer"
|
162
|
+
)
|
163
|
+
|
164
|
+
text_with_display = f"Before\n{display_html}\nAfter"
|
165
|
+
|
166
|
+
result = open_handler.remove_response_display(
|
167
|
+
assistant_id=assistant_id, text=text_with_display
|
168
|
+
)
|
169
|
+
|
170
|
+
assert "Before" in result
|
171
|
+
assert "After" in result
|
172
|
+
assert "Test answer" not in result
|
173
|
+
|
174
|
+
def test_get_detect_re_pattern_validity(self, open_handler, closed_handler):
|
175
|
+
"""Test that the regex patterns are valid and compilable."""
|
176
|
+
assistant_id = "test_123"
|
177
|
+
|
178
|
+
open_pattern = open_handler._get_detect_re(assistant_id)
|
179
|
+
closed_pattern = closed_handler._get_detect_re(assistant_id)
|
180
|
+
|
181
|
+
# Should not raise exceptions
|
182
|
+
re.compile(open_pattern)
|
183
|
+
re.compile(closed_pattern)
|
184
|
+
|
185
|
+
assert "(?s)" in open_pattern # multiline flag
|
186
|
+
assert "(?s)" in closed_pattern
|
187
|
+
assert "details open" in open_pattern
|
188
|
+
assert "details>" in closed_pattern
|
189
|
+
assert "details open" not in closed_pattern
|
190
|
+
|
191
|
+
|
192
|
+
class TestDisplayFunctions:
|
193
|
+
"""Test suite for module-level display functions."""
|
194
|
+
|
195
|
+
@pytest.fixture
|
196
|
+
def sample_data(self):
|
197
|
+
"""Sample data for testing."""
|
198
|
+
return {
|
199
|
+
"display_name": "Test Assistant",
|
200
|
+
"assistant_id": "test_assistant_123",
|
201
|
+
"answer": "This is a test answer.",
|
202
|
+
}
|
203
|
+
|
204
|
+
def test_build_sub_agent_answer_display_details_open(self, sample_data):
|
205
|
+
"""Test building sub-agent answer display with DETAILS_OPEN mode."""
|
206
|
+
result = build_sub_agent_answer_display(
|
207
|
+
display_name=sample_data["display_name"],
|
208
|
+
display_mode=ResponseDisplayMode.DETAILS_OPEN,
|
209
|
+
answer=sample_data["answer"],
|
210
|
+
assistant_id=sample_data["assistant_id"],
|
211
|
+
)
|
212
|
+
|
213
|
+
assert "<details open>" in result
|
214
|
+
assert sample_data["display_name"] in result
|
215
|
+
assert sample_data["answer"] in result
|
216
|
+
assert sample_data["assistant_id"] in result
|
217
|
+
|
218
|
+
def test_build_sub_agent_answer_display_details_closed(self, sample_data):
|
219
|
+
"""Test building sub-agent answer display with DETAILS_CLOSED mode."""
|
220
|
+
result = build_sub_agent_answer_display(
|
221
|
+
display_name=sample_data["display_name"],
|
222
|
+
display_mode=ResponseDisplayMode.DETAILS_CLOSED,
|
223
|
+
answer=sample_data["answer"],
|
224
|
+
assistant_id=sample_data["assistant_id"],
|
225
|
+
)
|
226
|
+
|
227
|
+
assert "<details>" in result
|
228
|
+
assert "<details open>" not in result
|
229
|
+
assert sample_data["display_name"] in result
|
230
|
+
assert sample_data["answer"] in result
|
231
|
+
assert sample_data["assistant_id"] in result
|
232
|
+
|
233
|
+
def test_build_sub_agent_answer_display_hidden_mode(self, sample_data):
|
234
|
+
"""Test building sub-agent answer display with HIDDEN mode."""
|
235
|
+
result = build_sub_agent_answer_display(
|
236
|
+
display_name=sample_data["display_name"],
|
237
|
+
display_mode=ResponseDisplayMode.HIDDEN,
|
238
|
+
answer=sample_data["answer"],
|
239
|
+
assistant_id=sample_data["assistant_id"],
|
240
|
+
)
|
241
|
+
|
242
|
+
assert result == ""
|
243
|
+
|
244
|
+
def test_remove_sub_agent_answer_from_text_details_open(self, sample_data):
|
245
|
+
"""Test removing sub-agent answer from text with DETAILS_OPEN mode."""
|
246
|
+
# First build the display
|
247
|
+
display_html = build_sub_agent_answer_display(
|
248
|
+
display_name=sample_data["display_name"],
|
249
|
+
display_mode=ResponseDisplayMode.DETAILS_OPEN,
|
250
|
+
answer=sample_data["answer"],
|
251
|
+
assistant_id=sample_data["assistant_id"],
|
252
|
+
)
|
253
|
+
|
254
|
+
text_with_display = f"Before\n{display_html}\nAfter"
|
255
|
+
|
256
|
+
result = remove_sub_agent_answer_from_text(
|
257
|
+
display_mode=ResponseDisplayMode.DETAILS_OPEN,
|
258
|
+
text=text_with_display,
|
259
|
+
assistant_id=sample_data["assistant_id"],
|
260
|
+
)
|
261
|
+
|
262
|
+
assert "Before" in result
|
263
|
+
assert "After" in result
|
264
|
+
assert sample_data["answer"] not in result
|
265
|
+
|
266
|
+
def test_remove_sub_agent_answer_from_text_details_closed(self, sample_data):
|
267
|
+
"""Test removing sub-agent answer from text with DETAILS_CLOSED mode."""
|
268
|
+
# First build the display
|
269
|
+
display_html = build_sub_agent_answer_display(
|
270
|
+
display_name=sample_data["display_name"],
|
271
|
+
display_mode=ResponseDisplayMode.DETAILS_CLOSED,
|
272
|
+
answer=sample_data["answer"],
|
273
|
+
assistant_id=sample_data["assistant_id"],
|
274
|
+
)
|
275
|
+
|
276
|
+
text_with_display = f"Before\n{display_html}\nAfter"
|
277
|
+
|
278
|
+
result = remove_sub_agent_answer_from_text(
|
279
|
+
display_mode=ResponseDisplayMode.DETAILS_CLOSED,
|
280
|
+
text=text_with_display,
|
281
|
+
assistant_id=sample_data["assistant_id"],
|
282
|
+
)
|
283
|
+
|
284
|
+
assert "Before" in result
|
285
|
+
assert "After" in result
|
286
|
+
assert sample_data["answer"] not in result
|
287
|
+
|
288
|
+
def test_remove_sub_agent_answer_from_text_hidden_mode(self, sample_data):
|
289
|
+
"""Test removing sub-agent answer from text with HIDDEN mode."""
|
290
|
+
text = "Some text here"
|
291
|
+
result = remove_sub_agent_answer_from_text(
|
292
|
+
display_mode=ResponseDisplayMode.HIDDEN,
|
293
|
+
text=text,
|
294
|
+
assistant_id=sample_data["assistant_id"],
|
295
|
+
)
|
296
|
+
|
297
|
+
assert result == text
|
298
|
+
|
299
|
+
def test_roundtrip_build_and_remove(self, sample_data):
|
300
|
+
"""Test that building and then removing display results in clean text."""
|
301
|
+
original_text = "This is the original text."
|
302
|
+
|
303
|
+
# Build display
|
304
|
+
display_html = build_sub_agent_answer_display(
|
305
|
+
display_name=sample_data["display_name"],
|
306
|
+
display_mode=ResponseDisplayMode.DETAILS_OPEN,
|
307
|
+
answer=sample_data["answer"],
|
308
|
+
assistant_id=sample_data["assistant_id"],
|
309
|
+
)
|
310
|
+
|
311
|
+
# Insert into text
|
312
|
+
text_with_display = f"{original_text}\n{display_html}"
|
313
|
+
|
314
|
+
# Remove display
|
315
|
+
result = remove_sub_agent_answer_from_text(
|
316
|
+
display_mode=ResponseDisplayMode.DETAILS_OPEN,
|
317
|
+
text=text_with_display,
|
318
|
+
assistant_id=sample_data["assistant_id"],
|
319
|
+
)
|
320
|
+
|
321
|
+
# Should be back to original (with some whitespace differences)
|
322
|
+
assert original_text in result.strip()
|
323
|
+
assert sample_data["answer"] not in result
|
324
|
+
|
325
|
+
|
326
|
+
class TestEdgeCases:
|
327
|
+
"""Test suite for edge cases and error conditions."""
|
328
|
+
|
329
|
+
def test_empty_strings(self):
|
330
|
+
"""Test handling of empty strings."""
|
331
|
+
handler = _DetailsResponseDisplayHandler(mode="open")
|
332
|
+
|
333
|
+
result = handler.build_response_display(
|
334
|
+
display_name="", assistant_id="test", answer=""
|
335
|
+
)
|
336
|
+
|
337
|
+
assert "<details open>" in result
|
338
|
+
assert "<summary></summary>" in result
|
339
|
+
|
340
|
+
def test_multiline_content(self):
|
341
|
+
"""Test handling of multiline content."""
|
342
|
+
handler = _DetailsResponseDisplayHandler(mode="open")
|
343
|
+
|
344
|
+
multiline_answer = """Line 1
|
345
|
+
Line 2
|
346
|
+
Line 3 with spaces
|
347
|
+
|
348
|
+
Line 5 after blank line"""
|
349
|
+
|
350
|
+
result = handler.build_response_display(
|
351
|
+
display_name="Multi-line Test",
|
352
|
+
assistant_id="test_ml",
|
353
|
+
answer=multiline_answer,
|
354
|
+
)
|
355
|
+
|
356
|
+
assert multiline_answer in result
|
357
|
+
|
358
|
+
# Test removal
|
359
|
+
text_with_display = f"Before\n{result}\nAfter"
|
360
|
+
clean_result = handler.remove_response_display(
|
361
|
+
assistant_id="test_ml", text=text_with_display
|
362
|
+
)
|
363
|
+
|
364
|
+
assert "Before" in clean_result
|
365
|
+
assert "After" in clean_result
|
366
|
+
assert multiline_answer not in clean_result
|
367
|
+
|
368
|
+
def test_html_content_in_answer(self):
|
369
|
+
"""Test handling of HTML content within the answer."""
|
370
|
+
handler = _DetailsResponseDisplayHandler(mode="open")
|
371
|
+
|
372
|
+
html_answer = "<p>This is <strong>bold</strong> text with <em>emphasis</em></p>"
|
373
|
+
|
374
|
+
result = handler.build_response_display(
|
375
|
+
display_name="HTML Test", assistant_id="test_html", answer=html_answer
|
376
|
+
)
|
377
|
+
|
378
|
+
assert html_answer in result
|
379
|
+
|
380
|
+
# Test removal
|
381
|
+
text_with_display = f"Before\n{result}\nAfter"
|
382
|
+
clean_result = handler.remove_response_display(
|
383
|
+
assistant_id="test_html", text=text_with_display
|
384
|
+
)
|
385
|
+
|
386
|
+
assert "Before" in clean_result
|
387
|
+
assert "After" in clean_result
|
388
|
+
assert html_answer not in clean_result
|
389
|
+
|
390
|
+
def test_unicode_content(self):
|
391
|
+
"""Test handling of Unicode content."""
|
392
|
+
handler = _DetailsResponseDisplayHandler(mode="open")
|
393
|
+
|
394
|
+
unicode_content = "Testing Unicode: 你好 🌟 café naïve résumé"
|
395
|
+
|
396
|
+
result = handler.build_response_display(
|
397
|
+
display_name="Unicode Test",
|
398
|
+
assistant_id="test_unicode",
|
399
|
+
answer=unicode_content,
|
400
|
+
)
|
401
|
+
|
402
|
+
assert unicode_content in result
|
403
|
+
|
404
|
+
# Test removal
|
405
|
+
text_with_display = f"Before\n{result}\nAfter"
|
406
|
+
clean_result = handler.remove_response_display(
|
407
|
+
assistant_id="test_unicode", text=text_with_display
|
408
|
+
)
|
409
|
+
|
410
|
+
assert "Before" in clean_result
|
411
|
+
assert "After" in clean_result
|
412
|
+
assert unicode_content not in clean_result
|
@@ -1,8 +1,13 @@
|
|
1
|
+
from typing import Protocol, override
|
2
|
+
|
3
|
+
import unique_sdk
|
1
4
|
from pydantic import Field, create_model
|
2
5
|
from unique_sdk.utils.chat_in_space import send_message_and_wait_for_completion
|
3
6
|
|
4
7
|
from unique_toolkit.agentic.evaluation.schemas import EvaluationMetricName
|
5
|
-
from unique_toolkit.agentic.tools.a2a.config import
|
8
|
+
from unique_toolkit.agentic.tools.a2a.config import (
|
9
|
+
SubAgentToolConfig,
|
10
|
+
)
|
6
11
|
from unique_toolkit.agentic.tools.a2a.memory import (
|
7
12
|
get_sub_agent_short_term_memory_manager,
|
8
13
|
)
|
@@ -10,6 +15,7 @@ from unique_toolkit.agentic.tools.a2a.schema import (
|
|
10
15
|
SubAgentShortTermMemorySchema,
|
11
16
|
SubAgentToolInput,
|
12
17
|
)
|
18
|
+
from unique_toolkit.agentic.tools.agent_chunks_hanlder import AgentChunksHandler
|
13
19
|
from unique_toolkit.agentic.tools.factory import ToolFactory
|
14
20
|
from unique_toolkit.agentic.tools.schemas import ToolCallResponse
|
15
21
|
from unique_toolkit.agentic.tools.tool import Tool
|
@@ -20,9 +26,17 @@ from unique_toolkit.agentic.tools.tool_progress_reporter import (
|
|
20
26
|
from unique_toolkit.app import ChatEvent
|
21
27
|
from unique_toolkit.language_model import (
|
22
28
|
LanguageModelFunction,
|
23
|
-
LanguageModelMessage,
|
24
29
|
LanguageModelToolDescription,
|
25
30
|
)
|
31
|
+
from unique_toolkit.language_model.schemas import LanguageModelMessage
|
32
|
+
|
33
|
+
|
34
|
+
class SubAgentResponseSubscriber(Protocol):
|
35
|
+
def notify_sub_agent_response(
|
36
|
+
self,
|
37
|
+
sub_agent_assistant_id: str,
|
38
|
+
response: unique_sdk.Space.Message,
|
39
|
+
) -> None: ...
|
26
40
|
|
27
41
|
|
28
42
|
class SubAgentTool(Tool[SubAgentToolConfig]):
|
@@ -33,11 +47,15 @@ class SubAgentTool(Tool[SubAgentToolConfig]):
|
|
33
47
|
configuration: SubAgentToolConfig,
|
34
48
|
event: ChatEvent,
|
35
49
|
tool_progress_reporter: ToolProgressReporter | None = None,
|
50
|
+
name: str = "SubAgentTool",
|
51
|
+
display_name: str = "SubAgentTool",
|
36
52
|
):
|
37
53
|
super().__init__(configuration, event, tool_progress_reporter)
|
38
54
|
self._user_id = event.user_id
|
39
55
|
self._company_id = event.company_id
|
40
|
-
|
56
|
+
|
57
|
+
self.name = name
|
58
|
+
self._display_name = display_name
|
41
59
|
|
42
60
|
self._short_term_memory_manager = get_sub_agent_short_term_memory_manager(
|
43
61
|
company_id=self._company_id,
|
@@ -45,6 +63,13 @@ class SubAgentTool(Tool[SubAgentToolConfig]):
|
|
45
63
|
chat_id=event.payload.chat_id,
|
46
64
|
assistant_id=self.config.assistant_id,
|
47
65
|
)
|
66
|
+
self._subscribers: list[SubAgentResponseSubscriber] = []
|
67
|
+
|
68
|
+
def display_name(self) -> str:
|
69
|
+
return self._display_name
|
70
|
+
|
71
|
+
def subscribe(self, subscriber: SubAgentResponseSubscriber) -> None:
|
72
|
+
self._subscribers.append(subscriber)
|
48
73
|
|
49
74
|
def tool_description(self) -> LanguageModelToolDescription:
|
50
75
|
tool_input_model_with_description = create_model(
|
@@ -105,52 +130,96 @@ class SubAgentTool(Tool[SubAgentToolConfig]):
|
|
105
130
|
SubAgentShortTermMemorySchema(chat_id=chat_id)
|
106
131
|
)
|
107
132
|
|
108
|
-
async def
|
109
|
-
|
110
|
-
|
111
|
-
|
133
|
+
async def _notify_progress(
|
134
|
+
self,
|
135
|
+
tool_call: LanguageModelFunction,
|
136
|
+
message: str,
|
137
|
+
state: ProgressState,
|
138
|
+
) -> None:
|
139
|
+
if self.tool_progress_reporter is not None:
|
112
140
|
await self.tool_progress_reporter.notify_from_tool_call(
|
113
141
|
tool_call=tool_call,
|
114
|
-
name=
|
115
|
-
message=
|
116
|
-
state=
|
142
|
+
name=self._display_name,
|
143
|
+
message=message,
|
144
|
+
state=state,
|
145
|
+
)
|
146
|
+
|
147
|
+
async def _execute_and_handle_timeout(
|
148
|
+
self, tool_user_message: str, chat_id: str, tool_call: LanguageModelFunction
|
149
|
+
) -> unique_sdk.Space.Message:
|
150
|
+
try:
|
151
|
+
return await send_message_and_wait_for_completion(
|
152
|
+
user_id=self._user_id,
|
153
|
+
assistant_id=self.config.assistant_id,
|
154
|
+
company_id=self._company_id,
|
155
|
+
text=tool_user_message,
|
156
|
+
chat_id=chat_id,
|
157
|
+
poll_interval=self.config.poll_interval,
|
158
|
+
max_wait=self.config.max_wait,
|
159
|
+
stop_condition="completedAt",
|
160
|
+
)
|
161
|
+
except TimeoutError as e:
|
162
|
+
await self._notify_progress(
|
163
|
+
tool_call=tool_call,
|
164
|
+
message="Timeout while waiting for response from sub agent.",
|
165
|
+
state=ProgressState.FAILED,
|
117
166
|
)
|
118
167
|
|
168
|
+
raise TimeoutError(
|
169
|
+
"Timeout while waiting for response from sub agent. The user should consider increasing the max wait time.",
|
170
|
+
) from e
|
171
|
+
|
172
|
+
def _notify_subscribers(self, response: unique_sdk.Space.Message) -> None:
|
173
|
+
for subsciber in self._subscribers:
|
174
|
+
subsciber.notify_sub_agent_response(
|
175
|
+
sub_agent_assistant_id=self.config.assistant_id,
|
176
|
+
response=response,
|
177
|
+
)
|
178
|
+
|
179
|
+
async def run(self, tool_call: LanguageModelFunction) -> ToolCallResponse:
|
180
|
+
tool_input = SubAgentToolInput.model_validate(tool_call.arguments)
|
181
|
+
|
182
|
+
await self._notify_progress(
|
183
|
+
tool_call=tool_call,
|
184
|
+
message=tool_input.user_message,
|
185
|
+
state=ProgressState.RUNNING,
|
186
|
+
)
|
187
|
+
|
119
188
|
# Check if there is a saved chat id in short term memory
|
120
189
|
chat_id = await self._get_chat_id()
|
121
190
|
|
122
|
-
response = await
|
123
|
-
|
124
|
-
assistant_id=self.config.assistant_id,
|
125
|
-
company_id=self._company_id,
|
126
|
-
text=tool_input.user_message, # type: ignore
|
191
|
+
response = await self._execute_and_handle_timeout(
|
192
|
+
tool_user_message=tool_input.user_message, # type: ignore
|
127
193
|
chat_id=chat_id, # type: ignore
|
128
|
-
|
129
|
-
max_wait=self.config.max_wait,
|
194
|
+
tool_call=tool_call,
|
130
195
|
)
|
131
196
|
|
132
|
-
|
197
|
+
self._notify_subscribers(response)
|
198
|
+
|
199
|
+
if chat_id is None and self.config.reuse_chat:
|
133
200
|
await self._save_chat_id(response["chatId"])
|
134
201
|
|
135
202
|
if response["text"] is None:
|
136
203
|
raise ValueError("No response returned from sub agent")
|
137
204
|
|
138
|
-
self.
|
205
|
+
await self._notify_progress(
|
206
|
+
tool_call=tool_call,
|
207
|
+
message=tool_input.user_message,
|
208
|
+
state=ProgressState.FINISHED,
|
209
|
+
)
|
210
|
+
|
139
211
|
return ToolCallResponse(
|
140
212
|
id=tool_call.id, # type: ignore
|
141
213
|
name=tool_call.name,
|
142
214
|
content=response["text"],
|
143
215
|
)
|
144
216
|
|
217
|
+
@override
|
145
218
|
def get_tool_call_result_for_loop_history(
|
146
219
|
self,
|
147
220
|
tool_response: ToolCallResponse,
|
148
|
-
|
149
|
-
|
150
|
-
id=tool_response.id, # type: ignore
|
151
|
-
name=tool_response.name,
|
152
|
-
content=tool_response.content,
|
153
|
-
)
|
221
|
+
agent_chunks_handler: AgentChunksHandler,
|
222
|
+
) -> LanguageModelMessage: ... # Empty as method is deprecated
|
154
223
|
|
155
224
|
|
156
225
|
ToolFactory.register_tool(SubAgentTool, SubAgentToolConfig)
|
@@ -78,8 +78,17 @@ class ToolBuildConfig(BaseModel):
|
|
78
78
|
# Configuration can remain as a dict
|
79
79
|
return value
|
80
80
|
|
81
|
+
is_sub_agent_tool = (
|
82
|
+
value.get("is_sub_agent") or value.get("isSubAgent") or False
|
83
|
+
)
|
84
|
+
|
81
85
|
configuration = value.get("configuration", {})
|
82
|
-
|
86
|
+
|
87
|
+
if is_sub_agent_tool:
|
88
|
+
from unique_toolkit.agentic.tools.a2a.config import SubAgentToolConfig
|
89
|
+
|
90
|
+
config = SubAgentToolConfig.model_validate(configuration)
|
91
|
+
elif isinstance(configuration, dict):
|
83
92
|
# Local import to avoid circular import at module import time
|
84
93
|
from unique_toolkit.agentic.tools.factory import ToolFactory
|
85
94
|
|
@@ -6,6 +6,7 @@ from pydantic import BaseModel, Field
|
|
6
6
|
|
7
7
|
from unique_toolkit.agentic.evaluation.schemas import EvaluationMetricName
|
8
8
|
from unique_toolkit.agentic.tools.a2a.manager import A2AManager
|
9
|
+
from unique_toolkit.agentic.tools.a2a.service import SubAgentTool
|
9
10
|
from unique_toolkit.agentic.tools.config import ToolBuildConfig
|
10
11
|
from unique_toolkit.agentic.tools.factory import ToolFactory
|
11
12
|
from unique_toolkit.agentic.tools.mcp.manager import MCPManager
|
@@ -113,6 +114,7 @@ class ToolManager:
|
|
113
114
|
mcp_tools = self._mcp_manager.get_all_mcp_tools()
|
114
115
|
# Combine both types of tools
|
115
116
|
self.available_tools = internal_tools + mcp_tools + sub_agents
|
117
|
+
self._sub_agents = sub_agents
|
116
118
|
|
117
119
|
for t in self.available_tools:
|
118
120
|
if not t.is_enabled():
|
@@ -136,6 +138,10 @@ class ToolManager:
|
|
136
138
|
|
137
139
|
self._tools.append(t)
|
138
140
|
|
141
|
+
@property
|
142
|
+
def sub_agents(self) -> list[SubAgentTool]:
|
143
|
+
return self._sub_agents
|
144
|
+
|
139
145
|
def get_evaluation_check_list(self) -> list[EvaluationMetricName]:
|
140
146
|
return list(self._tool_evaluation_check_list)
|
141
147
|
|
@@ -143,7 +149,7 @@ class ToolManager:
|
|
143
149
|
self._logger.info(f"Loaded tools: {[tool.name for tool in self._tools]}")
|
144
150
|
|
145
151
|
def get_tools(self) -> list[Tool]:
|
146
|
-
return self._tools
|
152
|
+
return self._tools # type: ignore
|
147
153
|
|
148
154
|
def get_tool_by_name(self, name: str) -> Tool | None:
|
149
155
|
for tool in self._tools:
|
@@ -80,13 +80,14 @@ class LanguageModelFunction(BaseModel):
|
|
80
80
|
|
81
81
|
@field_validator("id", mode="before")
|
82
82
|
def randomize_id(cls, value):
|
83
|
-
|
83
|
+
if not value:
|
84
|
+
return uuid4().hex
|
85
|
+
else:
|
86
|
+
return value
|
84
87
|
|
85
88
|
@model_serializer()
|
86
89
|
def serialize_model(self):
|
87
90
|
seralization = {}
|
88
|
-
if self.id:
|
89
|
-
seralization["id"] = self.id
|
90
91
|
seralization["name"] = self.name
|
91
92
|
if self.arguments:
|
92
93
|
seralization["arguments"] = json.dumps(self.arguments)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: unique_toolkit
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.2.0
|
4
4
|
Summary:
|
5
5
|
License: Proprietary
|
6
6
|
Author: Cedric Klinkert
|
@@ -118,6 +118,13 @@ All notable changes to this project will be documented in this file.
|
|
118
118
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
119
119
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
120
120
|
|
121
|
+
|
122
|
+
## [1.2.0] - 2025-09-24
|
123
|
+
- Add ability to display sub agent responses in the chat.
|
124
|
+
|
125
|
+
## [1.1.9] - 2025-09-24
|
126
|
+
- Fix bug in `LanguageModelFunction` to extend support mistral tool calling.
|
127
|
+
|
121
128
|
## [1.1.8] - 2025-09-23
|
122
129
|
- Revert last to version 1.1.6
|
123
130
|
|
@@ -44,19 +44,23 @@ unique_toolkit/agentic/history_manager/history_construction_with_contents.py,sha
|
|
44
44
|
unique_toolkit/agentic/history_manager/history_manager.py,sha256=qo6vjEXueCXUxHSrawYeMmFn7tuTEXGTWnBVb0H8bDY,8423
|
45
45
|
unique_toolkit/agentic/history_manager/loop_token_reducer.py,sha256=9ZWh3dfs7SGsU33bxzKo9x94haEhc7Rerk9PtfFzcxg,18474
|
46
46
|
unique_toolkit/agentic/history_manager/utils.py,sha256=NDSSz0Jp3oVJU3iKlVScmM1AOe-6hTiVjLr16DUPsV0,5656
|
47
|
-
unique_toolkit/agentic/postprocessor/postprocessor_manager.py,sha256=
|
47
|
+
unique_toolkit/agentic/postprocessor/postprocessor_manager.py,sha256=GDzJhaoOUwxZ37IINkQ7au4CHmAOFS5miP2lqv8ZwZA,4277
|
48
48
|
unique_toolkit/agentic/reference_manager/reference_manager.py,sha256=1GeoFX1-RLdTcns1358GJADDSAcTAM2J0jJJpln08qo,4005
|
49
49
|
unique_toolkit/agentic/short_term_memory_manager/persistent_short_term_memory_manager.py,sha256=uF3HSoZF0hBfuNhIE9N8KRtuwDfpoeXUFVrv_cyZ3Sw,5839
|
50
50
|
unique_toolkit/agentic/thinking_manager/thinking_manager.py,sha256=41QWFsdRrbWlQHBfYCFv726UDom4WbcvaRfjCmoUOQI,4183
|
51
51
|
unique_toolkit/agentic/tools/__init__.py,sha256=-ToY9-Xiz0K7qCUydH1h1yG6n4h1hQS8sBuSVPNEq2Y,43
|
52
52
|
unique_toolkit/agentic/tools/a2a/__init__.py,sha256=NdY0J33b1G4sbx6UWwNS74JVSAeEtu8u_iEXOT64Uq0,187
|
53
|
-
unique_toolkit/agentic/tools/a2a/config.py,sha256=
|
54
|
-
unique_toolkit/agentic/tools/a2a/manager.py,sha256=
|
53
|
+
unique_toolkit/agentic/tools/a2a/config.py,sha256=m6INkff6jHEMB_DPm8MmtXfrGaMy1TIwXd1vFwHBFAU,1312
|
54
|
+
unique_toolkit/agentic/tools/a2a/manager.py,sha256=yuuQuBrAcsT3gAWEdxf6EvRnL_iWtvaK14lRs21w5PA,1665
|
55
55
|
unique_toolkit/agentic/tools/a2a/memory.py,sha256=4VFBzITCv5E_8YCc4iF4Y6FhzplS2C-FZaZHdeC7DyA,1028
|
56
|
+
unique_toolkit/agentic/tools/a2a/postprocessing/__init__.py,sha256=9M5RRSO4gmQVI1gfve6MGEfVktT9WcELQFZ8Sv6xu4Y,160
|
57
|
+
unique_toolkit/agentic/tools/a2a/postprocessing/display.py,sha256=rlBWO2M8Lr5Kx-vmvwlV-vEu33BZE4votP-TMr3-3Dw,3366
|
58
|
+
unique_toolkit/agentic/tools/a2a/postprocessing/postprocessor.py,sha256=BWXFsv5akPhkNxRQ3JLktrBkoT1_66joTel_uM4BgRY,6963
|
59
|
+
unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display.py,sha256=UVOd5tVTltngVfsdOn6cuyvG7QmRBkfgUj_SruTgmHI,15279
|
56
60
|
unique_toolkit/agentic/tools/a2a/schema.py,sha256=T1l5z6trtPE5nhqPzt5tvfRNDhqL_ST1Wj7_lBWJ58g,304
|
57
|
-
unique_toolkit/agentic/tools/a2a/service.py,sha256=
|
61
|
+
unique_toolkit/agentic/tools/a2a/service.py,sha256=cACUPwWXzi_XfPlFqK0x6nawugx17Vq18-XRUbhgL4k,7669
|
58
62
|
unique_toolkit/agentic/tools/agent_chunks_hanlder.py,sha256=x32Dp1Z8cVW5i-XzXbaMwX2KHPcNGmqEU-FB4AV9ZGo,1909
|
59
|
-
unique_toolkit/agentic/tools/config.py,sha256=
|
63
|
+
unique_toolkit/agentic/tools/config.py,sha256=91Gw92YoTC6WeWa4lfOpXSvIYekCwELcVNkHZZEkW2o,4936
|
60
64
|
unique_toolkit/agentic/tools/factory.py,sha256=Wt0IGSbLg8ZTq5PU9p_JTW0LtNATWLpc3336irJKXlM,1277
|
61
65
|
unique_toolkit/agentic/tools/mcp/__init__.py,sha256=RLF_p-LDRC7GhiB3fdCi4u3bh6V9PY_w26fg61BLyco,122
|
62
66
|
unique_toolkit/agentic/tools/mcp/manager.py,sha256=DPYwwDe6RSZyuPaxn-je49fP_qOOs0ZV46EM6GZcV4c,2748
|
@@ -66,7 +70,7 @@ unique_toolkit/agentic/tools/schemas.py,sha256=0ZR8xCdGj1sEdPE0lfTIG2uSR5zqWoprU
|
|
66
70
|
unique_toolkit/agentic/tools/test/test_mcp_manager.py,sha256=9F7FjpYKeOkg2Z4bt2H1WGaxu9fzB-1iiE-b7g3KzQk,15724
|
67
71
|
unique_toolkit/agentic/tools/test/test_tool_progress_reporter.py,sha256=dod5QPqgGUInVAGXAbsAKNTEypIi6pUEWhDbJr9YfUU,6307
|
68
72
|
unique_toolkit/agentic/tools/tool.py,sha256=m56VLxiHuKU2_J5foZp00xhm5lTxWEW7zRLGbIE9ssU,6744
|
69
|
-
unique_toolkit/agentic/tools/tool_manager.py,sha256=
|
73
|
+
unique_toolkit/agentic/tools/tool_manager.py,sha256=l8OGQiSeMWqesnFQ4vmgnrSU4e6ipAbB2pq0HmQM4AE,11140
|
70
74
|
unique_toolkit/agentic/tools/tool_progress_reporter.py,sha256=ixud9VoHey1vlU1t86cW0-WTvyTwMxNSWBon8I11SUk,7955
|
71
75
|
unique_toolkit/agentic/tools/utils/__init__.py,sha256=iD1YYzf9LcJFv95Z8BqCAFSewNBabybZRZyvPKGfvro,27
|
72
76
|
unique_toolkit/agentic/tools/utils/execution/__init__.py,sha256=OHiKpqBnfhBiEQagKVWJsZlHv8smPp5OI4dFIexzibw,37
|
@@ -117,7 +121,7 @@ unique_toolkit/language_model/functions.py,sha256=PNCmbYovhgMSkY89p7-3DunG6jIeka
|
|
117
121
|
unique_toolkit/language_model/infos.py,sha256=eHln--Y5f6znFxknV6A8m-fRaEpH5-kmRh9m-ZWqco4,57188
|
118
122
|
unique_toolkit/language_model/prompt.py,sha256=JSawaLjQg3VR-E2fK8engFyJnNdk21zaO8pPIodzN4Q,3991
|
119
123
|
unique_toolkit/language_model/reference.py,sha256=nkX2VFz-IrUz8yqyc3G5jUMNwrNpxITBrMEKkbqqYoI,8583
|
120
|
-
unique_toolkit/language_model/schemas.py,sha256=
|
124
|
+
unique_toolkit/language_model/schemas.py,sha256=w23zH2OAYkTsS-wAqelUdhO9TCgis0TbFa8PszmhZYY,16501
|
121
125
|
unique_toolkit/language_model/service.py,sha256=JkYGtCug8POQskTv_aoYkzTMOaPCWRM94y73o3bUttQ,12019
|
122
126
|
unique_toolkit/language_model/utils.py,sha256=bPQ4l6_YO71w-zaIPanUUmtbXC1_hCvLK0tAFc3VCRc,1902
|
123
127
|
unique_toolkit/protocols/support.py,sha256=V15WEIFKVMyF1QCnR8vIi4GrJy4dfTCB6d6JlqPZ58o,2341
|
@@ -128,7 +132,7 @@ unique_toolkit/short_term_memory/schemas.py,sha256=OhfcXyF6ACdwIXW45sKzjtZX_gkcJ
|
|
128
132
|
unique_toolkit/short_term_memory/service.py,sha256=5PeVBu1ZCAfyDb2HLVvlmqSbyzBBuE9sI2o9Aajqjxg,8884
|
129
133
|
unique_toolkit/smart_rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
130
134
|
unique_toolkit/smart_rules/compile.py,sha256=cxWjb2dxEI2HGsakKdVCkSNi7VK9mr08w5sDcFCQyWI,9553
|
131
|
-
unique_toolkit-1.
|
132
|
-
unique_toolkit-1.
|
133
|
-
unique_toolkit-1.
|
134
|
-
unique_toolkit-1.
|
135
|
+
unique_toolkit-1.2.0.dist-info/LICENSE,sha256=GlN8wHNdh53xwOPg44URnwag6TEolCjoq3YD_KrWgss,193
|
136
|
+
unique_toolkit-1.2.0.dist-info/METADATA,sha256=Fp7cz1tbhox_Vcohuj8hL8ahu14rw0GJ6gUbL0t-T1c,33290
|
137
|
+
unique_toolkit-1.2.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
138
|
+
unique_toolkit-1.2.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|