unique_toolkit 1.20.1__py3-none-any.whl → 1.21.1__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,242 @@ 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
+ "started": "⚪ {tool_name}: {message}",
256
+ "running": "⏳ {tool_name}: {message}",
257
+ "finished": "✅ {tool_name}: {message}",
258
+ "failed": "❌ {tool_name}: {message}",
259
+ }
260
+ )
261
+ reporter = ToolProgressReporter(chat_service, config=custom_config)
262
+
263
+ # Act
264
+ await reporter.notify_from_tool_call(
265
+ tool_call=tool_call,
266
+ name="My Tool",
267
+ message="Working on it",
268
+ state=ProgressState.RUNNING,
269
+ )
270
+
271
+ # Assert
272
+ chat_service.modify_assistant_message_async.assert_called()
273
+ call_args = chat_service.modify_assistant_message_async.call_args
274
+ content = call_args.kwargs["content"]
275
+ assert "⏳ My Tool: Working on it" in content
276
+ assert "🟡" not in content # Default emoji should not appear
277
+
278
+ @pytest.mark.ai
279
+ @pytest.mark.asyncio
280
+ async def test_config__skips_states_with_empty_template__when_state_hidden(
281
+ self, chat_service, tool_call
282
+ ) -> None:
283
+ """
284
+ Purpose: Verify that states with empty string templates are not displayed.
285
+ Why this matters: Allows selective display of only certain states (e.g., hide STARTED).
286
+ Setup summary: Create config with empty string for RUNNING state, verify message is not displayed.
287
+ """
288
+ # Arrange
289
+ custom_config = ToolProgressReporterConfig(
290
+ state_to_display_template={
291
+ "started": "",
292
+ "running": "", # Empty string hides RUNNING state
293
+ "finished": "✅ {tool_name}: {message}",
294
+ "failed": "❌ {tool_name}: {message}",
295
+ }
296
+ )
297
+ reporter = ToolProgressReporter(chat_service, config=custom_config)
298
+
299
+ # Act
300
+ await reporter.notify_from_tool_call(
301
+ tool_call=tool_call,
302
+ name="Test Tool",
303
+ message="Processing",
304
+ state=ProgressState.RUNNING,
305
+ )
306
+
307
+ # Assert
308
+ chat_service.modify_assistant_message_async.assert_called()
309
+ call_args = chat_service.modify_assistant_message_async.call_args
310
+ content = call_args.kwargs["content"]
311
+ # Content should not contain the message since RUNNING template is empty
312
+ assert "Processing" not in content
313
+ assert "Test Tool" not in content
314
+
315
+ @pytest.mark.ai
316
+ @pytest.mark.asyncio
317
+ async def test_config__formats_placeholders_correctly__with_multiple_tools(
318
+ self, chat_service
319
+ ) -> None:
320
+ """
321
+ Purpose: Verify that {tool_name} and {message} placeholders are replaced correctly for multiple tools.
322
+ Why this matters: Ensures template formatting works correctly in multi-tool scenarios.
323
+ Setup summary: Add multiple tool statuses with different names/messages, verify formatting.
324
+ """
325
+ # Arrange
326
+ custom_config = ToolProgressReporterConfig(
327
+ state_to_display_template={
328
+ "started": "○ {tool_name} - {message}",
329
+ "running": "▶️ {tool_name} - {message}",
330
+ "finished": "✓ {tool_name} - {message}",
331
+ "failed": "✗ {tool_name} - {message}",
332
+ }
333
+ )
334
+ reporter = ToolProgressReporter(chat_service, config=custom_config)
335
+ tool_call_1 = LanguageModelFunction(id="tool_1", name="search")
336
+ tool_call_2 = LanguageModelFunction(id="tool_2", name="analyze")
337
+
338
+ # Act
339
+ await reporter.notify_from_tool_call(
340
+ tool_call=tool_call_1,
341
+ name="Search Tool",
342
+ message="Searching database",
343
+ state=ProgressState.RUNNING,
344
+ )
345
+ await reporter.notify_from_tool_call(
346
+ tool_call=tool_call_2,
347
+ name="Analysis Tool",
348
+ message="Analyzing results",
349
+ state=ProgressState.FINISHED,
350
+ )
351
+
352
+ # Assert
353
+ call_args = chat_service.modify_assistant_message_async.call_args
354
+ content = call_args.kwargs["content"]
355
+ assert "▶️ Search Tool - Searching database" in content
356
+ assert "✓ Analysis Tool - Analyzing results" in content
357
+
358
+ @pytest.mark.ai
359
+ @pytest.mark.asyncio
360
+ async def test_config__shows_only_finished_state__when_only_finished_configured(
361
+ self, chat_service, tool_call
362
+ ) -> None:
363
+ """
364
+ Purpose: Verify selective state display shows only FINISHED when other states use empty templates.
365
+ Why this matters: Use case where user only wants final results, not intermediate steps.
366
+ Setup summary: Configure only FINISHED with content, send STARTED and FINISHED, verify only FINISHED appears.
367
+ """
368
+ # Arrange
369
+ custom_config = ToolProgressReporterConfig(
370
+ state_to_display_template={
371
+ "started": "", # Empty template hides STARTED
372
+ "running": "", # Empty template hides RUNNING
373
+ "finished": "Done: {tool_name} - {message}",
374
+ "failed": "Failed: {tool_name} - {message}",
375
+ }
376
+ )
377
+ reporter = ToolProgressReporter(chat_service, config=custom_config)
378
+
379
+ # Act - Send STARTED state (should not appear)
380
+ await reporter.notify_from_tool_call(
381
+ tool_call=tool_call,
382
+ name="Test Tool",
383
+ message="Starting",
384
+ state=ProgressState.STARTED,
385
+ )
386
+
387
+ # Get first call content
388
+ first_call_args = chat_service.modify_assistant_message_async.call_args
389
+ first_content = first_call_args.kwargs["content"]
390
+
391
+ # Act - Update to FINISHED state (should appear)
392
+ await reporter.notify_from_tool_call(
393
+ tool_call=tool_call,
394
+ name="Test Tool",
395
+ message="Completed successfully",
396
+ state=ProgressState.FINISHED,
397
+ )
398
+
399
+ # Assert
400
+ final_call_args = chat_service.modify_assistant_message_async.call_args
401
+ final_content = final_call_args.kwargs["content"]
402
+
403
+ # STARTED state should not appear in first call
404
+ assert "Starting" not in first_content
405
+
406
+ # FINISHED state should appear in final call
407
+ assert "Done: Test Tool - Completed successfully" in final_content
408
+
409
+ @pytest.mark.ai
410
+ @pytest.mark.asyncio
411
+ async def test_config__handles_all_empty_templates__when_all_states_hidden(
412
+ self, chat_service, tool_call
413
+ ) -> None:
414
+ """
415
+ Purpose: Verify that all empty string templates result in no messages being displayed.
416
+ Why this matters: Edge case handling and allows disabling all progress display.
417
+ Setup summary: Create config with all empty templates, verify no tool messages appear.
418
+ """
419
+ # Arrange
420
+ custom_config = ToolProgressReporterConfig(
421
+ state_to_display_template={
422
+ "started": "",
423
+ "running": "",
424
+ "finished": "",
425
+ "failed": "",
426
+ }
427
+ )
428
+ reporter = ToolProgressReporter(chat_service, config=custom_config)
429
+
430
+ # Act
431
+ await reporter.notify_from_tool_call(
432
+ tool_call=tool_call,
433
+ name="Test Tool",
434
+ message="Processing",
435
+ state=ProgressState.RUNNING,
436
+ )
437
+
438
+ # Assert
439
+ chat_service.modify_assistant_message_async.assert_called()
440
+ call_args = chat_service.modify_assistant_message_async.call_args
441
+ content = call_args.kwargs["content"]
442
+
443
+ # Should only have the progress start text and newlines, no actual messages
444
+ assert "Test Tool" not in content
445
+ 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
- from typing import Protocol
5
+ from typing import Protocol, TypedDict
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,54 @@ 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
+ class StateToDisplayTemplate(TypedDict):
37
+ started: str
38
+ running: str
39
+ failed: str
40
+ finished: str
41
+
42
+
43
+ _DEFAULT_STATE_TO_DISPLAY_TEMPLATE: StateToDisplayTemplate = {
44
+ "started": "{arrow}**{{tool_name}}** ⚪: {{message}}".format(arrow=ARROW),
45
+ "running": "{arrow}**{{tool_name}}** 🟡: {{message}}".format(arrow=ARROW),
46
+ "finished": "{arrow}**{{tool_name}}** 🟢: {{message}}".format(arrow=ARROW),
47
+ "failed": "{arrow}**{{tool_name}}** 🔴: {{message}}".format(arrow=ARROW),
48
+ }
49
+
50
+
51
+ state_to_display_template_description = """
52
+ Display templates for the different progress states.
53
+ The template is a string that will be used to display the progress status.
54
+ It can contain the following placeholders:
55
+ - `{tool_name}`: The name of the tool
56
+ - `{message}`: The message to display (sent by the tool)
57
+ """.strip()
58
+
59
+
60
+ class ToolProgressReporterConfig(BaseModel):
61
+ model_config = get_configuration_dict()
62
+
63
+ state_to_display_template: StateToDisplayTemplate = Field(
64
+ default=_DEFAULT_STATE_TO_DISPLAY_TEMPLATE,
65
+ description=state_to_display_template_description,
66
+ title="Display Templates",
67
+ )
33
68
 
34
69
 
35
70
  class ToolProgressReporter:
36
- def __init__(self, chat_service: ChatService):
71
+ def __init__(
72
+ self,
73
+ chat_service: ChatService,
74
+ config: ToolProgressReporterConfig | None = None,
75
+ ):
37
76
  self.chat_service = chat_service
38
77
  self.tool_statuses: dict[str, ToolExecutionStatus] = {}
39
78
  self._progress_start_text = ""
40
79
  self._requires_new_assistant_message = False
80
+ self._config = config or ToolProgressReporterConfig()
41
81
 
42
82
  @property
43
83
  def requires_new_assistant_message(self):
@@ -77,16 +117,13 @@ class ToolProgressReporter:
77
117
  references (list[ContentReference], optional): List of content references. Defaults to [].
78
118
  requires_new_assistant_message (bool, optional): Whether a new assistant message is needed when tool call is finished.
79
119
  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
120
  """
84
- assert tool_call.id is not None
85
121
  self.tool_statuses[tool_call.id] = ToolExecutionStatus(
86
122
  name=name,
87
123
  message=message,
88
124
  state=state,
89
125
  references=references,
126
+ timestamp=self._get_timestamp_for_tool_call(tool_call),
90
127
  )
91
128
  self.requires_new_assistant_message = (
92
129
  self.requires_new_assistant_message or requires_new_assistant_message
@@ -103,7 +140,11 @@ class ToolProgressReporter:
103
140
  references = self._correct_reference_sequence(references, start_number)
104
141
  all_references.extend(references)
105
142
 
106
- messages.append(f"{ARROW}**{item.name} {item.state.value}**: {message}")
143
+ display_message = self._get_tool_status_display_message(
144
+ name=item.name, message=message, state=item.state
145
+ )
146
+ if display_message is not None:
147
+ messages.append(display_message)
107
148
 
108
149
  await self.chat_service.modify_assistant_message_async(
109
150
  content=self._progress_start_text + "\n\n" + "\n\n".join(messages),
@@ -130,6 +171,31 @@ class ToolProgressReporter:
130
171
  reference.sequence_number = i
131
172
  return references
132
173
 
174
+ def _get_timestamp_for_tool_call(
175
+ self, tool_call: LanguageModelFunction
176
+ ) -> datetime:
177
+ """
178
+ Keep the same timestamp if the tool call is already in the statuses.
179
+ This ensures the display order stays consistent.
180
+ """
181
+ if tool_call.id in self.tool_statuses:
182
+ return self.tool_statuses[tool_call.id].timestamp
183
+
184
+ return datetime.now()
185
+
186
+ def _get_tool_status_display_message(
187
+ self, name: str, message: str, state: ProgressState
188
+ ) -> str | None:
189
+ display_message = self._config.state_to_display_template[state.value].format(
190
+ tool_name=name,
191
+ message=message,
192
+ )
193
+ # Don't display empty messages
194
+ if display_message.strip() == "":
195
+ return None
196
+
197
+ return display_message
198
+
133
199
 
134
200
  class ToolWithToolProgressReporter(Protocol):
135
201
  tool_progress_reporter: ToolProgressReporter
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: unique_toolkit
3
- Version: 1.20.1
3
+ Version: 1.21.1
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.1] - 2025-10-30
123
+ - Improve Spaces 2.0 display of tool progress reporter configuration.
124
+
125
+ ## [1.21.0] - 2025-10-30
126
+ - Add option to customize the display of tool progress statuses.
127
+
122
128
  ## [1.20.1] - 2025-10-30
123
129
  - Fix typing issues in `LanguageModelFunction`.
124
130
 
@@ -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=XHNezB8itj9KzpQgD0cwRtp2AgRUfUkQsHc3bTyPj6c,15801
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=GaR0oqDUJZvBB9WCUVYYh0Zvs6U-LMygJCCrqPlitgA,10296
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
@@ -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.1.dist-info/LICENSE,sha256=GlN8wHNdh53xwOPg44URnwag6TEolCjoq3YD_KrWgss,193
170
- unique_toolkit-1.20.1.dist-info/METADATA,sha256=CscyMf-zTg-Z4iZB6X6mmaqgMyBwGW55bGmVHYnSc7E,39566
171
- unique_toolkit-1.20.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
172
- unique_toolkit-1.20.1.dist-info/RECORD,,
169
+ unique_toolkit-1.21.1.dist-info/LICENSE,sha256=GlN8wHNdh53xwOPg44URnwag6TEolCjoq3YD_KrWgss,193
170
+ unique_toolkit-1.21.1.dist-info/METADATA,sha256=anvdaQ88ZJ6rRjaHBHGuIs_gYO9qNWyE0qnCYzKzPPs,39753
171
+ unique_toolkit-1.21.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
172
+ unique_toolkit-1.21.1.dist-info/RECORD,,