unique_toolkit 1.19.3__py3-none-any.whl → 1.20.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.

@@ -85,15 +85,39 @@ class PostprocessorManager:
85
85
  self._chat_service = chat_service
86
86
  self._postprocessors: list[Postprocessor | ResponsesApiPostprocessor] = []
87
87
 
88
+ # Allow to add postprocessors that should be run before or after the others.
89
+ self._first_postprocessor: Postprocessor | ResponsesApiPostprocessor | None = (
90
+ None
91
+ )
92
+ self._last_postprocessor: Postprocessor | ResponsesApiPostprocessor | None = (
93
+ None
94
+ )
95
+
88
96
  def add_postprocessor(
89
97
  self, postprocessor: Postprocessor | ResponsesApiPostprocessor
90
98
  ):
91
99
  self._postprocessors.append(postprocessor)
92
100
 
101
+ def set_first_postprocessor(
102
+ self, postprocessor: Postprocessor | ResponsesApiPostprocessor
103
+ ) -> None:
104
+ if self._first_postprocessor is not None:
105
+ raise ValueError("Cannot set first postprocessor if already set.")
106
+
107
+ self._first_postprocessor = postprocessor
108
+
109
+ def set_last_postprocessor(
110
+ self, postprocessor: Postprocessor | ResponsesApiPostprocessor
111
+ ) -> None:
112
+ if self._last_postprocessor is not None:
113
+ raise ValueError("Cannot set last postprocessor if already set.")
114
+
115
+ self._last_postprocessor = postprocessor
116
+
93
117
  def get_postprocessors(
94
118
  self, name: str
95
119
  ) -> list[Postprocessor | ResponsesApiPostprocessor]:
96
- return self._postprocessors
120
+ return self._get_all_postprocessors()
97
121
 
98
122
  async def run_postprocessors(
99
123
  self,
@@ -103,14 +127,7 @@ class PostprocessorManager:
103
127
  logger=self._logger,
104
128
  )
105
129
 
106
- if isinstance(loop_response, ResponsesLanguageModelStreamResponse):
107
- postprocessors = self._postprocessors
108
- else:
109
- postprocessors = [
110
- postprocessor
111
- for postprocessor in self._postprocessors
112
- if isinstance(postprocessor, Postprocessor)
113
- ]
130
+ postprocessors = self._get_valid_postprocessors_for_loop_response(loop_response)
114
131
 
115
132
  tasks = [
116
133
  task_executor.execute_async(
@@ -122,17 +139,14 @@ class PostprocessorManager:
122
139
  ]
123
140
  postprocessor_results = await asyncio.gather(*tasks)
124
141
 
125
- for postprocessor, result in zip(postprocessors, postprocessor_results):
126
- if not result.success:
127
- self._logger.warning(
128
- "Postprocessor %s failed to run.",
129
- postprocessor.get_name(),
130
- exc_info=result.exception,
131
- )
142
+ successful_postprocessors: list[Postprocessor | ResponsesApiPostprocessor] = []
143
+ for i in range(len(postprocessors)):
144
+ if postprocessor_results[i].success:
145
+ successful_postprocessors.append(postprocessors[i])
132
146
 
133
147
  modification_results = [
134
- postprocessor.apply_postprocessing_to_response(loop_response) # type: ignore
135
- for postprocessor in postprocessors
148
+ postprocessor.apply_postprocessing_to_response(loop_response) # type: ignore (checked in `get_valid_postprocessors_for_loop_response`)
149
+ for postprocessor in successful_postprocessors
136
150
  ]
137
151
 
138
152
  has_been_modified = any(modification_results)
@@ -155,6 +169,44 @@ class PostprocessorManager:
155
169
  self,
156
170
  text: str,
157
171
  ) -> str:
158
- for postprocessor in self._postprocessors:
172
+ for postprocessor in self._get_all_postprocessors():
159
173
  text = await postprocessor.remove_from_text(text)
160
174
  return text
175
+
176
+ def _get_all_postprocessors(
177
+ self,
178
+ ) -> list[Postprocessor | ResponsesApiPostprocessor]:
179
+ return [
180
+ postprocessor
181
+ for postprocessor in [
182
+ self._first_postprocessor,
183
+ *self._postprocessors,
184
+ self._last_postprocessor,
185
+ ]
186
+ if postprocessor is not None
187
+ ]
188
+
189
+ def _get_valid_postprocessors_for_loop_response(
190
+ self, loop_response: LanguageModelStreamResponse
191
+ ) -> list[Postprocessor | ResponsesApiPostprocessor]:
192
+ all_postprocessors = self._get_all_postprocessors()
193
+
194
+ postprocessors: list[Postprocessor | ResponsesApiPostprocessor] = []
195
+
196
+ if isinstance(loop_response, ResponsesLanguageModelStreamResponse):
197
+ """
198
+ All processore can be executed, since `ResponsesLanguageModelStreamResponse`
199
+ is a subclass of `LanguageModelStreamResponse`
200
+ """
201
+ postprocessors = all_postprocessors
202
+ else:
203
+ """
204
+ Cannot execute Responses API-specific postprocessors
205
+ """
206
+ postprocessors = [
207
+ postprocessor
208
+ for postprocessor in all_postprocessors
209
+ if isinstance(postprocessor, Postprocessor)
210
+ ]
211
+
212
+ return postprocessors
@@ -52,7 +52,7 @@ def _format_single_assessment_found(name: str, explanation: str) -> str:
52
52
  return _SingleAssessmentData(name=name, explanation=explanation).model_dump_json()
53
53
 
54
54
 
55
- @failsafe(failure_return_value=False)
55
+ @failsafe(failure_return_value=False, log_exceptions=False)
56
56
  def _is_single_assessment_found(value: str) -> bool:
57
57
  _ = _SingleAssessmentData.model_validate_json(value)
58
58
  return True
@@ -77,10 +77,17 @@ def _add_line_break(text: str, before: bool = True, after: bool = True) -> str:
77
77
  return _wrap_text(text, start_tag, end_tag)
78
78
 
79
79
 
80
+ def _prepare_title_template(
81
+ display_title_template: str, display_name_placeholder: str
82
+ ) -> str:
83
+ return display_title_template.replace("{}", "{%s}" % display_name_placeholder)
84
+
85
+
80
86
  def _get_display_template(
81
87
  mode: SubAgentResponseDisplayMode,
82
88
  add_quote_border: bool,
83
89
  add_block_border: bool,
90
+ display_title_template: str,
84
91
  answer_placeholder: str = "answer",
85
92
  assistant_id_placeholder: str = "assistant_id",
86
93
  display_name_placeholder: str = "display_name",
@@ -89,7 +96,9 @@ def _get_display_template(
89
96
  return ""
90
97
 
91
98
  assistant_id_placeholder = _wrap_hidden_div("{%s}" % assistant_id_placeholder)
92
- display_name_placeholder = _wrap_strong("{%s}" % display_name_placeholder)
99
+ title_template = _prepare_title_template(
100
+ display_title_template, display_name_placeholder
101
+ )
93
102
  template = _join_text_blocks(
94
103
  assistant_id_placeholder, "{%s}" % answer_placeholder, sep="\n\n"
95
104
  ) # Double line break is needed for markdown formatting
@@ -104,20 +113,14 @@ def _get_display_template(
104
113
  template = _wrap_with_details_tag(
105
114
  template,
106
115
  "open",
107
- display_name_placeholder,
116
+ title_template,
108
117
  )
109
118
  case SubAgentResponseDisplayMode.DETAILS_CLOSED:
110
- template = _wrap_with_details_tag(
111
- template, "closed", display_name_placeholder
112
- )
119
+ template = _wrap_with_details_tag(template, "closed", title_template)
113
120
  case SubAgentResponseDisplayMode.PLAIN:
114
- display_name_placeholder = _add_line_break(
115
- display_name_placeholder, before=False, after=True
116
- )
117
- template = _join_text_blocks(display_name_placeholder, template)
118
121
  # Add a hidden block border to seperate sub agent answers from the rest of the text.
119
122
  hidden_block_border = _wrap_hidden_div("sub_agent_answer_block")
120
- template = _join_text_blocks(template, hidden_block_border)
123
+ template = _join_text_blocks(title_template, template, hidden_block_border)
121
124
 
122
125
  if add_block_border:
123
126
  template = _wrap_with_block_border(template)
@@ -130,11 +133,13 @@ def _get_display_removal_re(
130
133
  mode: SubAgentResponseDisplayMode,
131
134
  add_quote_border: bool,
132
135
  add_block_border: bool,
136
+ display_title_template: str,
133
137
  ) -> re.Pattern[str]:
134
138
  template = _get_display_template(
135
139
  mode=mode,
136
140
  add_quote_border=add_quote_border,
137
141
  add_block_border=add_block_border,
142
+ display_title_template=display_title_template,
138
143
  )
139
144
 
140
145
  pattern = template.format(
@@ -147,6 +152,7 @@ def _get_display_removal_re(
147
152
  def _build_sub_agent_answer_display(
148
153
  display_name: str,
149
154
  display_mode: SubAgentResponseDisplayMode,
155
+ display_title_template: str,
150
156
  add_quote_border: bool,
151
157
  add_block_border: bool,
152
158
  answer: str,
@@ -156,6 +162,7 @@ def _build_sub_agent_answer_display(
156
162
  mode=display_mode,
157
163
  add_quote_border=add_quote_border,
158
164
  add_block_border=add_block_border,
165
+ display_title_template=display_title_template,
159
166
  )
160
167
  return template.format(
161
168
  display_name=display_name, answer=answer, assistant_id=assistant_id
@@ -168,11 +175,13 @@ def _remove_sub_agent_answer_from_text(
168
175
  add_block_border: bool,
169
176
  text: str,
170
177
  assistant_id: str,
178
+ display_title_template: str,
171
179
  ) -> str:
172
180
  pattern = _get_display_removal_re(
173
181
  assistant_id=assistant_id,
174
182
  mode=display_mode,
175
183
  add_quote_border=add_quote_border,
176
184
  add_block_border=add_block_border,
185
+ display_title_template=display_title_template,
177
186
  )
178
187
  return re.sub(pattern, "", text)
@@ -1,4 +1,5 @@
1
1
  from enum import StrEnum
2
+ from typing import Literal
2
3
 
3
4
  from pydantic import BaseModel, Field
4
5
 
@@ -31,3 +32,14 @@ class SubAgentDisplayConfig(BaseModel):
31
32
  default=False,
32
33
  description="If set, a block border is added around the sub agent response.",
33
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
+ )
@@ -116,7 +116,9 @@ class SubAgentResponsesPostprocessor(Postprocessor):
116
116
  list(displayed_sub_agent_infos.values()), existing_refs
117
117
  )
118
118
 
119
- answers = []
119
+ answers_displayed_before = []
120
+ answers_displayed_after = []
121
+
120
122
  for assistant_id in displayed_sub_agent_infos.keys():
121
123
  messages = self._assistant_id_to_tool_info[assistant_id]["responses"]
122
124
 
@@ -129,17 +131,23 @@ class SubAgentResponsesPostprocessor(Postprocessor):
129
131
  if len(messages) > 1:
130
132
  display_name += f" {sequence_number}"
131
133
 
132
- answers.append(
133
- _build_sub_agent_answer_display(
134
- display_name=display_name,
135
- assistant_id=assistant_id,
136
- display_mode=display_mode,
137
- answer=message["text"],
138
- add_quote_border=tool_info["display_config"].add_quote_border,
139
- add_block_border=tool_info["display_config"].add_block_border,
140
- )
134
+ answer = _build_sub_agent_answer_display(
135
+ display_name=display_name,
136
+ display_mode=display_mode,
137
+ add_quote_border=tool_info["display_config"].add_quote_border,
138
+ add_block_border=tool_info["display_config"].add_block_border,
139
+ answer=message["text"],
140
+ assistant_id=assistant_id,
141
+ display_title_template=tool_info[
142
+ "display_config"
143
+ ].display_title_template,
141
144
  )
142
145
 
146
+ if tool_info["display_config"].position == "before":
147
+ answers_displayed_before.append(answer)
148
+ else:
149
+ answers_displayed_after.append(answer)
150
+
143
151
  loop_response.message.references.extend(
144
152
  ContentReference(
145
153
  message_id=self._main_agent_message["id"],
@@ -152,13 +160,20 @@ class SubAgentResponsesPostprocessor(Postprocessor):
152
160
  for ref in message["references"]
153
161
  )
154
162
 
155
- if len(answers) > 0:
163
+ if len(answers_displayed_before) > 0:
156
164
  loop_response.message.text = (
157
- "<br>\n\n".join(answers)
165
+ "<br>\n\n".join(answers_displayed_before)
158
166
  + "<br>\n\n"
159
167
  + loop_response.message.text.strip()
160
168
  )
161
169
 
170
+ if len(answers_displayed_after) > 0:
171
+ loop_response.message.text = (
172
+ loop_response.message.text.strip()
173
+ + "<br>\n\n"
174
+ + "<br>\n\n".join(answers_displayed_after)
175
+ )
176
+
162
177
  return True
163
178
 
164
179
  @override
@@ -172,6 +187,7 @@ class SubAgentResponsesPostprocessor(Postprocessor):
172
187
  assistant_id=assistant_id,
173
188
  add_quote_border=display_config.add_quote_border,
174
189
  add_block_border=display_config.add_block_border,
190
+ display_title_template=display_config.display_title_template,
175
191
  )
176
192
  return text
177
193
 
@@ -379,7 +379,10 @@ def test_get_display_template__returns_empty__when_hidden_mode() -> None:
379
379
 
380
380
  # Act
381
381
  result = _get_display_template(
382
- mode=mode, add_quote_border=False, add_block_border=False
382
+ mode=mode,
383
+ add_quote_border=False,
384
+ add_block_border=False,
385
+ display_title_template="Answer from <strong>{}</strong>",
383
386
  )
384
387
 
385
388
  # Assert
@@ -403,7 +406,10 @@ def test_get_display_template__includes_placeholders__for_all_modes() -> None:
403
406
  for mode in modes:
404
407
  # Act
405
408
  result = _get_display_template(
406
- mode=mode, add_quote_border=False, add_block_border=False
409
+ mode=mode,
410
+ add_quote_border=False,
411
+ add_block_border=False,
412
+ display_title_template="Answer from <strong>{}</strong>",
407
413
  )
408
414
 
409
415
  # Assert
@@ -424,7 +430,10 @@ def test_get_display_template__wraps_assistant_id__as_hidden_div() -> None:
424
430
 
425
431
  # Act
426
432
  result = _get_display_template(
427
- mode=mode, add_quote_border=False, add_block_border=False
433
+ mode=mode,
434
+ add_quote_border=False,
435
+ add_block_border=False,
436
+ display_title_template="Answer from <strong>{}</strong>",
428
437
  )
429
438
 
430
439
  # Assert
@@ -444,7 +453,10 @@ def test_get_display_template__wraps_display_name__as_strong() -> None:
444
453
 
445
454
  # Act
446
455
  result = _get_display_template(
447
- mode=mode, add_quote_border=False, add_block_border=False
456
+ mode=mode,
457
+ add_quote_border=False,
458
+ add_block_border=False,
459
+ display_title_template="Answer from <strong>{}</strong>",
448
460
  )
449
461
 
450
462
  # Assert
@@ -465,7 +477,10 @@ def test_get_display_template__adds_details_open__when_details_open_mode() -> No
465
477
 
466
478
  # Act
467
479
  result = _get_display_template(
468
- mode=mode, add_quote_border=False, add_block_border=False
480
+ mode=mode,
481
+ add_quote_border=False,
482
+ add_block_border=False,
483
+ display_title_template="Answer from <strong>{}</strong>",
469
484
  )
470
485
 
471
486
  # Assert
@@ -487,7 +502,10 @@ def test_get_display_template__adds_details_closed__when_details_closed_mode() -
487
502
 
488
503
  # Act
489
504
  result = _get_display_template(
490
- mode=mode, add_quote_border=False, add_block_border=False
505
+ mode=mode,
506
+ add_quote_border=False,
507
+ add_block_border=False,
508
+ display_title_template="Answer from <strong>{}</strong>",
491
509
  )
492
510
 
493
511
  # Assert
@@ -510,7 +528,10 @@ def test_get_display_template__adds_line_break_after_name__in_plain_mode() -> No
510
528
 
511
529
  # Act
512
530
  result = _get_display_template(
513
- mode=mode, add_quote_border=False, add_block_border=False
531
+ mode=mode,
532
+ add_quote_border=False,
533
+ add_block_border=False,
534
+ display_title_template="Answer from <strong>{}</strong>",
514
535
  )
515
536
 
516
537
  # Assert
@@ -530,7 +551,10 @@ def test_get_display_template__adds_quote_border__when_flag_true() -> None:
530
551
 
531
552
  # Act
532
553
  result = _get_display_template(
533
- mode=mode, add_quote_border=True, add_block_border=False
554
+ mode=mode,
555
+ add_quote_border=True,
556
+ add_block_border=False,
557
+ display_title_template="Answer from <strong>{}</strong>",
534
558
  )
535
559
 
536
560
  # Assert
@@ -550,7 +574,10 @@ def test_get_display_template__adds_block_border__when_flag_true() -> None:
550
574
 
551
575
  # Act
552
576
  result = _get_display_template(
553
- mode=mode, add_quote_border=False, add_block_border=True
577
+ mode=mode,
578
+ add_quote_border=False,
579
+ add_block_border=True,
580
+ display_title_template="Answer from <strong>{}</strong>",
554
581
  )
555
582
 
556
583
  # Assert
@@ -570,7 +597,10 @@ def test_get_display_template__adds_both_borders__when_both_flags_true() -> None
570
597
 
571
598
  # Act
572
599
  result = _get_display_template(
573
- mode=mode, add_quote_border=True, add_block_border=True
600
+ mode=mode,
601
+ add_quote_border=True,
602
+ add_block_border=True,
603
+ display_title_template="Answer from <strong>{}</strong>",
574
604
  )
575
605
 
576
606
  # Assert
@@ -601,6 +631,7 @@ def test_get_display_removal_re__returns_pattern__for_plain_mode() -> None:
601
631
  mode=mode,
602
632
  add_quote_border=False,
603
633
  add_block_border=False,
634
+ display_title_template="Answer from <strong>{}</strong>",
604
635
  )
605
636
 
606
637
  # Assert
@@ -629,6 +660,7 @@ def test_get_display_removal_re__returns_pattern__for_details_modes() -> None:
629
660
  mode=mode,
630
661
  add_quote_border=False,
631
662
  add_block_border=False,
663
+ display_title_template="Answer from <strong>{}</strong>",
632
664
  )
633
665
 
634
666
  # Assert
@@ -653,6 +685,7 @@ def test_get_display_removal_re__includes_assistant_id__in_pattern() -> None:
653
685
  mode=mode,
654
686
  add_quote_border=False,
655
687
  add_block_border=False,
688
+ display_title_template="Answer from <strong>{}</strong>",
656
689
  )
657
690
 
658
691
  # Assert
@@ -676,6 +709,7 @@ def test_get_display_removal_re__has_capture_groups__for_answer_and_name() -> No
676
709
  mode=mode,
677
710
  add_quote_border=False,
678
711
  add_block_border=False,
712
+ display_title_template="Answer from <strong>{}</strong>",
679
713
  )
680
714
 
681
715
  # Assert
@@ -707,6 +741,7 @@ def test_build_sub_agent_answer_display__creates_html__for_plain_mode() -> None:
707
741
  add_block_border=False,
708
742
  answer=answer,
709
743
  assistant_id=assistant_id,
744
+ display_title_template="Answer from <strong>{}</strong>",
710
745
  )
711
746
 
712
747
  # Assert
@@ -738,6 +773,7 @@ def test_build_sub_agent_answer_display__creates_details__for_details_open() ->
738
773
  add_block_border=False,
739
774
  answer=answer,
740
775
  assistant_id=assistant_id,
776
+ display_title_template="Answer from <strong>{}</strong>",
741
777
  )
742
778
 
743
779
  # Assert
@@ -769,6 +805,7 @@ def test_build_sub_agent_answer_display__creates_details__for_details_closed() -
769
805
  add_block_border=False,
770
806
  answer=answer,
771
807
  assistant_id=assistant_id,
808
+ display_title_template="Answer from <strong>{}</strong>",
772
809
  )
773
810
 
774
811
  # Assert
@@ -800,6 +837,7 @@ def test_build_sub_agent_answer_display__returns_empty__for_hidden_mode() -> Non
800
837
  add_block_border=False,
801
838
  answer=answer,
802
839
  assistant_id=assistant_id,
840
+ display_title_template="Answer from <strong>{}</strong>",
803
841
  )
804
842
 
805
843
  # Assert
@@ -830,6 +868,7 @@ def test_remove_sub_agent_answer__removes_plain_display__from_text() -> None:
830
868
  add_block_border=False,
831
869
  answer=answer,
832
870
  assistant_id=assistant_id,
871
+ display_title_template="Answer from <strong>{}</strong>",
833
872
  )
834
873
 
835
874
  text_with_display = f"Before content\n{display}\nAfter content"
@@ -841,6 +880,7 @@ def test_remove_sub_agent_answer__removes_plain_display__from_text() -> None:
841
880
  add_block_border=False,
842
881
  text=text_with_display,
843
882
  assistant_id=assistant_id,
883
+ display_title_template="Answer from <strong>{}</strong>",
844
884
  )
845
885
 
846
886
  # Assert
@@ -871,6 +911,7 @@ def test_remove_sub_agent_answer__removes_details_open__from_text() -> None:
871
911
  add_block_border=False,
872
912
  answer=answer,
873
913
  assistant_id=assistant_id,
914
+ display_title_template="Answer from <strong>{}</strong>",
874
915
  )
875
916
 
876
917
  text_with_display = f"Start\n{display}\nEnd"
@@ -882,6 +923,7 @@ def test_remove_sub_agent_answer__removes_details_open__from_text() -> None:
882
923
  add_block_border=False,
883
924
  text=text_with_display,
884
925
  assistant_id=assistant_id,
926
+ display_title_template="Answer from <strong>{}</strong>",
885
927
  )
886
928
 
887
929
  # Assert
@@ -913,6 +955,7 @@ def test_remove_sub_agent_answer__removes_details_closed__from_text() -> None:
913
955
  add_block_border=False,
914
956
  answer=answer,
915
957
  assistant_id=assistant_id,
958
+ display_title_template="Answer from <strong>{}</strong>",
916
959
  )
917
960
 
918
961
  text_with_display = f"Beginning\n{display}\nEnding"
@@ -924,6 +967,7 @@ def test_remove_sub_agent_answer__removes_details_closed__from_text() -> None:
924
967
  add_block_border=False,
925
968
  text=text_with_display,
926
969
  assistant_id=assistant_id,
970
+ display_title_template="Answer from <strong>{}</strong>",
927
971
  )
928
972
 
929
973
  # Assert
@@ -955,6 +999,7 @@ def test_remove_sub_agent_answer__removes_with_quote_border__from_text() -> None
955
999
  add_block_border=False,
956
1000
  answer=answer,
957
1001
  assistant_id=assistant_id,
1002
+ display_title_template="Answer from <strong>{}</strong>",
958
1003
  )
959
1004
 
960
1005
  text_with_display = f"Before\n{display}\nAfter"
@@ -966,6 +1011,7 @@ def test_remove_sub_agent_answer__removes_with_quote_border__from_text() -> None
966
1011
  add_block_border=False,
967
1012
  text=text_with_display,
968
1013
  assistant_id=assistant_id,
1014
+ display_title_template="Answer from <strong>{}</strong>",
969
1015
  )
970
1016
 
971
1017
  # Assert
@@ -997,6 +1043,7 @@ def test_remove_sub_agent_answer__removes_with_block_border__from_text() -> None
997
1043
  add_block_border=True,
998
1044
  answer=answer,
999
1045
  assistant_id=assistant_id,
1046
+ display_title_template="Answer from <strong>{}</strong>",
1000
1047
  )
1001
1048
 
1002
1049
  text_with_display = f"Start\n{display}\nFinish"
@@ -1008,6 +1055,7 @@ def test_remove_sub_agent_answer__removes_with_block_border__from_text() -> None
1008
1055
  add_block_border=True,
1009
1056
  text=text_with_display,
1010
1057
  assistant_id=assistant_id,
1058
+ display_title_template="Answer from <strong>{}</strong>",
1011
1059
  )
1012
1060
 
1013
1061
  # Assert
@@ -1039,6 +1087,7 @@ def test_remove_sub_agent_answer__removes_with_both_borders__from_text() -> None
1039
1087
  add_block_border=True,
1040
1088
  answer=answer,
1041
1089
  assistant_id=assistant_id,
1090
+ display_title_template="Answer from <strong>{}</strong>",
1042
1091
  )
1043
1092
 
1044
1093
  text_with_display = f"Prefix\n{display}\nSuffix"
@@ -1050,6 +1099,7 @@ def test_remove_sub_agent_answer__removes_with_both_borders__from_text() -> None
1050
1099
  add_block_border=True,
1051
1100
  text=text_with_display,
1052
1101
  assistant_id=assistant_id,
1102
+ display_title_template="Answer from <strong>{}</strong>",
1053
1103
  )
1054
1104
 
1055
1105
  # Assert
@@ -1082,6 +1132,7 @@ def test_remove_sub_agent_answer__preserves_other_content__with_multiple_display
1082
1132
  add_block_border=False,
1083
1133
  answer="Answer from agent 1",
1084
1134
  assistant_id=assistant_id_1,
1135
+ display_title_template="Answer from <strong>{}</strong>",
1085
1136
  )
1086
1137
 
1087
1138
  display_2 = _build_sub_agent_answer_display(
@@ -1091,6 +1142,7 @@ def test_remove_sub_agent_answer__preserves_other_content__with_multiple_display
1091
1142
  add_block_border=False,
1092
1143
  answer="Answer from agent 2",
1093
1144
  assistant_id=assistant_id_2,
1145
+ display_title_template="Answer from <strong>{}</strong>",
1094
1146
  )
1095
1147
 
1096
1148
  text_with_displays = f"Start\n{display_1}\nMiddle\n{display_2}\nEnd"
@@ -1102,6 +1154,7 @@ def test_remove_sub_agent_answer__preserves_other_content__with_multiple_display
1102
1154
  add_block_border=False,
1103
1155
  text=text_with_displays,
1104
1156
  assistant_id=assistant_id_1,
1157
+ display_title_template="Answer from <strong>{}</strong>",
1105
1158
  )
1106
1159
 
1107
1160
  # Assert
@@ -1135,6 +1188,7 @@ def test_remove_sub_agent_answer__handles_multiline_answer__with_dotall_flag() -
1135
1188
  add_block_border=False,
1136
1189
  answer=answer,
1137
1190
  assistant_id=assistant_id,
1191
+ display_title_template="Answer from <strong>{}</strong>",
1138
1192
  )
1139
1193
 
1140
1194
  text_with_display = f"Before\n{display}\nAfter"
@@ -1146,6 +1200,7 @@ def test_remove_sub_agent_answer__handles_multiline_answer__with_dotall_flag() -
1146
1200
  add_block_border=False,
1147
1201
  text=text_with_display,
1148
1202
  assistant_id=assistant_id,
1203
+ display_title_template="Answer from <strong>{}</strong>",
1149
1204
  )
1150
1205
 
1151
1206
  # Assert
@@ -1179,6 +1234,7 @@ def test_remove_sub_agent_answer__handles_special_regex_chars__in_answer() -> No
1179
1234
  add_block_border=False,
1180
1235
  answer=answer,
1181
1236
  assistant_id=assistant_id,
1237
+ display_title_template="Answer from <strong>{}</strong>",
1182
1238
  )
1183
1239
 
1184
1240
  text_with_display = f"Start\n{display}\nEnd"
@@ -1190,6 +1246,7 @@ def test_remove_sub_agent_answer__handles_special_regex_chars__in_answer() -> No
1190
1246
  add_block_border=False,
1191
1247
  text=text_with_display,
1192
1248
  assistant_id=assistant_id,
1249
+ display_title_template="Answer from <strong>{}</strong>",
1193
1250
  )
1194
1251
 
1195
1252
  # Assert
@@ -1220,6 +1277,7 @@ def test_remove_sub_agent_answer__handles_empty_answer__successfully() -> None:
1220
1277
  add_block_border=False,
1221
1278
  answer=answer,
1222
1279
  assistant_id=assistant_id,
1280
+ display_title_template="Answer from <strong>{}</strong>",
1223
1281
  )
1224
1282
 
1225
1283
  text_with_display = f"Beginning\n{display}\nEnding"
@@ -1231,6 +1289,7 @@ def test_remove_sub_agent_answer__handles_empty_answer__successfully() -> None:
1231
1289
  add_block_border=False,
1232
1290
  text=text_with_display,
1233
1291
  assistant_id=assistant_id,
1292
+ display_title_template="Answer from <strong>{}</strong>",
1234
1293
  )
1235
1294
 
1236
1295
  # Assert
@@ -1259,6 +1318,7 @@ def test_remove_sub_agent_answer__no_op_when_assistant_not_found() -> None:
1259
1318
  add_block_border=False,
1260
1319
  answer="Present answer",
1261
1320
  assistant_id=assistant_id_present,
1321
+ display_title_template="Answer from <strong>{}</strong>",
1262
1322
  )
1263
1323
 
1264
1324
  text_with_display = f"Start\n{display}\nEnd"
@@ -1271,6 +1331,7 @@ def test_remove_sub_agent_answer__no_op_when_assistant_not_found() -> None:
1271
1331
  add_block_border=False,
1272
1332
  text=text_with_display,
1273
1333
  assistant_id=assistant_id_absent,
1334
+ display_title_template="Answer from <strong>{}</strong>",
1274
1335
  )
1275
1336
 
1276
1337
  # Assert
@@ -16,7 +16,7 @@ P = ParamSpec("P")
16
16
  R = TypeVar("R")
17
17
 
18
18
 
19
- logger = logging.getLogger(__name__)
19
+ _logger = logging.getLogger(__name__)
20
20
 
21
21
 
22
22
  class Result(Generic[R]):
@@ -87,7 +87,7 @@ class SafeTaskExecutor:
87
87
  self._ignored_exceptions = tuple(ignored_exceptions)
88
88
  self._log_exceptions = log_exceptions
89
89
  self._log_exc_info = log_exc_info
90
- self._logger = logger
90
+ self._logger = logger or _logger
91
91
 
92
92
  def execute(
93
93
  self, f: Callable[P, R], *args: P.args, **kwargs: P.kwargs
@@ -98,7 +98,9 @@ class SafeTaskExecutor:
98
98
  if isinstance(e, self._ignored_exceptions):
99
99
  raise e
100
100
  if self._log_exceptions:
101
- logger.error(f"Error in {f.__name__}: {e}", exc_info=self._log_exc_info)
101
+ self._logger.error(
102
+ f"Error in {f.__name__}: {e}", exc_info=self._log_exc_info
103
+ )
102
104
  return Result(False, exception=e)
103
105
 
104
106
  async def execute_async(
@@ -110,7 +112,9 @@ class SafeTaskExecutor:
110
112
  if isinstance(e, self._ignored_exceptions):
111
113
  raise e
112
114
  if self._log_exceptions:
113
- logger.error(f"Error in {f.__name__}: {e}", exc_info=self._log_exc_info)
115
+ self._logger.error(
116
+ f"Error in {f.__name__}: {e}", exc_info=self._log_exc_info
117
+ )
114
118
  return Result(False, exception=e)
115
119
 
116
120
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: unique_toolkit
3
- Version: 1.19.3
3
+ Version: 1.20.0
4
4
  Summary:
5
5
  License: Proprietary
6
6
  Author: Cedric Klinkert
@@ -119,6 +119,11 @@ 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
121
 
122
+ ## [1.20.0] - 2025-10-30
123
+ - Fix bug where async tasks executed with `SafeTaskExecutor` did not log exceptions.
124
+ - Add option to customize sub agent response display title.
125
+ - Add option to display sub agent responses after the main agent response.
126
+ - Add option to specify postprocessors to run before or after the others in the `PostprocessorManager`.
122
127
 
123
128
  ## [1.19.3] - 2025-10-29
124
129
  - More documentation on advanced rendering
@@ -134,6 +139,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
134
139
  ## [1.19.0] - 2025-10-28
135
140
  - Enable additional headers on openai and langchain client
136
141
 
142
+
137
143
  ## [1.18.1] - 2025-10-28
138
144
  - Fix bug where sub agent references were not properly displayed in the main agent response when the sub agent response was hidden.
139
145
 
@@ -47,7 +47,7 @@ unique_toolkit/agentic/history_manager/history_construction_with_contents.py,sha
47
47
  unique_toolkit/agentic/history_manager/history_manager.py,sha256=6V4D5-YJFCsl_WthGVGw0Eq4WobXFgkFiX2YUtdjOZw,9275
48
48
  unique_toolkit/agentic/history_manager/loop_token_reducer.py,sha256=4XUX2-yVBnaYthV8p0zj2scVBUdK_3IhxBgoNlrytyQ,18498
49
49
  unique_toolkit/agentic/history_manager/utils.py,sha256=VIn_UmcR3jHtpux0qp5lQQzczgAm8XYSeQiPo87jC3A,3143
50
- unique_toolkit/agentic/postprocessor/postprocessor_manager.py,sha256=Z6rMQjhD0x6uC4p1cdxbUVv3jO-31hZTyNE1SiGYIu8,5680
50
+ unique_toolkit/agentic/postprocessor/postprocessor_manager.py,sha256=s6HFhA61TE05aAay15NFTWI1JvdSlxmGpEVfpBbGFyM,7684
51
51
  unique_toolkit/agentic/reference_manager/reference_manager.py,sha256=x51CT0D8HHu2LzgXdHGy0leOYpjnsxVbPZ2nc28G9mA,4005
52
52
  unique_toolkit/agentic/responses_api/__init__.py,sha256=9WTO-ef7fGE9Y1QtZJFm8Q_jkwK8Srtl-HWvpAD2Wxs,668
53
53
  unique_toolkit/agentic/responses_api/postprocessors/code_display.py,sha256=qbrxXL_AQ3ufBOW2TuNgml7d8u6qY_WGriS5jyZlJlE,1902
@@ -61,16 +61,16 @@ unique_toolkit/agentic/tools/a2a/config.py,sha256=6diTTSiS2prY294LfYozB-db2wmJ6j
61
61
  unique_toolkit/agentic/tools/a2a/evaluation/__init__.py,sha256=_cR8uBwLbG7lyXoRskTpItzacgs4n23e2LeqClrytuc,354
62
62
  unique_toolkit/agentic/tools/a2a/evaluation/_utils.py,sha256=GtcPAMWkwGwJ--hBxn35ow9jN0VKYx8h2qMUXR8DCho,1877
63
63
  unique_toolkit/agentic/tools/a2a/evaluation/config.py,sha256=Ra5rzJArS3r8C7RTmzKB8cLEYh4w-u3pe_XLgul3GOA,2355
64
- unique_toolkit/agentic/tools/a2a/evaluation/evaluator.py,sha256=-XDqkC2fLDFMkl8KZIefb1fl9dvlwug-uzcWmhil0vk,9144
64
+ unique_toolkit/agentic/tools/a2a/evaluation/evaluator.py,sha256=CMSoJK2yLf2vvqm7r0n_fAX_Upma8xKjWEqVGcgu4qM,9166
65
65
  unique_toolkit/agentic/tools/a2a/evaluation/summarization_user_message.j2,sha256=acP1YqD_sCy6DT0V2EIfhQTmaUKeqpeWNJ7RGgceo8I,271
66
66
  unique_toolkit/agentic/tools/a2a/manager.py,sha256=FkO9jY7o8Td0t-HBkkatmxwhJGSJXmYkFYKFhPdbpMo,1674
67
67
  unique_toolkit/agentic/tools/a2a/postprocessing/__init__.py,sha256=R90CSecxJrKH7TbwiMYPyTwsXUUmouL8fbEUlL4ee9Q,362
68
- unique_toolkit/agentic/tools/a2a/postprocessing/_display.py,sha256=rdWk-6M6V27v7G3dqaaDj1vXcgsFvDrHswFuSKQfd2I,5186
68
+ unique_toolkit/agentic/tools/a2a/postprocessing/_display.py,sha256=iSHgcHuz3ryBZajgSf_uqYI2DpC801fxLdU1Ym4qGCw,5452
69
69
  unique_toolkit/agentic/tools/a2a/postprocessing/_utils.py,sha256=JsWwylR2Ao_L0wk1UlhqeN2fTxPnrbhoi1klYHVBnLk,750
70
- unique_toolkit/agentic/tools/a2a/postprocessing/config.py,sha256=hqo3vQZX63yXoLT7wN13vf0rC7qKiozKKfdkicYCQno,1061
71
- unique_toolkit/agentic/tools/a2a/postprocessing/postprocessor.py,sha256=MMy3NeN4koHWmamaXvsEnrRbsh7XBaWhD0PRcr1i7C4,11931
70
+ unique_toolkit/agentic/tools/a2a/postprocessing/config.py,sha256=XhOeGu2qi_amz4VY2qF1ZGYQ1frYeVmVtAWYaKskgfY,1582
71
+ unique_toolkit/agentic/tools/a2a/postprocessing/postprocessor.py,sha256=VkQDKEEYhNu0ztbXHizbKLeXIADEdFQT4IzBGYY8svg,12623
72
72
  unique_toolkit/agentic/tools/a2a/postprocessing/test/test_consolidate_references.py,sha256=HvisnbhPUwuR9uELGunxLjNHvCXheoD5eW52L0tvUXk,26789
73
- unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display.py,sha256=Hn2GOsVhpCkpFV9Asvy_IyiYvCd88rxDzi0E_zt2kWc,37553
73
+ unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display.py,sha256=ISuLZ2ZDT5IPyk0U5UBubsN6jlprNuVvBiAVm9R9wOg,40445
74
74
  unique_toolkit/agentic/tools/a2a/postprocessing/test/test_postprocessor_reference_functions.py,sha256=GxSkkY-Xgd61Bk8sIRfEtkT9hqL1VgPLWrq-6XoB0rA,11360
75
75
  unique_toolkit/agentic/tools/a2a/prompts.py,sha256=0ILHL_RAcT04gFm2d470j4Gho7PoJXdCJy-bkZgf_wk,2401
76
76
  unique_toolkit/agentic/tools/a2a/tool/__init__.py,sha256=JIJKZBTLTA39OWhxoUd6uairxmqINur1Ex6iXDk9ef8,197
@@ -99,7 +99,7 @@ unique_toolkit/agentic/tools/tool_manager.py,sha256=DtxJobe_7QKFe6CjnMhCP-mnKO6M
99
99
  unique_toolkit/agentic/tools/tool_progress_reporter.py,sha256=ixud9VoHey1vlU1t86cW0-WTvyTwMxNSWBon8I11SUk,7955
100
100
  unique_toolkit/agentic/tools/utils/__init__.py,sha256=s75sjY5nrJchjLGs3MwSIqhDW08fFXIaX7eRQjFIA4s,346
101
101
  unique_toolkit/agentic/tools/utils/execution/__init__.py,sha256=OHiKpqBnfhBiEQagKVWJsZlHv8smPp5OI4dFIexzibw,37
102
- unique_toolkit/agentic/tools/utils/execution/execution.py,sha256=vjG2Y6awsGNtlvyQAGCTthQ5thWHYnn-vzZXaYLb3QE,7922
102
+ unique_toolkit/agentic/tools/utils/execution/execution.py,sha256=ocPGGfUwa851207HNTLYiBJ1pNzJp4VhMZ49OPP33gU,8022
103
103
  unique_toolkit/agentic/tools/utils/source_handling/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
104
104
  unique_toolkit/agentic/tools/utils/source_handling/schema.py,sha256=iHBKuks6tUy8tvian4Pd0B6_-8__SehVVNcxIUAUjEA,882
105
105
  unique_toolkit/agentic/tools/utils/source_handling/source_formatting.py,sha256=uZ0QXqrPWgId3ZA67dvjHQ6xrW491LK1xxx_sVJmFHg,9160
@@ -166,7 +166,7 @@ unique_toolkit/short_term_memory/service.py,sha256=5PeVBu1ZCAfyDb2HLVvlmqSbyzBBu
166
166
  unique_toolkit/smart_rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
167
167
  unique_toolkit/smart_rules/compile.py,sha256=Ozhh70qCn2yOzRWr9d8WmJeTo7AQurwd3tStgBMPFLA,1246
168
168
  unique_toolkit/test_utilities/events.py,sha256=_mwV2bs5iLjxS1ynDCjaIq-gjjKhXYCK-iy3dRfvO3g,6410
169
- unique_toolkit-1.19.3.dist-info/LICENSE,sha256=GlN8wHNdh53xwOPg44URnwag6TEolCjoq3YD_KrWgss,193
170
- unique_toolkit-1.19.3.dist-info/METADATA,sha256=kcF3i4FpSU1Dnp8OTmkjqWtIw6uyIwMZukexyeyvjcU,39142
171
- unique_toolkit-1.19.3.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
172
- unique_toolkit-1.19.3.dist-info/RECORD,,
169
+ unique_toolkit-1.20.0.dist-info/LICENSE,sha256=GlN8wHNdh53xwOPg44URnwag6TEolCjoq3YD_KrWgss,193
170
+ unique_toolkit-1.20.0.dist-info/METADATA,sha256=clAfPiA8yIUHHPqoIS3JcmZJR_SJGj53uH0UTN_RVwQ,39492
171
+ unique_toolkit-1.20.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
172
+ unique_toolkit-1.20.0.dist-info/RECORD,,