unique_toolkit 1.8.1__py3-none-any.whl → 1.23.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.

Files changed (105) hide show
  1. unique_toolkit/__init__.py +20 -0
  2. unique_toolkit/_common/api_calling/human_verification_manager.py +121 -28
  3. unique_toolkit/_common/chunk_relevancy_sorter/config.py +3 -3
  4. unique_toolkit/_common/chunk_relevancy_sorter/tests/test_service.py +2 -5
  5. unique_toolkit/_common/default_language_model.py +9 -3
  6. unique_toolkit/_common/docx_generator/__init__.py +7 -0
  7. unique_toolkit/_common/docx_generator/config.py +12 -0
  8. unique_toolkit/_common/docx_generator/schemas.py +80 -0
  9. unique_toolkit/_common/docx_generator/service.py +252 -0
  10. unique_toolkit/_common/docx_generator/template/Doc Template.docx +0 -0
  11. unique_toolkit/_common/endpoint_builder.py +138 -117
  12. unique_toolkit/_common/endpoint_requestor.py +240 -14
  13. unique_toolkit/_common/exception.py +20 -0
  14. unique_toolkit/_common/feature_flags/schema.py +1 -5
  15. unique_toolkit/_common/referencing.py +53 -0
  16. unique_toolkit/_common/string_utilities.py +52 -1
  17. unique_toolkit/_common/tests/test_referencing.py +521 -0
  18. unique_toolkit/_common/tests/test_string_utilities.py +506 -0
  19. unique_toolkit/_common/utils/files.py +43 -0
  20. unique_toolkit/agentic/debug_info_manager/debug_info_manager.py +16 -6
  21. unique_toolkit/agentic/debug_info_manager/test/test_debug_info_manager.py +278 -0
  22. unique_toolkit/agentic/evaluation/config.py +3 -2
  23. unique_toolkit/agentic/evaluation/context_relevancy/service.py +2 -2
  24. unique_toolkit/agentic/evaluation/evaluation_manager.py +9 -5
  25. unique_toolkit/agentic/evaluation/hallucination/constants.py +1 -1
  26. unique_toolkit/agentic/evaluation/hallucination/hallucination_evaluation.py +26 -3
  27. unique_toolkit/agentic/history_manager/history_manager.py +14 -11
  28. unique_toolkit/agentic/history_manager/loop_token_reducer.py +3 -4
  29. unique_toolkit/agentic/history_manager/utils.py +10 -87
  30. unique_toolkit/agentic/postprocessor/postprocessor_manager.py +107 -16
  31. unique_toolkit/agentic/reference_manager/reference_manager.py +1 -1
  32. unique_toolkit/agentic/responses_api/__init__.py +19 -0
  33. unique_toolkit/agentic/responses_api/postprocessors/code_display.py +63 -0
  34. unique_toolkit/agentic/responses_api/postprocessors/generated_files.py +145 -0
  35. unique_toolkit/agentic/responses_api/stream_handler.py +15 -0
  36. unique_toolkit/agentic/tools/a2a/__init__.py +18 -2
  37. unique_toolkit/agentic/tools/a2a/evaluation/__init__.py +2 -0
  38. unique_toolkit/agentic/tools/a2a/evaluation/_utils.py +3 -3
  39. unique_toolkit/agentic/tools/a2a/evaluation/config.py +1 -1
  40. unique_toolkit/agentic/tools/a2a/evaluation/evaluator.py +143 -91
  41. unique_toolkit/agentic/tools/a2a/manager.py +7 -1
  42. unique_toolkit/agentic/tools/a2a/postprocessing/__init__.py +11 -3
  43. unique_toolkit/agentic/tools/a2a/postprocessing/_display_utils.py +185 -0
  44. unique_toolkit/agentic/tools/a2a/postprocessing/_ref_utils.py +73 -0
  45. unique_toolkit/agentic/tools/a2a/postprocessing/config.py +21 -0
  46. unique_toolkit/agentic/tools/a2a/postprocessing/display.py +180 -0
  47. unique_toolkit/agentic/tools/a2a/postprocessing/references.py +101 -0
  48. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display_utils.py +1335 -0
  49. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_ref_utils.py +603 -0
  50. unique_toolkit/agentic/tools/a2a/prompts.py +46 -0
  51. unique_toolkit/agentic/tools/a2a/response_watcher/__init__.py +6 -0
  52. unique_toolkit/agentic/tools/a2a/response_watcher/service.py +91 -0
  53. unique_toolkit/agentic/tools/a2a/tool/config.py +15 -5
  54. unique_toolkit/agentic/tools/a2a/tool/service.py +69 -36
  55. unique_toolkit/agentic/tools/config.py +16 -2
  56. unique_toolkit/agentic/tools/factory.py +4 -0
  57. unique_toolkit/agentic/tools/mcp/tool_wrapper.py +7 -35
  58. unique_toolkit/agentic/tools/openai_builtin/__init__.py +11 -0
  59. unique_toolkit/agentic/tools/openai_builtin/base.py +30 -0
  60. unique_toolkit/agentic/tools/openai_builtin/code_interpreter/__init__.py +8 -0
  61. unique_toolkit/agentic/tools/openai_builtin/code_interpreter/config.py +57 -0
  62. unique_toolkit/agentic/tools/openai_builtin/code_interpreter/service.py +230 -0
  63. unique_toolkit/agentic/tools/openai_builtin/manager.py +62 -0
  64. unique_toolkit/agentic/tools/test/test_mcp_manager.py +95 -7
  65. unique_toolkit/agentic/tools/test/test_tool_progress_reporter.py +240 -0
  66. unique_toolkit/agentic/tools/tool.py +0 -11
  67. unique_toolkit/agentic/tools/tool_manager.py +337 -122
  68. unique_toolkit/agentic/tools/tool_progress_reporter.py +81 -15
  69. unique_toolkit/agentic/tools/utils/__init__.py +18 -0
  70. unique_toolkit/agentic/tools/utils/execution/execution.py +8 -4
  71. unique_toolkit/agentic/tools/utils/source_handling/schema.py +1 -1
  72. unique_toolkit/chat/__init__.py +8 -1
  73. unique_toolkit/chat/deprecated/service.py +232 -0
  74. unique_toolkit/chat/functions.py +54 -40
  75. unique_toolkit/chat/rendering.py +34 -0
  76. unique_toolkit/chat/responses_api.py +461 -0
  77. unique_toolkit/chat/schemas.py +1 -1
  78. unique_toolkit/chat/service.py +96 -1569
  79. unique_toolkit/content/functions.py +116 -1
  80. unique_toolkit/content/schemas.py +59 -0
  81. unique_toolkit/content/service.py +5 -37
  82. unique_toolkit/content/smart_rules.py +301 -0
  83. unique_toolkit/framework_utilities/langchain/client.py +27 -3
  84. unique_toolkit/framework_utilities/openai/client.py +12 -1
  85. unique_toolkit/framework_utilities/openai/message_builder.py +85 -1
  86. unique_toolkit/language_model/default_language_model.py +3 -0
  87. unique_toolkit/language_model/functions.py +25 -9
  88. unique_toolkit/language_model/infos.py +72 -4
  89. unique_toolkit/language_model/schemas.py +246 -40
  90. unique_toolkit/protocols/support.py +91 -9
  91. unique_toolkit/services/__init__.py +7 -0
  92. unique_toolkit/services/chat_service.py +1630 -0
  93. unique_toolkit/services/knowledge_base.py +861 -0
  94. unique_toolkit/smart_rules/compile.py +56 -301
  95. unique_toolkit/test_utilities/events.py +197 -0
  96. {unique_toolkit-1.8.1.dist-info → unique_toolkit-1.23.0.dist-info}/METADATA +173 -3
  97. {unique_toolkit-1.8.1.dist-info → unique_toolkit-1.23.0.dist-info}/RECORD +99 -67
  98. unique_toolkit/agentic/tools/a2a/postprocessing/_display.py +0 -122
  99. unique_toolkit/agentic/tools/a2a/postprocessing/_utils.py +0 -19
  100. unique_toolkit/agentic/tools/a2a/postprocessing/postprocessor.py +0 -230
  101. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_consolidate_references.py +0 -665
  102. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display.py +0 -391
  103. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_postprocessor_reference_functions.py +0 -256
  104. {unique_toolkit-1.8.1.dist-info → unique_toolkit-1.23.0.dist-info}/LICENSE +0 -0
  105. {unique_toolkit-1.8.1.dist-info → unique_toolkit-1.23.0.dist-info}/WHEEL +0 -0
@@ -1,7 +1,12 @@
1
1
  import asyncio
2
+ from abc import ABC, abstractmethod
2
3
  from logging import Logger, getLogger
3
- from typing import Any
4
+ from typing import Literal, override
4
5
 
6
+ from openai.types.chat import (
7
+ ChatCompletionNamedToolChoiceParam,
8
+ )
9
+ from openai.types.responses import ToolParam, response_create_params
5
10
  from pydantic import BaseModel, Field
6
11
 
7
12
  from unique_toolkit.agentic.evaluation.schemas import EvaluationMetricName
@@ -9,6 +14,10 @@ from unique_toolkit.agentic.tools.a2a import A2AManager, SubAgentTool
9
14
  from unique_toolkit.agentic.tools.config import ToolBuildConfig
10
15
  from unique_toolkit.agentic.tools.factory import ToolFactory
11
16
  from unique_toolkit.agentic.tools.mcp.manager import MCPManager
17
+ from unique_toolkit.agentic.tools.openai_builtin.base import (
18
+ OpenAIBuiltInTool,
19
+ )
20
+ from unique_toolkit.agentic.tools.openai_builtin.manager import OpenAIBuiltInToolManager
12
21
  from unique_toolkit.agentic.tools.schemas import ToolCallResponse, ToolPrompts
13
22
  from unique_toolkit.agentic.tools.tool import Tool
14
23
  from unique_toolkit.agentic.tools.tool_progress_reporter import ToolProgressReporter
@@ -19,7 +28,6 @@ from unique_toolkit.agentic.tools.utils.execution.execution import (
19
28
  from unique_toolkit.app.schemas import ChatEvent
20
29
  from unique_toolkit.language_model.schemas import (
21
30
  LanguageModelFunction,
22
- LanguageModelTool,
23
31
  LanguageModelToolDescription,
24
32
  )
25
33
 
@@ -44,135 +52,41 @@ class ToolManagerConfig(BaseModel):
44
52
  )
45
53
 
46
54
 
47
- class ToolManager:
48
- """
49
- Manages the tools available to the agent and executes tool calls.
50
-
51
- This class is responsible for:
52
- - Initializing tools based on the provided configuration and runtime events.
53
- - Filtering tools based on availability, exclusivity, and user-defined constraints.
54
- - Managing the lifecycle of tools, including retrieval, execution, and logging.
55
- - Executing tool calls in parallel when possible to optimize performance.
56
- - Enforcing limits on the number of tool calls and handling duplicate requests.
57
-
58
- Key Features:
59
- - Dynamic Tool Initialization: Tools are dynamically selected and initialized
60
- based on runtime events and user preferences.
61
- - Parallel Execution: Supports asynchronous execution of tools for efficiency.
62
- - Error Handling: Provides detailed error messages and logs for failed tool calls.
63
- - Scalability: Designed to handle a large number of tools and tool calls efficiently.
64
-
65
- Only the ToolManager is allowed to interact with the tools directly.
66
- """
67
-
68
- def __init__(
69
- self,
70
- logger: Logger,
71
- config: ToolManagerConfig,
72
- event: ChatEvent,
73
- tool_progress_reporter: ToolProgressReporter,
74
- mcp_manager: MCPManager,
75
- a2a_manager: A2AManager,
76
- ):
77
- self._logger = logger
55
+ class BaseToolManager(ABC):
56
+ def __init__(self, config: ToolManagerConfig):
78
57
  self._config = config
79
- self._tool_progress_reporter = tool_progress_reporter
80
- self._tools = []
81
- self._tool_choices = event.payload.tool_choices
82
- self._disabled_tools = event.payload.disabled_tools
83
58
  # this needs to be a set of strings to avoid duplicates
84
59
  self._tool_evaluation_check_list: set[EvaluationMetricName] = set()
85
- self._mcp_manager = mcp_manager
86
- self._a2a_manager = a2a_manager
87
- self._init__tools(event)
88
-
89
- def _init__tools(self, event: ChatEvent) -> None:
90
- tool_choices = self._tool_choices
91
- tool_configs = self._config.tools
92
- self._logger.info("Initializing tool definitions...")
93
- self._logger.info(f"Tool choices: {tool_choices}")
94
- self._logger.info(f"Tool configs: {tool_configs}")
95
-
96
- tool_configs, sub_agents = self._a2a_manager.get_all_sub_agents(
97
- tool_configs, event
98
- )
99
-
100
- # Build internal tools from configurations
101
- internal_tools = [
102
- ToolFactory.build_tool_with_settings(
103
- t.name,
104
- t,
105
- t.configuration,
106
- event,
107
- tool_progress_reporter=self._tool_progress_reporter,
108
- )
109
- for t in tool_configs
110
- ]
111
-
112
- # Get MCP tools (these are already properly instantiated)
113
- mcp_tools = self._mcp_manager.get_all_mcp_tools()
114
- # Combine both types of tools
115
- self.available_tools = internal_tools + mcp_tools + sub_agents
116
- self._sub_agents = sub_agents
117
-
118
- for t in self.available_tools:
119
- if not t.is_enabled():
120
- continue
121
- if t.name in self._disabled_tools:
122
- continue
123
- # if tool choices are given, only include those tools
124
- if len(self._tool_choices) > 0 and t.name not in self._tool_choices:
125
- continue
126
- # is the tool exclusive and has been choosen by the user?
127
- if t.is_exclusive() and len(tool_choices) > 0 and t.name in tool_choices:
128
- self._tools = [t] # override all other tools
129
- break
130
- # if the tool is exclusive but no tool choices are given, skip it
131
- if t.is_exclusive():
132
- continue
133
-
134
- self._tools.append(t)
135
-
136
- @property
137
- def sub_agents(self) -> list[SubAgentTool]:
138
- return self._sub_agents
139
-
140
- def get_evaluation_check_list(self) -> list[EvaluationMetricName]:
141
- return list(self._tool_evaluation_check_list)
142
-
143
- def log_loaded_tools(self):
144
- self._logger.info(f"Loaded tools: {[tool.name for tool in self._tools]}")
145
-
146
- def get_tools(self) -> list[Tool]:
147
- return self._tools # type: ignore
148
60
 
61
+ @abstractmethod
149
62
  def get_tool_by_name(self, name: str) -> Tool | None:
150
- for tool in self._tools:
151
- if tool.name == name:
152
- return tool
153
- return None
63
+ raise NotImplementedError()
154
64
 
155
- def get_forced_tools(self) -> list[dict[str, Any]]:
156
- return [
157
- self._convert_to_forced_tool(t.name)
158
- for t in self._tools
159
- if t.name in self._tool_choices
160
- ]
65
+ @abstractmethod
66
+ def get_tool_choices(self) -> list[str]:
67
+ raise NotImplementedError()
161
68
 
162
- def add_forced_tool(self, name):
163
- tool = self.get_tool_by_name(name)
164
- if not tool:
165
- raise ValueError(f"Tool {name} not found")
166
- self._tools.append(tool)
167
- self._tool_choices.append(tool.name)
69
+ @abstractmethod
70
+ def get_exclusive_tools(self) -> list[str]:
71
+ raise NotImplementedError()
168
72
 
169
- def get_tool_definitions(
73
+ @abstractmethod
74
+ def filter_tool_calls(
170
75
  self,
171
- ) -> list[LanguageModelTool | LanguageModelToolDescription]:
172
- return [tool.tool_description() for tool in self._tools]
76
+ tool_calls: list[LanguageModelFunction],
77
+ tool_types: list[Literal["mcp", "internal", "subagent"]],
78
+ ) -> list[LanguageModelFunction]:
79
+ """
80
+ Filter tool calls by their types.
173
81
 
174
- def get_tool_prompts(self) -> list[ToolPrompts]:
175
- return [tool.get_tool_prompts() for tool in self._tools]
82
+ Args:
83
+ tool_calls: List of tool calls to filter
84
+ tool_types: List of tool types to include (e.g., ["mcp", "internal", "subagent"])
85
+
86
+ Returns:
87
+ Filtered list of tool calls matching the specified types
88
+ """
89
+ raise NotImplementedError()
176
90
 
177
91
  def does_a_tool_take_control(self, tool_calls: list[LanguageModelFunction]) -> bool:
178
92
  for tool_call in tool_calls:
@@ -233,6 +147,14 @@ class ToolManager:
233
147
  unpacked_tool_call_result = self._create_tool_call_response(
234
148
  result, tool_calls[i]
235
149
  )
150
+ if unpacked_tool_call_result.debug_info is None:
151
+ unpacked_tool_call_result.debug_info = {}
152
+ unpacked_tool_call_result.debug_info["is_exclusive"] = (
153
+ tool_calls[i].name in self.get_exclusive_tools()
154
+ )
155
+ unpacked_tool_call_result.debug_info["is_forced"] = (
156
+ tool_calls[i].name in self.get_tool_choices()
157
+ )
236
158
  tool_call_results_unpacked.append(unpacked_tool_call_result)
237
159
 
238
160
  return tool_call_results_unpacked
@@ -301,8 +223,301 @@ class ToolManager:
301
223
  )
302
224
  return unique_tool_calls
303
225
 
304
- def _convert_to_forced_tool(self, tool_name: str) -> dict[str, Any]:
226
+ def get_evaluation_check_list(self) -> list[EvaluationMetricName]:
227
+ return list(self._tool_evaluation_check_list)
228
+
229
+
230
+ class ToolManager(BaseToolManager):
231
+ """
232
+ Manages the tools available to the agent and executes tool calls.
233
+
234
+ This class is responsible for:
235
+ - Initializing tools based on the provided configuration and runtime events.
236
+ - Filtering tools based on availability, exclusivity, and user-defined constraints.
237
+ - Managing the lifecycle of tools, including retrieval, execution, and logging.
238
+ - Executing tool calls in parallel when possible to optimize performance.
239
+ - Enforcing limits on the number of tool calls and handling duplicate requests.
240
+
241
+ Key Features:
242
+ - Dynamic Tool Initialization: Tools are dynamically selected and initialized
243
+ based on runtime events and user preferences.
244
+ - Parallel Execution: Supports asynchronous execution of tools for efficiency.
245
+ - Error Handling: Provides detailed error messages and logs for failed tool calls.
246
+ - Scalability: Designed to handle a large number of tools and tool calls efficiently.
247
+
248
+ Only the ToolManager is allowed to interact with the tools directly.
249
+ """
250
+
251
+ def __init__(
252
+ self,
253
+ logger: Logger,
254
+ config: ToolManagerConfig,
255
+ event: ChatEvent,
256
+ tool_progress_reporter: ToolProgressReporter,
257
+ mcp_manager: MCPManager,
258
+ a2a_manager: A2AManager,
259
+ ):
260
+ super().__init__(config)
261
+ self._logger = logger
262
+ self._config = config
263
+ self._tool_progress_reporter = tool_progress_reporter
264
+ self._tools = []
265
+ self._tool_choices = event.payload.tool_choices
266
+ self._disabled_tools = event.payload.disabled_tools
267
+ self._exclusive_tools = [
268
+ tool.name for tool in self._config.tools if tool.is_exclusive
269
+ ]
270
+ # this needs to be a set of strings to avoid duplicates
271
+ self._tool_evaluation_check_list: set[EvaluationMetricName] = set()
272
+ self._mcp_manager = mcp_manager
273
+ self._a2a_manager = a2a_manager
274
+ self._init__tools(event)
275
+
276
+ def _init__tools(self, event: ChatEvent) -> None:
277
+ tool_choices = self._tool_choices
278
+ tool_configs = self._config.tools
279
+ self._logger.info("Initializing tool definitions...")
280
+ self._logger.info(f"Tool choices: {tool_choices}")
281
+
282
+ tool_configs, sub_agents = self._a2a_manager.get_all_sub_agents(
283
+ tool_configs, event
284
+ )
285
+
286
+ # Build internal tools from configurations
287
+ self._internal_tools = [
288
+ ToolFactory.build_tool_with_settings(
289
+ t.name,
290
+ t,
291
+ t.configuration,
292
+ event,
293
+ tool_progress_reporter=self._tool_progress_reporter,
294
+ )
295
+ for t in tool_configs
296
+ ]
297
+
298
+ # Get MCP tools (these are already properly instantiated)
299
+ self._mcp_tools = self._mcp_manager.get_all_mcp_tools()
300
+ # Combine both types of tools
301
+ self.available_tools = self._internal_tools + self._mcp_tools + sub_agents
302
+ self._sub_agents = sub_agents
303
+
304
+ for t in self.available_tools:
305
+ if not t.is_enabled():
306
+ continue
307
+ if t.name in self._disabled_tools:
308
+ continue
309
+ # if tool choices are given, only include those tools
310
+ if len(self._tool_choices) > 0 and t.name not in self._tool_choices:
311
+ continue
312
+ # is the tool exclusive and has been choosen by the user?
313
+ if t.is_exclusive() and len(tool_choices) > 0 and t.name in tool_choices:
314
+ self._tools = [t] # override all other tools
315
+ break
316
+ # if the tool is exclusive but no tool choices are given, skip it
317
+ if t.is_exclusive():
318
+ continue
319
+
320
+ self._tools.append(t)
321
+
322
+ @override
323
+ def filter_tool_calls(
324
+ self,
325
+ tool_calls: list[LanguageModelFunction],
326
+ tool_types: list[Literal["mcp", "internal", "subagent"]],
327
+ ) -> list[LanguageModelFunction]:
328
+ filtered_calls = []
329
+
330
+ # Build sets for efficient lookup
331
+ internal_tool_names = {tool.name for tool in self._internal_tools}
332
+ mcp_tool_names = {tool.name for tool in self._mcp_tools}
333
+ sub_agent_names = {tool.name for tool in self._sub_agents}
334
+
335
+ for tool_call in tool_calls:
336
+ if "internal" in tool_types and tool_call.name in internal_tool_names:
337
+ filtered_calls.append(tool_call)
338
+ elif "mcp" in tool_types and tool_call.name in mcp_tool_names:
339
+ filtered_calls.append(tool_call)
340
+ elif "subagent" in tool_types and tool_call.name in sub_agent_names:
341
+ filtered_calls.append(tool_call)
342
+
343
+ return filtered_calls
344
+
345
+ @property
346
+ def sub_agents(self) -> list[SubAgentTool]:
347
+ return self._sub_agents
348
+
349
+ def get_evaluation_check_list(self) -> list[EvaluationMetricName]:
350
+ return list(self._tool_evaluation_check_list)
351
+
352
+ def log_loaded_tools(self):
353
+ self._logger.info(f"Loaded tools: {[tool.name for tool in self._tools]}")
354
+
355
+ @override
356
+ def get_tool_by_name(self, name: str) -> Tool | None:
357
+ for tool in self._tools:
358
+ if tool.name == name:
359
+ return tool
360
+ return None
361
+
362
+ @override
363
+ def get_tool_choices(self) -> list[str]:
364
+ return self._tool_choices
365
+
366
+ @override
367
+ def get_exclusive_tools(self) -> list[str]:
368
+ return self._exclusive_tools
369
+
370
+ def get_tools(self) -> list[Tool]:
371
+ return self._tools # type: ignore
372
+
373
+ def get_forced_tools(
374
+ self,
375
+ ) -> list[ChatCompletionNamedToolChoiceParam]:
376
+ return [
377
+ self._convert_to_forced_tool(t.name)
378
+ for t in self._tools
379
+ if t.name in self._tool_choices
380
+ ]
381
+
382
+ def get_tool_definitions(
383
+ self,
384
+ ) -> list[LanguageModelToolDescription]:
385
+ return [tool.tool_description() for tool in self._tools]
386
+
387
+ def get_tool_prompts(self) -> list[ToolPrompts]:
388
+ return [tool.get_tool_prompts() for tool in self._tools]
389
+
390
+ def add_forced_tool(self, name):
391
+ tool = self.get_tool_by_name(name)
392
+ if not tool:
393
+ raise ValueError(f"Tool {name} not found")
394
+
395
+ if tool.name not in self._tool_choices:
396
+ self._tool_choices.append(tool.name)
397
+
398
+ def _convert_to_forced_tool(
399
+ self, tool_name: str
400
+ ) -> ChatCompletionNamedToolChoiceParam:
305
401
  return {
306
402
  "type": "function",
307
403
  "function": {"name": tool_name},
308
404
  }
405
+
406
+ def tool_choices(self) -> list[str]:
407
+ return self._tool_choices.copy()
408
+
409
+
410
+ class ResponsesApiToolManager(BaseToolManager):
411
+ def __init__(
412
+ self,
413
+ logger: Logger,
414
+ config: ToolManagerConfig,
415
+ tool_manager: ToolManager,
416
+ builtin_tools: list[OpenAIBuiltInTool],
417
+ ) -> None:
418
+ super().__init__(config)
419
+ self._logger = logger
420
+ self._config = config
421
+ self._tool_manager = tool_manager
422
+ self._builtin_tools = builtin_tools
423
+ self._tools = self._tool_manager.get_tools()
424
+
425
+ @classmethod
426
+ async def build_manager(
427
+ cls,
428
+ logger: Logger,
429
+ config: ToolManagerConfig,
430
+ event: ChatEvent,
431
+ tool_progress_reporter: ToolProgressReporter,
432
+ mcp_manager: MCPManager,
433
+ a2a_manager: A2AManager,
434
+ builtin_tool_manager: OpenAIBuiltInToolManager,
435
+ ) -> "ResponsesApiToolManager":
436
+ (
437
+ tool_configs,
438
+ builtin_tools,
439
+ ) = await builtin_tool_manager.get_all_openai_builtin_tools(config.tools)
440
+
441
+ completions_tool_manager_config = ToolManagerConfig(
442
+ tools=tool_configs, max_tool_calls=config.max_tool_calls
443
+ )
444
+ completions_tool_manager = ToolManager(
445
+ logger=logger,
446
+ config=completions_tool_manager_config,
447
+ event=event,
448
+ tool_progress_reporter=tool_progress_reporter,
449
+ mcp_manager=mcp_manager,
450
+ a2a_manager=a2a_manager,
451
+ )
452
+
453
+ return cls(
454
+ logger=logger,
455
+ config=config,
456
+ tool_manager=completions_tool_manager,
457
+ builtin_tools=builtin_tools,
458
+ )
459
+
460
+ @override
461
+ def filter_tool_calls(
462
+ self,
463
+ tool_calls: list[LanguageModelFunction],
464
+ tool_types: list[Literal["mcp", "internal", "subagent"]],
465
+ ) -> list[LanguageModelFunction]:
466
+ """Delegate filtering to the underlying tool manager."""
467
+ return self._tool_manager.filter_tool_calls(tool_calls, tool_types)
468
+
469
+ @override
470
+ def get_tool_by_name(self, name: str) -> Tool | None:
471
+ return self._tool_manager.get_tool_by_name(name)
472
+
473
+ @override
474
+ def get_tool_choices(self) -> list[str]:
475
+ return self._tool_manager._tool_choices
476
+
477
+ @override
478
+ def get_exclusive_tools(self) -> list[str]:
479
+ return self._tool_manager._exclusive_tools
480
+
481
+ @property
482
+ def sub_agents(self) -> list[SubAgentTool]:
483
+ return self._tool_manager.sub_agents
484
+
485
+ def log_loaded_tools(self):
486
+ self._logger.info(
487
+ f"Loaded tools: {[tool.name for tool in self._tools + self._builtin_tools]}"
488
+ )
489
+
490
+ def get_tools(self) -> list[Tool]:
491
+ return self._tool_manager.get_tools()
492
+
493
+ def get_forced_tools(
494
+ self,
495
+ ) -> list[response_create_params.ToolChoice]:
496
+ """
497
+ Note that built-in tools cannot be forced at the moment
498
+ """
499
+ return [
500
+ {
501
+ "name": t.name,
502
+ "type": "function",
503
+ }
504
+ for t in self._tools
505
+ if t.name in self._tool_manager.tool_choices()
506
+ ]
507
+
508
+ def get_tool_definitions(
509
+ self,
510
+ ) -> list[LanguageModelToolDescription | ToolParam]:
511
+ if len(self._tool_manager.tool_choices()) > 0:
512
+ # We cannot send a builtin tool in this case (api error)
513
+ return [tool.tool_description() for tool in self._tools]
514
+ else:
515
+ return [
516
+ tool.tool_description() for tool in self._tools + self._builtin_tools
517
+ ]
518
+
519
+ def get_tool_prompts(self) -> list[ToolPrompts]:
520
+ return [tool.get_tool_prompts() for tool in self._tools + self._builtin_tools]
521
+
522
+ def add_forced_tool(self, name: str) -> None:
523
+ self._tool_manager.add_forced_tool(name)
@@ -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 +1,19 @@
1
1
  """Utilities for tools."""
2
+
3
+ from unique_toolkit.agentic.tools.utils.execution.execution import (
4
+ Result,
5
+ SafeTaskExecutor,
6
+ failsafe,
7
+ failsafe_async,
8
+ safe_execute,
9
+ safe_execute_async,
10
+ )
11
+
12
+ __all__ = [
13
+ "failsafe",
14
+ "failsafe_async",
15
+ "safe_execute",
16
+ "safe_execute_async",
17
+ "SafeTaskExecutor",
18
+ "Result",
19
+ ]