unique_toolkit 1.20.0__py3-none-any.whl → 1.21.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.

@@ -7,6 +7,7 @@ from unique_toolkit.agentic.tools.tool_progress_reporter import (
7
7
  ProgressState,
8
8
  ToolExecutionStatus,
9
9
  ToolProgressReporter,
10
+ ToolProgressReporterConfig,
10
11
  ToolWithToolProgressReporter,
11
12
  track_tool_progress,
12
13
  )
@@ -203,3 +204,226 @@ class TestToolProgressDecorator:
203
204
 
204
205
  status = tool_progress_reporter.tool_statuses[tool_call.id]
205
206
  assert status.state == ProgressState.FAILED
207
+
208
+
209
+ class TestToolProgressReporterConfig:
210
+ """Tests for ToolProgressReporterConfig and custom display configuration."""
211
+
212
+ @pytest.mark.ai
213
+ @pytest.mark.asyncio
214
+ async def test_config__uses_default_templates__when_no_config_provided(
215
+ self, chat_service, tool_call
216
+ ) -> None:
217
+ """
218
+ Purpose: Verify that default state-to-display templates are used when no config is provided.
219
+ Why this matters: Ensures backward compatibility and default behavior.
220
+ Setup summary: Create reporter without config, add status, verify default template is used.
221
+ """
222
+ # Arrange
223
+ reporter = ToolProgressReporter(chat_service)
224
+
225
+ # Act
226
+ await reporter.notify_from_tool_call(
227
+ tool_call=tool_call,
228
+ name="Test Tool",
229
+ message="Processing data",
230
+ state=ProgressState.RUNNING,
231
+ )
232
+
233
+ # Assert
234
+ assert tool_call.id in reporter.tool_statuses
235
+ chat_service.modify_assistant_message_async.assert_called()
236
+ call_args = chat_service.modify_assistant_message_async.call_args
237
+ content = call_args.kwargs["content"]
238
+ assert "Test Tool" in content
239
+ assert "🟡" in content # Default emoji for RUNNING state
240
+ assert "Processing data" in content
241
+
242
+ @pytest.mark.ai
243
+ @pytest.mark.asyncio
244
+ async def test_config__uses_custom_templates__when_config_provided(
245
+ self, chat_service, tool_call
246
+ ) -> None:
247
+ """
248
+ Purpose: Verify that custom templates are used when provided via config.
249
+ Why this matters: Enables customization of progress display format.
250
+ Setup summary: Create reporter with custom template, verify custom format is used.
251
+ """
252
+ # Arrange
253
+ custom_config = ToolProgressReporterConfig(
254
+ state_to_display_template={
255
+ ProgressState.RUNNING: "⏳ {tool_name}: {message}",
256
+ ProgressState.FINISHED: "✅ {tool_name}: {message}",
257
+ }
258
+ )
259
+ reporter = ToolProgressReporter(chat_service, config=custom_config)
260
+
261
+ # Act
262
+ await reporter.notify_from_tool_call(
263
+ tool_call=tool_call,
264
+ name="My Tool",
265
+ message="Working on it",
266
+ state=ProgressState.RUNNING,
267
+ )
268
+
269
+ # Assert
270
+ chat_service.modify_assistant_message_async.assert_called()
271
+ call_args = chat_service.modify_assistant_message_async.call_args
272
+ content = call_args.kwargs["content"]
273
+ assert "⏳ My Tool: Working on it" in content
274
+ assert "🟡" not in content # Default emoji should not appear
275
+
276
+ @pytest.mark.ai
277
+ @pytest.mark.asyncio
278
+ async def test_config__skips_states_not_in_template__when_state_excluded(
279
+ self, chat_service, tool_call
280
+ ) -> None:
281
+ """
282
+ Purpose: Verify that states not in the config mapping are not displayed.
283
+ Why this matters: Allows selective display of only certain states (e.g., hide STARTED).
284
+ Setup summary: Create config excluding RUNNING state, verify message is not displayed.
285
+ """
286
+ # Arrange
287
+ custom_config = ToolProgressReporterConfig(
288
+ state_to_display_template={
289
+ ProgressState.FINISHED: "✅ {tool_name}: {message}",
290
+ # RUNNING state is intentionally excluded
291
+ }
292
+ )
293
+ reporter = ToolProgressReporter(chat_service, config=custom_config)
294
+
295
+ # Act
296
+ await reporter.notify_from_tool_call(
297
+ tool_call=tool_call,
298
+ name="Test Tool",
299
+ message="Processing",
300
+ state=ProgressState.RUNNING,
301
+ )
302
+
303
+ # Assert
304
+ chat_service.modify_assistant_message_async.assert_called()
305
+ call_args = chat_service.modify_assistant_message_async.call_args
306
+ content = call_args.kwargs["content"]
307
+ # Content should not contain the message since RUNNING is excluded
308
+ assert "Processing" not in content
309
+ assert "Test Tool" not in content
310
+
311
+ @pytest.mark.ai
312
+ @pytest.mark.asyncio
313
+ async def test_config__formats_placeholders_correctly__with_multiple_tools(
314
+ self, chat_service
315
+ ) -> None:
316
+ """
317
+ Purpose: Verify that {tool_name} and {message} placeholders are replaced correctly for multiple tools.
318
+ Why this matters: Ensures template formatting works correctly in multi-tool scenarios.
319
+ Setup summary: Add multiple tool statuses with different names/messages, verify formatting.
320
+ """
321
+ # Arrange
322
+ custom_config = ToolProgressReporterConfig(
323
+ state_to_display_template={
324
+ ProgressState.RUNNING: "▶️ {tool_name} - {message}",
325
+ ProgressState.FINISHED: "✓ {tool_name} - {message}",
326
+ }
327
+ )
328
+ reporter = ToolProgressReporter(chat_service, config=custom_config)
329
+ tool_call_1 = LanguageModelFunction(id="tool_1", name="search")
330
+ tool_call_2 = LanguageModelFunction(id="tool_2", name="analyze")
331
+
332
+ # Act
333
+ await reporter.notify_from_tool_call(
334
+ tool_call=tool_call_1,
335
+ name="Search Tool",
336
+ message="Searching database",
337
+ state=ProgressState.RUNNING,
338
+ )
339
+ await reporter.notify_from_tool_call(
340
+ tool_call=tool_call_2,
341
+ name="Analysis Tool",
342
+ message="Analyzing results",
343
+ state=ProgressState.FINISHED,
344
+ )
345
+
346
+ # Assert
347
+ call_args = chat_service.modify_assistant_message_async.call_args
348
+ content = call_args.kwargs["content"]
349
+ assert "▶️ Search Tool - Searching database" in content
350
+ assert "✓ Analysis Tool - Analyzing results" in content
351
+
352
+ @pytest.mark.ai
353
+ @pytest.mark.asyncio
354
+ async def test_config__shows_only_finished_state__when_only_finished_configured(
355
+ self, chat_service, tool_call
356
+ ) -> None:
357
+ """
358
+ Purpose: Verify selective state display shows only FINISHED when it's the only state configured.
359
+ Why this matters: Use case where user only wants final results, not intermediate steps.
360
+ Setup summary: Configure only FINISHED state, send STARTED and FINISHED, verify only FINISHED appears.
361
+ """
362
+ # Arrange
363
+ custom_config = ToolProgressReporterConfig(
364
+ state_to_display_template={
365
+ ProgressState.FINISHED: "Done: {tool_name} - {message}",
366
+ }
367
+ )
368
+ reporter = ToolProgressReporter(chat_service, config=custom_config)
369
+
370
+ # Act - Send STARTED state (should not appear)
371
+ await reporter.notify_from_tool_call(
372
+ tool_call=tool_call,
373
+ name="Test Tool",
374
+ message="Starting",
375
+ state=ProgressState.STARTED,
376
+ )
377
+
378
+ # Get first call content
379
+ first_call_args = chat_service.modify_assistant_message_async.call_args
380
+ first_content = first_call_args.kwargs["content"]
381
+
382
+ # Act - Update to FINISHED state (should appear)
383
+ await reporter.notify_from_tool_call(
384
+ tool_call=tool_call,
385
+ name="Test Tool",
386
+ message="Completed successfully",
387
+ state=ProgressState.FINISHED,
388
+ )
389
+
390
+ # Assert
391
+ final_call_args = chat_service.modify_assistant_message_async.call_args
392
+ final_content = final_call_args.kwargs["content"]
393
+
394
+ # STARTED state should not appear in first call
395
+ assert "Starting" not in first_content
396
+
397
+ # FINISHED state should appear in final call
398
+ assert "Done: Test Tool - Completed successfully" in final_content
399
+
400
+ @pytest.mark.ai
401
+ @pytest.mark.asyncio
402
+ async def test_config__handles_empty_template_dict__when_all_states_excluded(
403
+ self, chat_service, tool_call
404
+ ) -> None:
405
+ """
406
+ Purpose: Verify that an empty template dict results in no messages being displayed.
407
+ Why this matters: Edge case handling and allows disabling all progress display.
408
+ Setup summary: Create config with empty dict, verify no tool messages appear.
409
+ """
410
+ # Arrange
411
+ custom_config = ToolProgressReporterConfig(state_to_display_template={})
412
+ reporter = ToolProgressReporter(chat_service, config=custom_config)
413
+
414
+ # Act
415
+ await reporter.notify_from_tool_call(
416
+ tool_call=tool_call,
417
+ name="Test Tool",
418
+ message="Processing",
419
+ state=ProgressState.RUNNING,
420
+ )
421
+
422
+ # Assert
423
+ chat_service.modify_assistant_message_async.assert_called()
424
+ call_args = chat_service.modify_assistant_message_async.call_args
425
+ content = call_args.kwargs["content"]
426
+
427
+ # Should only have the progress start text and newlines, no actual messages
428
+ assert "Test Tool" not in content
429
+ assert "Processing" not in content
@@ -1,11 +1,12 @@
1
1
  import re
2
2
  from datetime import datetime
3
- from enum import Enum
3
+ from enum import StrEnum
4
4
  from functools import wraps
5
5
  from typing import Protocol
6
6
 
7
- from pydantic import BaseModel
7
+ from pydantic import BaseModel, Field
8
8
 
9
+ from unique_toolkit._common.pydantic_helpers import get_configuration_dict
9
10
  from unique_toolkit.chat.service import ChatService
10
11
  from unique_toolkit.content.schemas import ContentReference
11
12
  from unique_toolkit.language_model.schemas import (
@@ -17,11 +18,11 @@ ARROW = "→ "
17
18
  DUMMY_REFERENCE_PLACEHOLDER = "<sup></sup>"
18
19
 
19
20
 
20
- class ProgressState(Enum):
21
- STARTED = ""
22
- RUNNING = "🟡"
23
- FAILED = "🔴"
24
- FINISHED = "🟢"
21
+ class ProgressState(StrEnum):
22
+ STARTED = "started"
23
+ RUNNING = "running"
24
+ FAILED = "failed"
25
+ FINISHED = "finished"
25
26
 
26
27
 
27
28
  class ToolExecutionStatus(BaseModel):
@@ -29,15 +30,56 @@ class ToolExecutionStatus(BaseModel):
29
30
  message: str
30
31
  state: ProgressState
31
32
  references: list[ContentReference] = []
32
- timestamp: datetime = datetime.now()
33
+ timestamp: datetime = Field(default_factory=datetime.now)
34
+
35
+
36
+ _DEFAULT_STATE_TO_DISPLAY_TEMPLATE = {
37
+ ProgressState.FINISHED: "{arrow}**{{tool_name}}** 🟢: {{message}}".format(
38
+ arrow=ARROW
39
+ ),
40
+ ProgressState.RUNNING: "{arrow}**{{tool_name}}** 🟡: {{message}}".format(
41
+ arrow=ARROW
42
+ ),
43
+ ProgressState.FAILED: "{arrow}**{{tool_name}}** 🔴: {{message}}".format(
44
+ arrow=ARROW
45
+ ),
46
+ ProgressState.STARTED: "{arrow}**{{tool_name}}** ⚪: {{message}}".format(
47
+ arrow=ARROW
48
+ ),
49
+ }
50
+
51
+
52
+ state_to_display_template_description = """
53
+ A mapping progress states to display templates.
54
+ The display template is a string that will be used to display the progress status.
55
+ The template can contain the following placeholders:
56
+ - `{tool_name}`: The name of the tool
57
+ - `{message}`: The message to display (sent by the tool)
58
+
59
+ If a state is not present in the mapping, then updates for that state will not be displayed.
60
+ """.strip()
61
+
62
+
63
+ class ToolProgressReporterConfig(BaseModel):
64
+ model_config = get_configuration_dict()
65
+
66
+ state_to_display_template: dict[ProgressState, str] = Field(
67
+ default=_DEFAULT_STATE_TO_DISPLAY_TEMPLATE,
68
+ description=state_to_display_template_description,
69
+ )
33
70
 
34
71
 
35
72
  class ToolProgressReporter:
36
- def __init__(self, chat_service: ChatService):
73
+ def __init__(
74
+ self,
75
+ chat_service: ChatService,
76
+ config: ToolProgressReporterConfig | None = None,
77
+ ):
37
78
  self.chat_service = chat_service
38
79
  self.tool_statuses: dict[str, ToolExecutionStatus] = {}
39
80
  self._progress_start_text = ""
40
81
  self._requires_new_assistant_message = False
82
+ self._config = config or ToolProgressReporterConfig()
41
83
 
42
84
  @property
43
85
  def requires_new_assistant_message(self):
@@ -77,16 +119,13 @@ class ToolProgressReporter:
77
119
  references (list[ContentReference], optional): List of content references. Defaults to [].
78
120
  requires_new_assistant_message (bool, optional): Whether a new assistant message is needed when tool call is finished.
79
121
  Defaults to False. If yes, the agentic steps will remain in chat history and will be overwritten by the stream response.
80
-
81
- Raises:
82
- AssertionError: If tool_call.id is None
83
122
  """
84
- assert tool_call.id is not None
85
123
  self.tool_statuses[tool_call.id] = ToolExecutionStatus(
86
124
  name=name,
87
125
  message=message,
88
126
  state=state,
89
127
  references=references,
128
+ timestamp=self._get_timestamp_for_tool_call(tool_call),
90
129
  )
91
130
  self.requires_new_assistant_message = (
92
131
  self.requires_new_assistant_message or requires_new_assistant_message
@@ -103,7 +142,11 @@ class ToolProgressReporter:
103
142
  references = self._correct_reference_sequence(references, start_number)
104
143
  all_references.extend(references)
105
144
 
106
- messages.append(f"{ARROW}**{item.name} {item.state.value}**: {message}")
145
+ display_message = self._get_tool_status_display_message(
146
+ name=item.name, message=message, state=item.state
147
+ )
148
+ if display_message is not None:
149
+ messages.append(display_message)
107
150
 
108
151
  await self.chat_service.modify_assistant_message_async(
109
152
  content=self._progress_start_text + "\n\n" + "\n\n".join(messages),
@@ -130,6 +173,28 @@ class ToolProgressReporter:
130
173
  reference.sequence_number = i
131
174
  return references
132
175
 
176
+ def _get_timestamp_for_tool_call(
177
+ self, tool_call: LanguageModelFunction
178
+ ) -> datetime:
179
+ """
180
+ Keep the same timestamp if the tool call is already in the statuses.
181
+ This ensures the display order stays consistent.
182
+ """
183
+ if tool_call.id in self.tool_statuses:
184
+ return self.tool_statuses[tool_call.id].timestamp
185
+
186
+ return datetime.now()
187
+
188
+ def _get_tool_status_display_message(
189
+ self, name: str, message: str, state: ProgressState
190
+ ) -> str | None:
191
+ if state in self._config.state_to_display_template:
192
+ return self._config.state_to_display_template[state].format(
193
+ tool_name=name,
194
+ message=message,
195
+ )
196
+ return None
197
+
133
198
 
134
199
  class ToolWithToolProgressReporter(Protocol):
135
200
  tool_progress_reporter: ToolProgressReporter
@@ -83,22 +83,21 @@ class LanguageModelStreamResponseMessage(BaseModel):
83
83
  class LanguageModelFunction(BaseModel):
84
84
  model_config = model_config
85
85
 
86
- id: str | None = None
86
+ id: str = Field(default_factory=lambda: uuid4().hex)
87
87
  name: str
88
- arguments: dict[str, Any] | str | None = None # type: ignore
88
+ arguments: dict[str, Any] | None = None
89
89
 
90
90
  @field_validator("arguments", mode="before")
91
- def set_arguments(cls, value):
91
+ def set_arguments(cls, value: Any) -> Any:
92
92
  if isinstance(value, str):
93
93
  return json.loads(value)
94
94
  return value
95
95
 
96
96
  @field_validator("id", mode="before")
97
- def randomize_id(cls, value):
98
- if not value:
97
+ def randomize_id(cls, value: Any) -> Any:
98
+ if value is None or value == "":
99
99
  return uuid4().hex
100
- else:
101
- return value
100
+ return value
102
101
 
103
102
  @model_serializer()
104
103
  def serialize_model(self):
@@ -108,7 +107,7 @@ class LanguageModelFunction(BaseModel):
108
107
  seralization["arguments"] = json.dumps(self.arguments)
109
108
  return seralization
110
109
 
111
- def __eq__(self, other: Self) -> bool:
110
+ def __eq__(self, other: object) -> bool:
112
111
  """Compare two tool calls based on name and arguments."""
113
112
  if not isinstance(other, LanguageModelFunction):
114
113
  return False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: unique_toolkit
3
- Version: 1.20.0
3
+ Version: 1.21.0
4
4
  Summary:
5
5
  License: Proprietary
6
6
  Author: Cedric Klinkert
@@ -119,6 +119,12 @@ 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.21.0] - 2025-10-30
123
+ - Add option to customize the display of tool progress statuses.
124
+
125
+ ## [1.20.1] - 2025-10-30
126
+ - Fix typing issues in `LanguageModelFunction`.
127
+
122
128
  ## [1.20.0] - 2025-10-30
123
129
  - Fix bug where async tasks executed with `SafeTaskExecutor` did not log exceptions.
124
130
  - Add option to customize sub agent response display title.
@@ -93,10 +93,10 @@ unique_toolkit/agentic/tools/openai_builtin/code_interpreter/service.py,sha256=R
93
93
  unique_toolkit/agentic/tools/openai_builtin/manager.py,sha256=kU4wGit9AnDbkijB7LJEHcGXG8UeTBhiZh4a7lxTGA8,2246
94
94
  unique_toolkit/agentic/tools/schemas.py,sha256=0ZR8xCdGj1sEdPE0lfTIG2uSR5zqWoprUa3Seqez4C8,4837
95
95
  unique_toolkit/agentic/tools/test/test_mcp_manager.py,sha256=8j5fNQf3qELOcQ0m5fd8OkIFgunL5ILgdWzYD_Ccg1I,18816
96
- unique_toolkit/agentic/tools/test/test_tool_progress_reporter.py,sha256=dod5QPqgGUInVAGXAbsAKNTEypIi6pUEWhDbJr9YfUU,6307
96
+ unique_toolkit/agentic/tools/test/test_tool_progress_reporter.py,sha256=lZr6QnEn8cuBCAe_a1R1iKX8tL2PRL_p2-sCTBfhrvw,15162
97
97
  unique_toolkit/agentic/tools/tool.py,sha256=mFuxc3h4sghClDO8OVL2-6kifiHQ-U7JMYGUyXqTYLE,6338
98
98
  unique_toolkit/agentic/tools/tool_manager.py,sha256=DtxJobe_7QKFe6CjnMhCP-mnKO6MjnZeDXsO3jBoC9w,16283
99
- unique_toolkit/agentic/tools/tool_progress_reporter.py,sha256=ixud9VoHey1vlU1t86cW0-WTvyTwMxNSWBon8I11SUk,7955
99
+ unique_toolkit/agentic/tools/tool_progress_reporter.py,sha256=I3q2SP1c_fwqU7qQLSi-_S4eEyOB8Qmjzf3wN3AfLVI,10270
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
102
  unique_toolkit/agentic/tools/utils/execution/execution.py,sha256=ocPGGfUwa851207HNTLYiBJ1pNzJp4VhMZ49OPP33gU,8022
@@ -151,7 +151,7 @@ unique_toolkit/language_model/functions.py,sha256=LGX3rR-XjkB-R520jp4w_Azgqf7BsI
151
151
  unique_toolkit/language_model/infos.py,sha256=oGbI9kA1jW9SdUUsWuSISD9O5Zm09PIzDIWXDyAnhzA,62649
152
152
  unique_toolkit/language_model/prompt.py,sha256=JSawaLjQg3VR-E2fK8engFyJnNdk21zaO8pPIodzN4Q,3991
153
153
  unique_toolkit/language_model/reference.py,sha256=nkX2VFz-IrUz8yqyc3G5jUMNwrNpxITBrMEKkbqqYoI,8583
154
- unique_toolkit/language_model/schemas.py,sha256=oHcJgmNSGpGW6ygjWvEB9iYaHgx250-Mtm-olSSJ-Ek,23760
154
+ unique_toolkit/language_model/schemas.py,sha256=rXEc6lUd5T-32yKFsIM7WYzDdtObweHduR2dKjGCLko,23796
155
155
  unique_toolkit/language_model/service.py,sha256=JkYGtCug8POQskTv_aoYkzTMOaPCWRM94y73o3bUttQ,12019
156
156
  unique_toolkit/language_model/utils.py,sha256=bPQ4l6_YO71w-zaIPanUUmtbXC1_hCvLK0tAFc3VCRc,1902
157
157
  unique_toolkit/protocols/support.py,sha256=ZEnbQL5w2-T_1AeM8OHycZJ3qbdfVI1nXe0nL9esQEw,5544
@@ -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.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,,
169
+ unique_toolkit-1.21.0.dist-info/LICENSE,sha256=GlN8wHNdh53xwOPg44URnwag6TEolCjoq3YD_KrWgss,193
170
+ unique_toolkit-1.21.0.dist-info/METADATA,sha256=Sho8NZxo-o5pg3Y3sd0wxJfy95QmPep8hV_VJa7n2DE,39657
171
+ unique_toolkit-1.21.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
172
+ unique_toolkit-1.21.0.dist-info/RECORD,,