unique_toolkit 1.31.0__py3-none-any.whl → 1.31.2__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.
@@ -1,6 +1,7 @@
1
1
  import re
2
2
  from typing import Literal, NamedTuple
3
3
 
4
+ from unique_toolkit._common.utils.jinja.render import render_template
4
5
  from unique_toolkit.agentic.tools.a2a.postprocessing.config import (
5
6
  SubAgentDisplayConfig,
6
7
  SubAgentResponseDisplayMode,
@@ -84,6 +85,12 @@ def _prepare_title_template(
84
85
  return display_title_template.replace("{}", "{%s}" % display_name_placeholder)
85
86
 
86
87
 
88
+ def _clean_linebreaks(text: str) -> str:
89
+ text = text.strip()
90
+ text = re.sub(r"^(<br>)*|(<br>)*$", "", text)
91
+ return text
92
+
93
+
87
94
  def _get_display_template(
88
95
  mode: SubAgentResponseDisplayMode,
89
96
  add_quote_border: bool,
@@ -126,7 +133,7 @@ def _get_display_template(
126
133
  if add_block_border:
127
134
  template = _wrap_with_block_border(template)
128
135
 
129
- return template.strip()
136
+ return _clean_linebreaks(template)
130
137
 
131
138
 
132
139
  def _get_display_removal_re(
@@ -167,8 +174,7 @@ def get_sub_agent_answer_parts(
167
174
 
168
175
  substrings = []
169
176
  for config in display_config.answer_substrings_config:
170
- match = re.search(config.regexp, answer)
171
- if match is not None:
177
+ for match in config.regexp.finditer(answer):
172
178
  text = match.group(0)
173
179
  substrings.append(
174
180
  SubAgentAnswerPart(
@@ -183,7 +189,7 @@ def get_sub_agent_answer_parts(
183
189
  def get_sub_agent_answer_display(
184
190
  display_name: str,
185
191
  display_config: SubAgentDisplayConfig,
186
- answer: str | list[str],
192
+ answer: str | list[SubAgentAnswerPart],
187
193
  assistant_id: str,
188
194
  ) -> str:
189
195
  template = _get_display_template(
@@ -194,7 +200,12 @@ def get_sub_agent_answer_display(
194
200
  )
195
201
 
196
202
  if isinstance(answer, list):
197
- answer = display_config.answer_substrings_separator.join(answer)
203
+ answer = render_template(
204
+ display_config.answer_substrings_jinja_template,
205
+ {
206
+ "substrings": [answer.formatted_text for answer in answer],
207
+ },
208
+ )
198
209
 
199
210
  return template.format(
200
211
  display_name=display_name, answer=answer, assistant_id=assistant_id
@@ -1,3 +1,4 @@
1
+ import re
1
2
  from enum import StrEnum
2
3
  from typing import Literal
3
4
 
@@ -16,7 +17,7 @@ class SubAgentResponseDisplayMode(StrEnum):
16
17
  class SubAgentAnswerSubstringConfig(BaseModel):
17
18
  model_config = get_configuration_dict()
18
19
 
19
- regexp: str = Field(
20
+ regexp: re.Pattern[str] = Field(
20
21
  description="The regular expression to use to extract the substring. The first capture group will always be used.",
21
22
  )
22
23
  display_template: str = Field(
@@ -25,6 +26,13 @@ class SubAgentAnswerSubstringConfig(BaseModel):
25
26
  )
26
27
 
27
28
 
29
+ _ANSWER_SUBSTRINGS_JINJA_TEMPLATE = """
30
+ {% for substring in substrings %}
31
+ {{ substring }}
32
+ {% endfor %}
33
+ """.strip()
34
+
35
+
28
36
  class SubAgentDisplayConfig(BaseModel):
29
37
  model_config = get_configuration_dict()
30
38
 
@@ -64,7 +72,7 @@ class SubAgentDisplayConfig(BaseModel):
64
72
  default=[],
65
73
  description="If set, only parts of the answer matching the provided regular expressions will be displayed.",
66
74
  )
67
- answer_substrings_separator: str = Field(
68
- default="\n",
69
- description="The separator to use between the substrings.",
75
+ answer_substrings_jinja_template: str = Field(
76
+ default=_ANSWER_SUBSTRINGS_JINJA_TEMPLATE,
77
+ description="The template to use in order to format the different answer substrings, if any.",
70
78
  )
@@ -7,6 +7,7 @@ import unique_sdk
7
7
  from pydantic import BaseModel, Field
8
8
 
9
9
  from unique_toolkit._common.pydantic_helpers import get_configuration_dict
10
+ from unique_toolkit._common.utils.jinja.render import render_template
10
11
  from unique_toolkit.agentic.postprocessor.postprocessor_manager import Postprocessor
11
12
  from unique_toolkit.agentic.tools.a2a.postprocessing._display_utils import (
12
13
  get_sub_agent_answer_display,
@@ -38,20 +39,22 @@ class SubAgentDisplaySpec(NamedTuple):
38
39
  display_config: SubAgentDisplayConfig
39
40
 
40
41
 
42
+ _ANSWERS_JINJA_TEMPLATE = """
43
+ {% for answer in answers %}
44
+ {{ answer }}
45
+ {% endfor %}
46
+ """.strip()
47
+
48
+
41
49
  class SubAgentResponsesPostprocessorConfig(BaseModel):
42
50
  model_config = get_configuration_dict()
43
51
 
44
52
  sleep_time_before_update: float = Field(
45
- default=1, description="Time to sleep before updating the main agent message."
53
+ default=0, description="Time to sleep before updating the main agent message."
46
54
  )
47
-
48
- remove_duplicate_answers: bool = Field(
49
- default=False,
50
- description="If set, duplicate answers will only be displayed once. If sub agent is configured to display only substrings, this will remove duplicate substrings across different responses.",
51
- )
52
- answer_separator: str = Field(
53
- default="",
54
- description="The separator to use between the different sub agent answers.",
55
+ answers_jinja_template: str = Field(
56
+ default=_ANSWERS_JINJA_TEMPLATE,
57
+ description="The template to use to display the sub agent answers.",
55
58
  )
56
59
 
57
60
 
@@ -106,8 +109,6 @@ class SubAgentResponsesDisplayPostprocessor(Postprocessor):
106
109
  answers_displayed_before = []
107
110
  answers_displayed_after = []
108
111
 
109
- all_answers_displayed = set()
110
-
111
112
  for assistant_id, responses in displayed_sub_agent_responses.items():
112
113
  for response in responses:
113
114
  message = response.message
@@ -130,34 +131,20 @@ class SubAgentResponsesDisplayPostprocessor(Postprocessor):
130
131
  assistant_id,
131
132
  response.sequence_number,
132
133
  )
133
-
134
- message_text = message["text"] or ""
134
+ continue
135
135
 
136
136
  answer_parts = get_sub_agent_answer_parts(
137
- answer=message_text,
137
+ answer=message["text"],
138
138
  display_config=tool_info.display_config,
139
139
  )
140
140
 
141
141
  if len(answer_parts) == 0:
142
142
  continue
143
143
 
144
- answer_display_texts = []
145
- if self._config.remove_duplicate_answers:
146
- for answer_part in answer_parts:
147
- if answer_part.matching_text in all_answers_displayed:
148
- continue
149
-
150
- all_answers_displayed.add(answer_part.matching_text)
151
- answer_display_texts.append(answer_part.formatted_text)
152
- else:
153
- answer_display_texts = [
154
- answer_part.formatted_text for answer_part in answer_parts
155
- ]
156
-
157
144
  answer = get_sub_agent_answer_display(
158
145
  display_name=display_name,
159
146
  display_config=tool_info.display_config,
160
- answer=answer_display_texts,
147
+ answer=answer_parts,
161
148
  assistant_id=assistant_id,
162
149
  )
163
150
 
@@ -170,7 +157,7 @@ class SubAgentResponsesDisplayPostprocessor(Postprocessor):
170
157
  text=loop_response.message.text,
171
158
  answers_before=answers_displayed_before,
172
159
  answers_after=answers_displayed_after,
173
- sep=self._config.answer_separator,
160
+ template=self._config.answers_jinja_template,
174
161
  )
175
162
 
176
163
  return True
@@ -211,12 +198,12 @@ def _get_final_answer_display(
211
198
  text: str,
212
199
  answers_before: list[str],
213
200
  answers_after: list[str],
214
- sep: str = "<br>\n\n",
201
+ template: str = _ANSWERS_JINJA_TEMPLATE,
215
202
  ) -> str:
216
203
  if len(answers_before) > 0:
217
- text = sep.join(answers_before) + sep + text
204
+ text = render_template(template, {"answers": answers_before}) + text
218
205
 
219
206
  if len(answers_after) > 0:
220
- text = text + sep + sep.join(answers_after)
207
+ text = text + render_template(template, {"answers": answers_after})
221
208
 
222
209
  return text.strip()
@@ -1492,9 +1492,7 @@ def test_get_sub_agent_answer_parts__returns_empty_list__when_no_matches() -> No
1492
1492
 
1493
1493
 
1494
1494
  @pytest.mark.ai
1495
- def test_get_sub_agent_answer_parts__extracts_first_match_only__for_each_regexp() -> (
1496
- None
1497
- ):
1495
+ def test_get_sub_agent_answer_parts__extracts_all_matches__for_each_regexp() -> None:
1498
1496
  """
1499
1497
  Purpose: Verify only first match per regexp is extracted.
1500
1498
  Why this matters: Function uses re.search which finds first occurrence.
@@ -1513,7 +1511,7 @@ def test_get_sub_agent_answer_parts__extracts_first_match_only__for_each_regexp(
1513
1511
  result = get_sub_agent_answer_parts(answer=answer, display_config=config)
1514
1512
 
1515
1513
  # Assert
1516
- assert len(result) == 1
1514
+ assert len(result) == 2
1517
1515
  assert result[0].matching_text == "42"
1518
1516
 
1519
1517
 
@@ -1661,10 +1659,12 @@ def test_get_sub_agent_answer_parts__preserves_order__of_configs_not_matches() -
1661
1659
  result = get_sub_agent_answer_parts(answer=answer, display_config=config)
1662
1660
 
1663
1661
  # Assert
1664
- assert len(result) == 2
1662
+ assert len(result) == 4
1665
1663
  # Results follow config order, not text order
1666
1664
  assert result[0].matching_text == "first"
1667
- assert result[1].matching_text == "123"
1665
+ assert result[1].matching_text == "then"
1666
+ assert result[2].matching_text == "abc"
1667
+ assert result[3].matching_text == "123"
1668
1668
 
1669
1669
 
1670
1670
  @pytest.mark.ai
@@ -231,25 +231,6 @@ class _ToolManager(Generic[_ApiMode]):
231
231
  self,
232
232
  tool_calls: list[LanguageModelFunction],
233
233
  ) -> list[ToolCallResponse]:
234
- tool_calls = tool_calls
235
-
236
- tool_calls = self.filter_duplicate_tool_calls(
237
- tool_calls=tool_calls,
238
- )
239
- num_tool_calls = len(tool_calls)
240
-
241
- if num_tool_calls > self._config.max_tool_calls:
242
- self._logger.warning(
243
- (
244
- "Number of tool calls %s exceeds the allowed maximum of %s."
245
- "The tool calls will be reduced to the first %s."
246
- ),
247
- num_tool_calls,
248
- self._config.max_tool_calls,
249
- self._config.max_tool_calls,
250
- )
251
- tool_calls = tool_calls[: self._config.max_tool_calls]
252
-
253
234
  tool_call_responses = await self._execute_parallelized(tool_calls)
254
235
  return tool_call_responses
255
236
 
@@ -358,6 +339,22 @@ class _ToolManager(Generic[_ApiMode]):
358
339
  )
359
340
  return unique_tool_calls
360
341
 
342
+ def filter_tool_calls_by_max_tool_calls_allowed(
343
+ self, tool_calls: list[LanguageModelFunction]
344
+ ) -> list[LanguageModelFunction]:
345
+ if len(tool_calls) > self._config.max_tool_calls:
346
+ self._logger.warning(
347
+ (
348
+ "Number of tool calls %s exceeds the allowed maximum of %s."
349
+ "The tool calls will be reduced to the first %s."
350
+ ),
351
+ len(tool_calls),
352
+ self._config.max_tool_calls,
353
+ self._config.max_tool_calls,
354
+ )
355
+ return tool_calls[: self._config.max_tool_calls]
356
+ return tool_calls
357
+
361
358
  @overload
362
359
  def get_tool_by_name(
363
360
  self: "_ToolManager[Literal['completions']]", name: str
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: unique_toolkit
3
- Version: 1.31.0
3
+ Version: 1.31.2
4
4
  Summary:
5
5
  License: Proprietary
6
6
  Author: Cedric Klinkert
@@ -121,6 +121,12 @@ All notable changes to this project will be documented in this file.
121
121
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
122
122
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
123
123
 
124
+ ## [1.31.2] - 2025-11-27
125
+ - Added the function `filter_tool_calls_by_max_tool_calls_allowed` in `tool_manager` to limit the number of parallel tool calls permitted per loop iteration.
126
+
127
+ ## [1.31.1] - 2025-11-27
128
+ - Various fixes to sub agent answers.
129
+
124
130
  ## [1.31.0] - 2025-11-20
125
131
  - Adding model `litellm:anthropic-claude-opus-4-5` to `language_model/info.py`
126
132
 
@@ -82,12 +82,12 @@ unique_toolkit/agentic/tools/a2a/evaluation/evaluator.py,sha256=AJvXu0UJKHe72nRm
82
82
  unique_toolkit/agentic/tools/a2a/evaluation/summarization_user_message.j2,sha256=acP1YqD_sCy6DT0V2EIfhQTmaUKeqpeWNJ7RGgceo8I,271
83
83
  unique_toolkit/agentic/tools/a2a/manager.py,sha256=pk06UUXKQdIUY-PyykYiItubBjmIydOaqWvBBDwhMN4,1939
84
84
  unique_toolkit/agentic/tools/a2a/postprocessing/__init__.py,sha256=aVtUBPN7kDrqA6Bze34AbqQpcBBqpvfyJG-xF65w7R0,659
85
- unique_toolkit/agentic/tools/a2a/postprocessing/_display_utils.py,sha256=lpcyCTm3KKhaYMF4Wh7DHrkP_WAOkbXha5QJc7zy2nc,6539
85
+ unique_toolkit/agentic/tools/a2a/postprocessing/_display_utils.py,sha256=cM5EpIFKMNjMfjmOwFfxFxz3KF8iEtY6mdmt-UaJtW4,6879
86
86
  unique_toolkit/agentic/tools/a2a/postprocessing/_ref_utils.py,sha256=E3KybH9SX5oOkh14sSaIaLnht4DhKVrHAiUkoACNBsg,2430
87
- unique_toolkit/agentic/tools/a2a/postprocessing/config.py,sha256=U5N2y6ok14daNf_u-E1SXnV9v9DwpW4jL_U_3o8oJ7Q,2633
88
- unique_toolkit/agentic/tools/a2a/postprocessing/display.py,sha256=9t6zsVAYCyGXMHkxyH_XHdIbU0w0WBY1O08wjJvjoig,7850
87
+ unique_toolkit/agentic/tools/a2a/postprocessing/config.py,sha256=nYH1otBrVGHq1Y2i1KII3pb1IVE0efwmOwhOLLsv1qU,2841
88
+ unique_toolkit/agentic/tools/a2a/postprocessing/display.py,sha256=L1YFwAvnjNfbG2fxKSLk9wCFcfagq1gtQXucS_kZBeg,7172
89
89
  unique_toolkit/agentic/tools/a2a/postprocessing/references.py,sha256=DGiv8WXMjIwumI7tlpWRgV8wSxnE282ryxEf03fgck8,3465
90
- unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display_utils.py,sha256=mj2kR96ZzVIcgW3x-70oAjOLgYl5QreCIWBum1eQE_I,51989
90
+ unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display_utils.py,sha256=v4P0fi7iKKaU050WxxLGjXG4RUEw6r7kQCRlj9Vu59g,52065
91
91
  unique_toolkit/agentic/tools/a2a/postprocessing/test/test_ref_utils.py,sha256=u-eNrHjsRFcu4TmzkD5XfFrxvaIToB42YGyXZ-RpsR0,17830
92
92
  unique_toolkit/agentic/tools/a2a/prompts.py,sha256=0ILHL_RAcT04gFm2d470j4Gho7PoJXdCJy-bkZgf_wk,2401
93
93
  unique_toolkit/agentic/tools/a2a/response_watcher/__init__.py,sha256=fS8Lq49DZo5spMcP8QGTMWwSg80rYSr2pTfYDbssGYs,184
@@ -112,10 +112,9 @@ unique_toolkit/agentic/tools/openai_builtin/code_interpreter/service.py,sha256=X
112
112
  unique_toolkit/agentic/tools/openai_builtin/manager.py,sha256=QeDVgLfnCXrSmXI3b9bgQa9oyfQe_L15wa_YfhfNe9E,2633
113
113
  unique_toolkit/agentic/tools/schemas.py,sha256=TXshRvivr2hD-McXHumO0bp-Z0mz_GnAmQRiVjT59rU,5025
114
114
  unique_toolkit/agentic/tools/test/test_mcp_manager.py,sha256=VpB4k4Dh0lQWakilJMQSzO8sBXapuEC26cub_lorl-M,19221
115
- unique_toolkit/agentic/tools/test/test_tool_manager.py,sha256=eO7BSCRrw8iiPhrVL348Ch_yOobc6dUEqK_3OZ5heDw,48298
116
115
  unique_toolkit/agentic/tools/test/test_tool_progress_reporter.py,sha256=XHNezB8itj9KzpQgD0cwRtp2AgRUfUkQsHc3bTyPj6c,15801
117
116
  unique_toolkit/agentic/tools/tool.py,sha256=YmhGZ-NkPlgQ4X1hA6IVod10LNM5YErzbd0Zn6ANA98,6529
118
- unique_toolkit/agentic/tools/tool_manager.py,sha256=zOshUjQYuZe-_lN7DgPCT8UgXK-V34CzVBgF7f7XsWg,17549
117
+ unique_toolkit/agentic/tools/tool_manager.py,sha256=ZwzCcjzHYZgB1FxrpKRmarepfaZknIhLilxeyKoe4BY,17541
119
118
  unique_toolkit/agentic/tools/tool_progress_reporter.py,sha256=GaR0oqDUJZvBB9WCUVYYh0Zvs6U-LMygJCCrqPlitgA,10296
120
119
  unique_toolkit/agentic/tools/utils/__init__.py,sha256=s75sjY5nrJchjLGs3MwSIqhDW08fFXIaX7eRQjFIA4s,346
121
120
  unique_toolkit/agentic/tools/utils/execution/__init__.py,sha256=OHiKpqBnfhBiEQagKVWJsZlHv8smPp5OI4dFIexzibw,37
@@ -189,7 +188,7 @@ unique_toolkit/short_term_memory/service.py,sha256=5PeVBu1ZCAfyDb2HLVvlmqSbyzBBu
189
188
  unique_toolkit/smart_rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
190
189
  unique_toolkit/smart_rules/compile.py,sha256=Ozhh70qCn2yOzRWr9d8WmJeTo7AQurwd3tStgBMPFLA,1246
191
190
  unique_toolkit/test_utilities/events.py,sha256=_mwV2bs5iLjxS1ynDCjaIq-gjjKhXYCK-iy3dRfvO3g,6410
192
- unique_toolkit-1.31.0.dist-info/LICENSE,sha256=GlN8wHNdh53xwOPg44URnwag6TEolCjoq3YD_KrWgss,193
193
- unique_toolkit-1.31.0.dist-info/METADATA,sha256=fCVcgrZkBcRqyYl21qWou_M1kuGb7qngixPIdR9sjAo,44652
194
- unique_toolkit-1.31.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
195
- unique_toolkit-1.31.0.dist-info/RECORD,,
191
+ unique_toolkit-1.31.2.dist-info/LICENSE,sha256=GlN8wHNdh53xwOPg44URnwag6TEolCjoq3YD_KrWgss,193
192
+ unique_toolkit-1.31.2.dist-info/METADATA,sha256=FU5HB8zOBqLFAp77GYJskJr5r3PJefSRQs9zrvVDUxQ,44900
193
+ unique_toolkit-1.31.2.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
194
+ unique_toolkit-1.31.2.dist-info/RECORD,,