unique_toolkit 1.7.0__py3-none-any.whl → 1.8.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.
Files changed (26) hide show
  1. unique_toolkit/agentic/tools/a2a/__init__.py +19 -3
  2. unique_toolkit/agentic/tools/a2a/config.py +12 -52
  3. unique_toolkit/agentic/tools/a2a/evaluation/__init__.py +10 -3
  4. unique_toolkit/agentic/tools/a2a/evaluation/_utils.py +66 -0
  5. unique_toolkit/agentic/tools/a2a/evaluation/config.py +13 -2
  6. unique_toolkit/agentic/tools/a2a/evaluation/evaluator.py +82 -89
  7. unique_toolkit/agentic/tools/a2a/manager.py +2 -2
  8. unique_toolkit/agentic/tools/a2a/postprocessing/__init__.py +9 -1
  9. unique_toolkit/agentic/tools/a2a/postprocessing/{display.py → _display.py} +16 -7
  10. unique_toolkit/agentic/tools/a2a/postprocessing/_utils.py +19 -0
  11. unique_toolkit/agentic/tools/a2a/postprocessing/config.py +24 -0
  12. unique_toolkit/agentic/tools/a2a/postprocessing/postprocessor.py +109 -110
  13. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_consolidate_references.py +665 -0
  14. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display.py +54 -75
  15. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_postprocessor_reference_functions.py +53 -45
  16. unique_toolkit/agentic/tools/a2a/tool/__init__.py +4 -0
  17. unique_toolkit/agentic/tools/a2a/{memory.py → tool/_memory.py} +1 -1
  18. unique_toolkit/agentic/tools/a2a/{schema.py → tool/_schema.py} +0 -6
  19. unique_toolkit/agentic/tools/a2a/tool/config.py +63 -0
  20. unique_toolkit/agentic/tools/a2a/{service.py → tool/service.py} +108 -65
  21. unique_toolkit/agentic/tools/config.py +2 -2
  22. unique_toolkit/agentic/tools/tool_manager.py +1 -2
  23. {unique_toolkit-1.7.0.dist-info → unique_toolkit-1.8.0.dist-info}/METADATA +5 -1
  24. {unique_toolkit-1.7.0.dist-info → unique_toolkit-1.8.0.dist-info}/RECORD +26 -20
  25. {unique_toolkit-1.7.0.dist-info → unique_toolkit-1.8.0.dist-info}/LICENSE +0 -0
  26. {unique_toolkit-1.7.0.dist-info → unique_toolkit-1.8.0.dist-info}/WHEEL +0 -0
@@ -1,19 +1,21 @@
1
1
  import logging
2
- import re
3
2
  from typing import TypedDict, override
4
3
 
5
4
  import unique_sdk
6
5
 
7
6
  from unique_toolkit.agentic.postprocessor.postprocessor_manager import Postprocessor
8
- from unique_toolkit.agentic.tools.a2a.config import (
9
- ResponseDisplayMode,
10
- SubAgentToolDisplayConfig,
7
+ from unique_toolkit.agentic.tools.a2a.postprocessing._display import (
8
+ _build_sub_agent_answer_display,
9
+ _remove_sub_agent_answer_from_text,
11
10
  )
12
- from unique_toolkit.agentic.tools.a2a.postprocessing.display import (
13
- build_sub_agent_answer_display,
14
- remove_sub_agent_answer_from_text,
11
+ from unique_toolkit.agentic.tools.a2a.postprocessing._utils import (
12
+ _replace_references_in_text,
15
13
  )
16
- from unique_toolkit.agentic.tools.a2a.service import SubAgentTool
14
+ from unique_toolkit.agentic.tools.a2a.postprocessing.config import (
15
+ SubAgentDisplayConfig,
16
+ SubAgentResponseDisplayMode,
17
+ )
18
+ from unique_toolkit.agentic.tools.a2a.tool import SubAgentTool
17
19
  from unique_toolkit.content.schemas import ContentReference
18
20
  from unique_toolkit.language_model.schemas import LanguageModelStreamResponse
19
21
 
@@ -23,14 +25,14 @@ SpaceMessage = unique_sdk.Space.Message
23
25
 
24
26
 
25
27
  class _SubAgentMessageInfo(TypedDict):
26
- text: str | None
28
+ text: str
27
29
  references: list[unique_sdk.Space.Reference]
28
30
 
29
31
 
30
32
  class _SubAgentToolInfo(TypedDict):
31
33
  display_name: str
32
- display_config: SubAgentToolDisplayConfig
33
- responses: list[_SubAgentMessageInfo]
34
+ display_config: SubAgentDisplayConfig
35
+ responses: dict[int, _SubAgentMessageInfo]
34
36
 
35
37
 
36
38
  class SubAgentResponsesPostprocessor(Postprocessor):
@@ -38,93 +40,79 @@ class SubAgentResponsesPostprocessor(Postprocessor):
38
40
  self,
39
41
  user_id: str,
40
42
  company_id: str,
41
- agent_chat_id: str,
42
- sub_agent_tools: list[SubAgentTool],
43
- ):
43
+ main_agent_chat_id: str,
44
+ ) -> None:
44
45
  super().__init__(name=self.__class__.__name__)
45
46
 
46
47
  self._user_id = user_id
47
48
  self._company_id = company_id
48
-
49
- self._agent_chat_id = agent_chat_id
49
+ self._main_agent_chat_id = main_agent_chat_id
50
50
 
51
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
- responses=[],
61
- )
62
- )
63
-
64
- self._sub_agent_message = None
52
+ self._main_agent_message: SpaceMessage | None = None
65
53
 
66
54
  @override
67
55
  async def run(self, loop_response: LanguageModelStreamResponse) -> None:
68
- self._sub_agent_message = await unique_sdk.Space.get_latest_message_async(
56
+ self._main_agent_message = await unique_sdk.Space.get_latest_message_async(
69
57
  user_id=self._user_id,
70
58
  company_id=self._company_id,
71
- chat_id=self._agent_chat_id,
59
+ chat_id=self._main_agent_chat_id,
72
60
  )
73
61
 
74
62
  @override
75
63
  def apply_postprocessing_to_response(
76
64
  self, loop_response: LanguageModelStreamResponse
77
65
  ) -> bool:
78
- logger.info("Adding sub agent responses to the response")
79
-
80
- # Get responses to display
81
- displayed = {}
82
- for assistant_id, tool_info in self._assistant_id_to_tool_info.items():
83
- display_mode = tool_info["display_config"].mode
84
-
85
- if len(tool_info["responses"]) == 0:
86
- logger.warning(
87
- "No response from assistant %s",
88
- assistant_id,
89
- )
90
- continue
91
-
92
- if display_mode != ResponseDisplayMode.HIDDEN:
93
- displayed[assistant_id] = tool_info["responses"]
66
+ logger.info("Prepending sub agent responses to the main agent response")
67
+
68
+ if len(self._assistant_id_to_tool_info) == 0 or all(
69
+ len(tool_info["responses"]) == 0
70
+ for tool_info in self._assistant_id_to_tool_info.values()
71
+ ):
72
+ logger.info("No sub agent responses to prepend")
73
+ return False
74
+
75
+ if self._main_agent_message is None:
76
+ raise ValueError(
77
+ "Main agent message is not set, the `run` method must be called first"
78
+ )
94
79
 
95
80
  existing_refs = {
96
81
  ref.source_id: ref.sequence_number
97
82
  for ref in loop_response.message.references
98
83
  }
99
- _consolidate_references_in_place(displayed, existing_refs)
100
84
 
101
- modified = len(displayed) > 0
102
- for assistant_id, messages in reversed(displayed.items()):
103
- for i in reversed(range(len(messages))):
104
- message = messages[i]
85
+ _consolidate_references_in_place(
86
+ list(self._assistant_id_to_tool_info.values()), existing_refs
87
+ )
88
+
89
+ answers = []
90
+ for assistant_id in self._assistant_id_to_tool_info.keys():
91
+ messages = self._assistant_id_to_tool_info[assistant_id]["responses"]
92
+
93
+ for sequence_number in sorted(messages):
94
+ message = messages[sequence_number]
105
95
  tool_info = self._assistant_id_to_tool_info[assistant_id]
96
+
106
97
  display_mode = tool_info["display_config"].mode
107
98
  display_name = tool_info["display_name"]
108
99
  if len(messages) > 1:
109
- display_name += f" {i + 1}"
100
+ display_name += f" {sequence_number}"
110
101
 
111
- loop_response.message.text = (
112
- build_sub_agent_answer_display(
102
+ answers.append(
103
+ _build_sub_agent_answer_display(
113
104
  display_name=display_name,
114
105
  assistant_id=assistant_id,
115
106
  display_mode=display_mode,
116
107
  answer=message["text"],
117
108
  )
118
- + loop_response.message.text
119
109
  )
120
110
 
121
- assert self._sub_agent_message is not None
122
-
123
111
  loop_response.message.references.extend(
124
112
  ContentReference(
125
- message_id=self._sub_agent_message["id"],
113
+ message_id=self._main_agent_message["id"],
126
114
  source_id=ref["sourceId"],
127
- url=ref["url"],
115
+ url=ref["url"] or "",
128
116
  source=ref["source"],
129
117
  name=ref["name"],
130
118
  sequence_number=ref["sequenceNumber"],
@@ -132,22 +120,44 @@ class SubAgentResponsesPostprocessor(Postprocessor):
132
120
  for ref in message["references"]
133
121
  )
134
122
 
135
- return modified
123
+ loop_response.message.text = "\n".join(answers) + loop_response.message.text
124
+
125
+ return True
136
126
 
137
127
  @override
138
128
  async def remove_from_text(self, text) -> str:
139
129
  for assistant_id, tool_info in self._assistant_id_to_tool_info.items():
140
130
  display_config = tool_info["display_config"]
141
131
  if display_config.remove_from_history:
142
- text = remove_sub_agent_answer_from_text(
132
+ text = _remove_sub_agent_answer_from_text(
143
133
  display_mode=display_config.mode,
144
134
  text=text,
145
135
  assistant_id=assistant_id,
146
136
  )
147
137
  return text
148
138
 
139
+ def register_sub_agent_tool(
140
+ self, tool: SubAgentTool, display_config: SubAgentDisplayConfig
141
+ ) -> None:
142
+ if display_config.mode == SubAgentResponseDisplayMode.HIDDEN:
143
+ logger.info(
144
+ "Sub agent tool %s has display mode `hidden`, responses will be ignored.",
145
+ tool.config.assistant_id,
146
+ )
147
+ return
148
+
149
+ if tool.config.assistant_id not in self._assistant_id_to_tool_info:
150
+ tool.subscribe(self)
151
+ self._assistant_id_to_tool_info[tool.config.assistant_id] = (
152
+ _SubAgentToolInfo(
153
+ display_config=display_config,
154
+ display_name=tool.display_name(),
155
+ responses={},
156
+ )
157
+ )
158
+
149
159
  def notify_sub_agent_response(
150
- self, sub_agent_assistant_id: str, response: SpaceMessage
160
+ self, response: SpaceMessage, sub_agent_assistant_id: str, sequence_number: int
151
161
  ) -> None:
152
162
  if sub_agent_assistant_id not in self._assistant_id_to_tool_info:
153
163
  logger.warning(
@@ -156,47 +166,57 @@ class SubAgentResponsesPostprocessor(Postprocessor):
156
166
  )
157
167
  return
158
168
 
159
- self._assistant_id_to_tool_info[sub_agent_assistant_id]["responses"].append(
160
- {
161
- "text": response["text"],
162
- "references": [
163
- {
164
- "name": ref["name"],
165
- "url": ref["url"],
166
- "sequenceNumber": ref["sequenceNumber"],
167
- "originalIndex": [],
168
- "sourceId": ref["sourceId"],
169
- "source": ref["source"],
170
- }
171
- for ref in response["references"] or []
172
- ],
173
- }
174
- )
169
+ if response["text"] is None:
170
+ logger.warning(
171
+ "Sub agent response %s has no text, message will be ignored.",
172
+ sequence_number,
173
+ )
174
+ return
175
+
176
+ self._assistant_id_to_tool_info[sub_agent_assistant_id]["responses"][
177
+ sequence_number
178
+ ] = {
179
+ "text": response["text"],
180
+ "references": [
181
+ {
182
+ "name": ref["name"],
183
+ "url": ref["url"],
184
+ "sequenceNumber": ref["sequenceNumber"],
185
+ "originalIndex": [],
186
+ "sourceId": ref["sourceId"],
187
+ "source": ref["source"],
188
+ }
189
+ for ref in response["references"] or []
190
+ ],
191
+ }
175
192
 
176
193
 
177
194
  def _consolidate_references_in_place(
178
- messages: dict[str, list[_SubAgentMessageInfo]], existing_refs: dict[str, int]
195
+ messages: list[_SubAgentToolInfo], existing_refs: dict[str, int]
179
196
  ) -> None:
180
197
  start_index = max(existing_refs.values(), default=0) + 1
181
198
 
182
- for assistant_id, assistant_messages in messages.items():
183
- for message in assistant_messages:
199
+ for assistant_tool_info in messages:
200
+ assistant_messages = assistant_tool_info["responses"]
201
+
202
+ for sequence_number in sorted(assistant_messages):
203
+ message = assistant_messages[sequence_number]
204
+
184
205
  references = message["references"]
185
- if len(references) == 0 or message["text"] is None:
186
- logger.info(
187
- "Message from assistant %s does not contain any references",
188
- assistant_id,
206
+ if len(references) == 0:
207
+ logger.debug(
208
+ "Message from assistant %s with sequence number %s does not contain any references",
209
+ assistant_tool_info["display_name"],
210
+ sequence_number,
189
211
  )
190
212
  continue
191
213
 
192
214
  references = list(sorted(references, key=lambda ref: ref["sequenceNumber"]))
193
-
194
215
  ref_map = {}
195
-
196
216
  message_new_refs = []
217
+
197
218
  for reference in references:
198
219
  source_id = reference["sourceId"]
199
-
200
220
  if source_id not in existing_refs:
201
221
  message_new_refs.append(reference)
202
222
  existing_refs[source_id] = start_index
@@ -208,24 +228,3 @@ def _consolidate_references_in_place(
208
228
 
209
229
  message["text"] = _replace_references_in_text(message["text"], ref_map)
210
230
  message["references"] = message_new_refs
211
-
212
-
213
- def _replace_references_in_text_non_overlapping(
214
- text: str, ref_map: dict[int, int]
215
- ) -> str:
216
- for orig, repl in ref_map.items():
217
- text = re.sub(rf"<sup>{orig}</sup>", f"<sup>{repl}</sup>", text)
218
- return text
219
-
220
-
221
- def _replace_references_in_text(text: str, ref_map: dict[int, int]) -> str:
222
- # 2 phase replacement, since the map keys and values can overlap
223
- max_ref = max(max(ref_map.keys(), default=0), max(ref_map.values(), default=0)) + 1
224
- unique_refs = range(max_ref, max_ref + len(ref_map))
225
-
226
- text = _replace_references_in_text_non_overlapping(
227
- text, dict(zip(ref_map.keys(), unique_refs))
228
- )
229
- return _replace_references_in_text_non_overlapping(
230
- text, dict(zip(unique_refs, ref_map.values()))
231
- )