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.
- unique_toolkit/__init__.py +20 -0
- unique_toolkit/_common/api_calling/human_verification_manager.py +121 -28
- unique_toolkit/_common/chunk_relevancy_sorter/config.py +3 -3
- unique_toolkit/_common/chunk_relevancy_sorter/tests/test_service.py +2 -5
- unique_toolkit/_common/default_language_model.py +9 -3
- unique_toolkit/_common/docx_generator/__init__.py +7 -0
- unique_toolkit/_common/docx_generator/config.py +12 -0
- unique_toolkit/_common/docx_generator/schemas.py +80 -0
- unique_toolkit/_common/docx_generator/service.py +252 -0
- unique_toolkit/_common/docx_generator/template/Doc Template.docx +0 -0
- unique_toolkit/_common/endpoint_builder.py +138 -117
- unique_toolkit/_common/endpoint_requestor.py +240 -14
- unique_toolkit/_common/exception.py +20 -0
- unique_toolkit/_common/feature_flags/schema.py +1 -5
- unique_toolkit/_common/referencing.py +53 -0
- unique_toolkit/_common/string_utilities.py +52 -1
- unique_toolkit/_common/tests/test_referencing.py +521 -0
- unique_toolkit/_common/tests/test_string_utilities.py +506 -0
- unique_toolkit/_common/utils/files.py +43 -0
- unique_toolkit/agentic/debug_info_manager/debug_info_manager.py +16 -6
- unique_toolkit/agentic/debug_info_manager/test/test_debug_info_manager.py +278 -0
- unique_toolkit/agentic/evaluation/config.py +3 -2
- unique_toolkit/agentic/evaluation/context_relevancy/service.py +2 -2
- unique_toolkit/agentic/evaluation/evaluation_manager.py +9 -5
- unique_toolkit/agentic/evaluation/hallucination/constants.py +1 -1
- unique_toolkit/agentic/evaluation/hallucination/hallucination_evaluation.py +26 -3
- unique_toolkit/agentic/history_manager/history_manager.py +14 -11
- unique_toolkit/agentic/history_manager/loop_token_reducer.py +3 -4
- unique_toolkit/agentic/history_manager/utils.py +10 -87
- unique_toolkit/agentic/postprocessor/postprocessor_manager.py +107 -16
- unique_toolkit/agentic/reference_manager/reference_manager.py +1 -1
- unique_toolkit/agentic/responses_api/__init__.py +19 -0
- unique_toolkit/agentic/responses_api/postprocessors/code_display.py +63 -0
- unique_toolkit/agentic/responses_api/postprocessors/generated_files.py +145 -0
- unique_toolkit/agentic/responses_api/stream_handler.py +15 -0
- unique_toolkit/agentic/tools/a2a/__init__.py +18 -2
- unique_toolkit/agentic/tools/a2a/evaluation/__init__.py +2 -0
- unique_toolkit/agentic/tools/a2a/evaluation/_utils.py +3 -3
- unique_toolkit/agentic/tools/a2a/evaluation/config.py +1 -1
- unique_toolkit/agentic/tools/a2a/evaluation/evaluator.py +143 -91
- unique_toolkit/agentic/tools/a2a/manager.py +7 -1
- unique_toolkit/agentic/tools/a2a/postprocessing/__init__.py +11 -3
- unique_toolkit/agentic/tools/a2a/postprocessing/_display_utils.py +185 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/_ref_utils.py +73 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/config.py +21 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/display.py +180 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/references.py +101 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display_utils.py +1335 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/test/test_ref_utils.py +603 -0
- unique_toolkit/agentic/tools/a2a/prompts.py +46 -0
- unique_toolkit/agentic/tools/a2a/response_watcher/__init__.py +6 -0
- unique_toolkit/agentic/tools/a2a/response_watcher/service.py +91 -0
- unique_toolkit/agentic/tools/a2a/tool/config.py +15 -5
- unique_toolkit/agentic/tools/a2a/tool/service.py +69 -36
- unique_toolkit/agentic/tools/config.py +16 -2
- unique_toolkit/agentic/tools/factory.py +4 -0
- unique_toolkit/agentic/tools/mcp/tool_wrapper.py +7 -35
- unique_toolkit/agentic/tools/openai_builtin/__init__.py +11 -0
- unique_toolkit/agentic/tools/openai_builtin/base.py +30 -0
- unique_toolkit/agentic/tools/openai_builtin/code_interpreter/__init__.py +8 -0
- unique_toolkit/agentic/tools/openai_builtin/code_interpreter/config.py +57 -0
- unique_toolkit/agentic/tools/openai_builtin/code_interpreter/service.py +230 -0
- unique_toolkit/agentic/tools/openai_builtin/manager.py +62 -0
- unique_toolkit/agentic/tools/test/test_mcp_manager.py +95 -7
- unique_toolkit/agentic/tools/test/test_tool_progress_reporter.py +240 -0
- unique_toolkit/agentic/tools/tool.py +0 -11
- unique_toolkit/agentic/tools/tool_manager.py +337 -122
- unique_toolkit/agentic/tools/tool_progress_reporter.py +81 -15
- unique_toolkit/agentic/tools/utils/__init__.py +18 -0
- unique_toolkit/agentic/tools/utils/execution/execution.py +8 -4
- unique_toolkit/agentic/tools/utils/source_handling/schema.py +1 -1
- unique_toolkit/chat/__init__.py +8 -1
- unique_toolkit/chat/deprecated/service.py +232 -0
- unique_toolkit/chat/functions.py +54 -40
- unique_toolkit/chat/rendering.py +34 -0
- unique_toolkit/chat/responses_api.py +461 -0
- unique_toolkit/chat/schemas.py +1 -1
- unique_toolkit/chat/service.py +96 -1569
- unique_toolkit/content/functions.py +116 -1
- unique_toolkit/content/schemas.py +59 -0
- unique_toolkit/content/service.py +5 -37
- unique_toolkit/content/smart_rules.py +301 -0
- unique_toolkit/framework_utilities/langchain/client.py +27 -3
- unique_toolkit/framework_utilities/openai/client.py +12 -1
- unique_toolkit/framework_utilities/openai/message_builder.py +85 -1
- unique_toolkit/language_model/default_language_model.py +3 -0
- unique_toolkit/language_model/functions.py +25 -9
- unique_toolkit/language_model/infos.py +72 -4
- unique_toolkit/language_model/schemas.py +246 -40
- unique_toolkit/protocols/support.py +91 -9
- unique_toolkit/services/__init__.py +7 -0
- unique_toolkit/services/chat_service.py +1630 -0
- unique_toolkit/services/knowledge_base.py +861 -0
- unique_toolkit/smart_rules/compile.py +56 -301
- unique_toolkit/test_utilities/events.py +197 -0
- {unique_toolkit-1.8.1.dist-info → unique_toolkit-1.23.0.dist-info}/METADATA +173 -3
- {unique_toolkit-1.8.1.dist-info → unique_toolkit-1.23.0.dist-info}/RECORD +99 -67
- unique_toolkit/agentic/tools/a2a/postprocessing/_display.py +0 -122
- unique_toolkit/agentic/tools/a2a/postprocessing/_utils.py +0 -19
- unique_toolkit/agentic/tools/a2a/postprocessing/postprocessor.py +0 -230
- unique_toolkit/agentic/tools/a2a/postprocessing/test/test_consolidate_references.py +0 -665
- unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display.py +0 -391
- unique_toolkit/agentic/tools/a2a/postprocessing/test/test_postprocessor_reference_functions.py +0 -256
- {unique_toolkit-1.8.1.dist-info → unique_toolkit-1.23.0.dist-info}/LICENSE +0 -0
- {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
|
|
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
|
|
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
|
-
|
|
151
|
-
if tool.name == name:
|
|
152
|
-
return tool
|
|
153
|
-
return None
|
|
63
|
+
raise NotImplementedError()
|
|
154
64
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
73
|
+
@abstractmethod
|
|
74
|
+
def filter_tool_calls(
|
|
170
75
|
self,
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
175
|
-
|
|
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
|
|
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
|
|
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(
|
|
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__(
|
|
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
|
-
|
|
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
|
+
]
|