agno 2.0.7__py3-none-any.whl → 2.0.9__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.
- agno/agent/agent.py +83 -51
- agno/db/base.py +14 -0
- agno/db/dynamo/dynamo.py +107 -27
- agno/db/firestore/firestore.py +109 -33
- agno/db/gcs_json/gcs_json_db.py +100 -20
- agno/db/in_memory/in_memory_db.py +95 -20
- agno/db/json/json_db.py +101 -21
- agno/db/migrations/v1_to_v2.py +322 -47
- agno/db/mongo/mongo.py +251 -26
- agno/db/mysql/mysql.py +307 -6
- agno/db/postgres/postgres.py +279 -33
- agno/db/redis/redis.py +99 -22
- agno/db/singlestore/singlestore.py +319 -38
- agno/db/sqlite/sqlite.py +339 -23
- agno/knowledge/embedder/sentence_transformer.py +3 -3
- agno/knowledge/knowledge.py +152 -31
- agno/knowledge/types.py +8 -0
- agno/models/anthropic/claude.py +0 -20
- agno/models/cometapi/__init__.py +5 -0
- agno/models/cometapi/cometapi.py +57 -0
- agno/models/google/gemini.py +4 -8
- agno/models/huggingface/huggingface.py +2 -1
- agno/models/ollama/chat.py +52 -3
- agno/models/openai/chat.py +9 -7
- agno/models/openai/responses.py +21 -17
- agno/os/interfaces/agui/agui.py +2 -2
- agno/os/interfaces/agui/utils.py +81 -18
- agno/os/interfaces/base.py +2 -0
- agno/os/interfaces/slack/router.py +50 -10
- agno/os/interfaces/slack/slack.py +6 -4
- agno/os/interfaces/whatsapp/router.py +7 -4
- agno/os/interfaces/whatsapp/whatsapp.py +2 -2
- agno/os/router.py +18 -0
- agno/os/utils.py +10 -2
- agno/reasoning/azure_ai_foundry.py +2 -2
- agno/reasoning/deepseek.py +2 -2
- agno/reasoning/default.py +3 -1
- agno/reasoning/groq.py +2 -2
- agno/reasoning/ollama.py +2 -2
- agno/reasoning/openai.py +2 -2
- agno/run/base.py +15 -2
- agno/session/agent.py +8 -5
- agno/session/team.py +14 -10
- agno/team/team.py +218 -111
- agno/tools/function.py +43 -4
- agno/tools/mcp.py +60 -37
- agno/tools/mcp_toolbox.py +284 -0
- agno/tools/scrapegraph.py +58 -31
- agno/tools/whatsapp.py +1 -1
- agno/utils/gemini.py +147 -19
- agno/utils/models/claude.py +9 -0
- agno/utils/print_response/agent.py +18 -2
- agno/utils/print_response/team.py +22 -6
- agno/utils/reasoning.py +22 -1
- agno/utils/string.py +9 -0
- agno/vectordb/base.py +2 -2
- agno/vectordb/langchaindb/langchaindb.py +5 -7
- agno/vectordb/llamaindex/llamaindexdb.py +25 -6
- agno/workflow/workflow.py +30 -15
- {agno-2.0.7.dist-info → agno-2.0.9.dist-info}/METADATA +4 -1
- {agno-2.0.7.dist-info → agno-2.0.9.dist-info}/RECORD +64 -61
- {agno-2.0.7.dist-info → agno-2.0.9.dist-info}/WHEEL +0 -0
- {agno-2.0.7.dist-info → agno-2.0.9.dist-info}/licenses/LICENSE +0 -0
- {agno-2.0.7.dist-info → agno-2.0.9.dist-info}/top_level.txt +0 -0
agno/tools/function.py
CHANGED
|
@@ -124,6 +124,8 @@ class Function(BaseModel):
|
|
|
124
124
|
_team: Optional[Any] = None
|
|
125
125
|
# The session state that the function is associated with
|
|
126
126
|
_session_state: Optional[Dict[str, Any]] = None
|
|
127
|
+
# The dependencies that the function is associated with
|
|
128
|
+
_dependencies: Optional[Dict[str, Any]] = None
|
|
127
129
|
|
|
128
130
|
# Media context that the function is associated with
|
|
129
131
|
_images: Optional[Sequence[Image]] = None
|
|
@@ -165,6 +167,8 @@ class Function(BaseModel):
|
|
|
165
167
|
del type_hints["audios"]
|
|
166
168
|
if "files" in sig.parameters and "files" in type_hints:
|
|
167
169
|
del type_hints["files"]
|
|
170
|
+
if "dependencies" in sig.parameters and "dependencies" in type_hints:
|
|
171
|
+
del type_hints["dependencies"]
|
|
168
172
|
# log_info(f"Type hints for {function_name}: {type_hints}")
|
|
169
173
|
|
|
170
174
|
# Filter out return type and only process parameters
|
|
@@ -172,7 +176,8 @@ class Function(BaseModel):
|
|
|
172
176
|
name: type_hints.get(name)
|
|
173
177
|
for name in sig.parameters
|
|
174
178
|
if name != "return"
|
|
175
|
-
and name
|
|
179
|
+
and name
|
|
180
|
+
not in ["agent", "team", "session_state", "self", "images", "videos", "audios", "files", "dependencies"]
|
|
176
181
|
}
|
|
177
182
|
|
|
178
183
|
# Parse docstring for parameters
|
|
@@ -201,7 +206,18 @@ class Function(BaseModel):
|
|
|
201
206
|
parameters["required"] = [
|
|
202
207
|
name
|
|
203
208
|
for name in parameters["properties"]
|
|
204
|
-
if name
|
|
209
|
+
if name
|
|
210
|
+
not in [
|
|
211
|
+
"agent",
|
|
212
|
+
"team",
|
|
213
|
+
"session_state",
|
|
214
|
+
"self",
|
|
215
|
+
"images",
|
|
216
|
+
"videos",
|
|
217
|
+
"audios",
|
|
218
|
+
"files",
|
|
219
|
+
"dependencies",
|
|
220
|
+
]
|
|
205
221
|
]
|
|
206
222
|
else:
|
|
207
223
|
# Mark a field as required if it has no default value (this would include optional fields)
|
|
@@ -209,7 +225,18 @@ class Function(BaseModel):
|
|
|
209
225
|
name
|
|
210
226
|
for name, param in sig.parameters.items()
|
|
211
227
|
if param.default == param.empty
|
|
212
|
-
and name
|
|
228
|
+
and name
|
|
229
|
+
not in [
|
|
230
|
+
"agent",
|
|
231
|
+
"team",
|
|
232
|
+
"session_state",
|
|
233
|
+
"self",
|
|
234
|
+
"images",
|
|
235
|
+
"videos",
|
|
236
|
+
"audios",
|
|
237
|
+
"files",
|
|
238
|
+
"dependencies",
|
|
239
|
+
]
|
|
213
240
|
]
|
|
214
241
|
|
|
215
242
|
# log_debug(f"JSON schema for {function_name}: {parameters}")
|
|
@@ -268,6 +295,8 @@ class Function(BaseModel):
|
|
|
268
295
|
del type_hints["audios"]
|
|
269
296
|
if "files" in sig.parameters and "files" in type_hints:
|
|
270
297
|
del type_hints["files"]
|
|
298
|
+
if "dependencies" in sig.parameters and "dependencies" in type_hints:
|
|
299
|
+
del type_hints["dependencies"]
|
|
271
300
|
# log_info(f"Type hints for {self.name}: {type_hints}")
|
|
272
301
|
|
|
273
302
|
# Filter out return type and only process parameters
|
|
@@ -281,6 +310,7 @@ class Function(BaseModel):
|
|
|
281
310
|
"videos",
|
|
282
311
|
"audios",
|
|
283
312
|
"files",
|
|
313
|
+
"dependencies",
|
|
284
314
|
]
|
|
285
315
|
if self.requires_user_input and self.user_input_fields:
|
|
286
316
|
if len(self.user_input_fields) == 0:
|
|
@@ -396,7 +426,8 @@ class Function(BaseModel):
|
|
|
396
426
|
self.parameters["required"] = [
|
|
397
427
|
name
|
|
398
428
|
for name in self.parameters["properties"]
|
|
399
|
-
if name
|
|
429
|
+
if name
|
|
430
|
+
not in ["agent", "team", "session_state", "images", "videos", "audios", "files", "self", "dependencies"]
|
|
400
431
|
]
|
|
401
432
|
|
|
402
433
|
def _get_cache_key(self, entrypoint_args: Dict[str, Any], call_args: Optional[Dict[str, Any]] = None) -> str:
|
|
@@ -419,6 +450,8 @@ class Function(BaseModel):
|
|
|
419
450
|
del copy_entrypoint_args["audios"]
|
|
420
451
|
if "files" in copy_entrypoint_args:
|
|
421
452
|
del copy_entrypoint_args["files"]
|
|
453
|
+
if "dependencies" in copy_entrypoint_args:
|
|
454
|
+
del copy_entrypoint_args["dependencies"]
|
|
422
455
|
args_str = str(copy_entrypoint_args)
|
|
423
456
|
|
|
424
457
|
kwargs_str = str(sorted((call_args or {}).items()))
|
|
@@ -599,6 +632,9 @@ class FunctionCall(BaseModel):
|
|
|
599
632
|
# Check if the entrypoint has an session_state argument
|
|
600
633
|
if "session_state" in signature(self.function.entrypoint).parameters: # type: ignore
|
|
601
634
|
entrypoint_args["session_state"] = self.function._session_state
|
|
635
|
+
# Check if the entrypoint has an dependencies argument
|
|
636
|
+
if "dependencies" in signature(self.function.entrypoint).parameters: # type: ignore
|
|
637
|
+
entrypoint_args["dependencies"] = self.function._dependencies
|
|
602
638
|
# Check if the entrypoint has an fc argument
|
|
603
639
|
if "fc" in signature(self.function.entrypoint).parameters: # type: ignore
|
|
604
640
|
entrypoint_args["fc"] = self
|
|
@@ -629,6 +665,9 @@ class FunctionCall(BaseModel):
|
|
|
629
665
|
# Check if the hook has an session_state argument
|
|
630
666
|
if "session_state" in signature(hook).parameters:
|
|
631
667
|
hook_args["session_state"] = self.function._session_state
|
|
668
|
+
# Check if the hook has an dependencies argument
|
|
669
|
+
if "dependencies" in signature(hook).parameters:
|
|
670
|
+
hook_args["dependencies"] = self.function._dependencies
|
|
632
671
|
|
|
633
672
|
if "name" in signature(hook).parameters:
|
|
634
673
|
hook_args["name"] = name
|
agno/tools/mcp.py
CHANGED
|
@@ -8,7 +8,7 @@ from typing import Any, Dict, List, Literal, Optional, Union
|
|
|
8
8
|
|
|
9
9
|
from agno.tools import Toolkit
|
|
10
10
|
from agno.tools.function import Function
|
|
11
|
-
from agno.utils.log import log_debug, log_info, log_warning
|
|
11
|
+
from agno.utils.log import log_debug, log_error, log_info, log_warning
|
|
12
12
|
from agno.utils.mcp import get_entrypoint_for_tool
|
|
13
13
|
|
|
14
14
|
try:
|
|
@@ -338,12 +338,13 @@ class MCPTools(Toolkit):
|
|
|
338
338
|
self.functions[f.name] = f
|
|
339
339
|
log_debug(f"Function: {f.name} registered with {self.name}")
|
|
340
340
|
except Exception as e:
|
|
341
|
-
|
|
341
|
+
log_error(f"Failed to register tool {tool.name}: {e}")
|
|
342
342
|
|
|
343
343
|
log_debug(f"{self.name} initialized with {len(filtered_tools)} tools")
|
|
344
344
|
self._initialized = True
|
|
345
|
+
|
|
345
346
|
except Exception as e:
|
|
346
|
-
|
|
347
|
+
log_error(f"Failed to get MCP tools: {e}")
|
|
347
348
|
raise
|
|
348
349
|
|
|
349
350
|
|
|
@@ -372,6 +373,7 @@ class MultiMCPTools(Toolkit):
|
|
|
372
373
|
client=None,
|
|
373
374
|
include_tools: Optional[list[str]] = None,
|
|
374
375
|
exclude_tools: Optional[list[str]] = None,
|
|
376
|
+
allow_partial_failure: bool = False,
|
|
375
377
|
**kwargs,
|
|
376
378
|
):
|
|
377
379
|
"""
|
|
@@ -387,6 +389,7 @@ class MultiMCPTools(Toolkit):
|
|
|
387
389
|
timeout_seconds: Timeout in seconds for managing timeouts for Client Session if Agent or Tool doesn't respond.
|
|
388
390
|
include_tools: Optional list of tool names to include (if None, includes all).
|
|
389
391
|
exclude_tools: Optional list of tool names to exclude (if None, excludes none).
|
|
392
|
+
allow_partial_failure: If True, allows toolkit to initialize even if some MCP servers fail to connect. If False, any failure will raise an exception.
|
|
390
393
|
"""
|
|
391
394
|
super().__init__(name="MultiMCPTools", **kwargs)
|
|
392
395
|
|
|
@@ -445,12 +448,16 @@ class MultiMCPTools(Toolkit):
|
|
|
445
448
|
self.server_params_list.append(StreamableHTTPClientParams(url=url))
|
|
446
449
|
|
|
447
450
|
self._async_exit_stack = AsyncExitStack()
|
|
451
|
+
self._successful_connections = 0
|
|
452
|
+
|
|
448
453
|
self._initialized = False
|
|
449
454
|
self._connection_task = None
|
|
450
455
|
self._active_contexts: list[Any] = []
|
|
451
456
|
self._used_as_context_manager = False
|
|
452
457
|
|
|
453
458
|
self._client = client
|
|
459
|
+
self._initialized = False
|
|
460
|
+
self.allow_partial_failure = allow_partial_failure
|
|
454
461
|
|
|
455
462
|
def cleanup():
|
|
456
463
|
"""Cancel active connections"""
|
|
@@ -511,39 +518,53 @@ class MultiMCPTools(Toolkit):
|
|
|
511
518
|
if self._initialized:
|
|
512
519
|
return
|
|
513
520
|
|
|
521
|
+
server_connection_errors = []
|
|
522
|
+
|
|
514
523
|
for server_params in self.server_params_list:
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
# Handle SSE connections
|
|
526
|
-
elif isinstance(server_params, SSEClientParams):
|
|
527
|
-
client_connection = await self._async_exit_stack.enter_async_context(
|
|
528
|
-
sse_client(**asdict(server_params))
|
|
529
|
-
)
|
|
530
|
-
self._active_contexts.append(client_connection)
|
|
531
|
-
read, write = client_connection
|
|
532
|
-
session = await self._async_exit_stack.enter_async_context(ClientSession(read, write))
|
|
533
|
-
self._active_contexts.append(session)
|
|
534
|
-
await self.initialize(session)
|
|
535
|
-
# Handle Streamable HTTP connections
|
|
536
|
-
elif isinstance(server_params, StreamableHTTPClientParams):
|
|
537
|
-
client_connection = await self._async_exit_stack.enter_async_context(
|
|
538
|
-
streamablehttp_client(**asdict(server_params))
|
|
539
|
-
)
|
|
540
|
-
self._active_contexts.append(client_connection)
|
|
541
|
-
read, write = client_connection[0:2]
|
|
542
|
-
session = await self._async_exit_stack.enter_async_context(ClientSession(read, write))
|
|
543
|
-
self._active_contexts.append(session)
|
|
544
|
-
await self.initialize(session)
|
|
524
|
+
try:
|
|
525
|
+
# Handle stdio connections
|
|
526
|
+
if isinstance(server_params, StdioServerParameters):
|
|
527
|
+
stdio_transport = await self._async_exit_stack.enter_async_context(stdio_client(server_params))
|
|
528
|
+
read, write = stdio_transport
|
|
529
|
+
session = await self._async_exit_stack.enter_async_context(
|
|
530
|
+
ClientSession(read, write, read_timeout_seconds=timedelta(seconds=self.timeout_seconds))
|
|
531
|
+
)
|
|
532
|
+
await self.initialize(session)
|
|
533
|
+
self._successful_connections += 1
|
|
545
534
|
|
|
546
|
-
|
|
535
|
+
# Handle SSE connections
|
|
536
|
+
elif isinstance(server_params, SSEClientParams):
|
|
537
|
+
client_connection = await self._async_exit_stack.enter_async_context(
|
|
538
|
+
sse_client(**asdict(server_params))
|
|
539
|
+
)
|
|
540
|
+
read, write = client_connection
|
|
541
|
+
session = await self._async_exit_stack.enter_async_context(ClientSession(read, write))
|
|
542
|
+
await self.initialize(session)
|
|
543
|
+
self._successful_connections += 1
|
|
544
|
+
|
|
545
|
+
# Handle Streamable HTTP connections
|
|
546
|
+
elif isinstance(server_params, StreamableHTTPClientParams):
|
|
547
|
+
client_connection = await self._async_exit_stack.enter_async_context(
|
|
548
|
+
streamablehttp_client(**asdict(server_params))
|
|
549
|
+
)
|
|
550
|
+
read, write = client_connection[0:2]
|
|
551
|
+
session = await self._async_exit_stack.enter_async_context(ClientSession(read, write))
|
|
552
|
+
await self.initialize(session)
|
|
553
|
+
self._successful_connections += 1
|
|
554
|
+
|
|
555
|
+
except Exception as e:
|
|
556
|
+
if not self.allow_partial_failure:
|
|
557
|
+
raise ValueError(f"MCP connection failed: {e}")
|
|
558
|
+
|
|
559
|
+
log_error(f"Failed to initialize MCP server with params {server_params}: {e}")
|
|
560
|
+
server_connection_errors.append(str(e))
|
|
561
|
+
continue
|
|
562
|
+
|
|
563
|
+
if self._successful_connections == 0 and server_connection_errors:
|
|
564
|
+
raise ValueError(f"All MCP connections failed: {server_connection_errors}")
|
|
565
|
+
|
|
566
|
+
if not self._initialized and self._successful_connections > 0:
|
|
567
|
+
self._initialized = True
|
|
547
568
|
|
|
548
569
|
async def close(self) -> None:
|
|
549
570
|
"""Close the MCP connections and clean up resources"""
|
|
@@ -563,6 +584,8 @@ class MultiMCPTools(Toolkit):
|
|
|
563
584
|
):
|
|
564
585
|
"""Exit the async context manager."""
|
|
565
586
|
await self._async_exit_stack.aclose()
|
|
587
|
+
self._initialized = False
|
|
588
|
+
self._successful_connections = 0
|
|
566
589
|
|
|
567
590
|
async def initialize(self, session: ClientSession) -> None:
|
|
568
591
|
"""Initialize the MCP toolkit by getting available tools from the MCP server"""
|
|
@@ -602,10 +625,10 @@ class MultiMCPTools(Toolkit):
|
|
|
602
625
|
self.functions[f.name] = f
|
|
603
626
|
log_debug(f"Function: {f.name} registered with {self.name}")
|
|
604
627
|
except Exception as e:
|
|
605
|
-
|
|
628
|
+
log_error(f"Failed to register tool {tool.name}: {e}")
|
|
606
629
|
|
|
607
|
-
log_debug(f"{self.name} initialized with {len(filtered_tools)} tools")
|
|
630
|
+
log_debug(f"{self.name} initialized with {len(filtered_tools)} tools from one MCP server")
|
|
608
631
|
self._initialized = True
|
|
609
632
|
except Exception as e:
|
|
610
|
-
|
|
633
|
+
log_error(f"Failed to get MCP tools: {e}")
|
|
611
634
|
raise
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
from typing import Any, Callable, Dict, List, Literal, Optional, Union
|
|
2
|
+
from warnings import warn
|
|
3
|
+
|
|
4
|
+
from agno.tools.function import Function
|
|
5
|
+
from agno.tools.mcp import MCPTools
|
|
6
|
+
from agno.utils.log import logger
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from toolbox_core import ToolboxClient # type: ignore
|
|
10
|
+
except ImportError:
|
|
11
|
+
raise ImportError("`toolbox_core` not installed. Please install using `pip install -U toolbox-core`.")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MCPToolsMeta(type):
|
|
15
|
+
"""Metaclass for MCPTools to ensure proper initialization with AgentOS"""
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def __name__(cls):
|
|
19
|
+
return "MCPTools"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MCPToolbox(MCPTools, metaclass=MCPToolsMeta):
|
|
23
|
+
"""
|
|
24
|
+
A toolkit that combines MCPTools server connectivity with MCP Toolbox for Databases client (toolbox-core).
|
|
25
|
+
|
|
26
|
+
MCPToolbox connects to an MCP Toolbox server and registers all available tools, then uses
|
|
27
|
+
toolbox-core to filter those tools by toolset or tool name. This enables agents to
|
|
28
|
+
receive only the specific tools they need while maintaining full MCP execution capabilities.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
url: str,
|
|
34
|
+
toolsets: Optional[List[str]] = None,
|
|
35
|
+
tool_name: Optional[str] = None,
|
|
36
|
+
headers: Optional[Dict[str, Any]] = None,
|
|
37
|
+
transport: Literal["stdio", "sse", "streamable-http"] = "streamable-http",
|
|
38
|
+
**kwargs,
|
|
39
|
+
):
|
|
40
|
+
"""Initialize MCPToolbox with filtering capabilities.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
url (str): Base URL for the toolbox service.
|
|
44
|
+
toolsets (Optional[List[str]], optional): List of toolset names to filter tools by. Defaults to None.
|
|
45
|
+
tool_name (Optional[str], optional): Single tool name to load. Defaults to None.
|
|
46
|
+
headers (Optional[Dict[str, Any]], optional): Headers for toolbox-core client requests. Defaults to None.
|
|
47
|
+
transport (Literal["stdio", "sse", "streamable-http"], optional): MCP transport protocol. Defaults to "streamable-http".
|
|
48
|
+
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
# Ensure the URL ends in "/mcp" as expected
|
|
52
|
+
if not url.endswith("/mcp"):
|
|
53
|
+
url = url + "/mcp"
|
|
54
|
+
|
|
55
|
+
super().__init__(url=url, transport=transport, **kwargs)
|
|
56
|
+
|
|
57
|
+
self.name = "toolbox_client"
|
|
58
|
+
self.toolbox_url = url
|
|
59
|
+
self.toolsets = toolsets
|
|
60
|
+
self.tool_name = tool_name
|
|
61
|
+
self.headers = headers
|
|
62
|
+
self._core_client_initialized = False
|
|
63
|
+
|
|
64
|
+
# Validate that only one of toolsets or tool_name is provided
|
|
65
|
+
filter_params = [toolsets, tool_name]
|
|
66
|
+
non_none_params = [p for p in filter_params if p is not None]
|
|
67
|
+
if len(non_none_params) > 1:
|
|
68
|
+
raise ValueError("Only one of toolsets or tool_name can be specified")
|
|
69
|
+
|
|
70
|
+
async def connect(self):
|
|
71
|
+
"""Initialize MCPToolbox instance and connect to the MCP server."""
|
|
72
|
+
# First, connect to MCP server and load all available tools
|
|
73
|
+
await super().connect()
|
|
74
|
+
|
|
75
|
+
if self._core_client_initialized:
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
# Then, connect to the ToolboxClient and filter tools based on toolsets or tool_name
|
|
79
|
+
await self._connect_toolbox_client()
|
|
80
|
+
|
|
81
|
+
async def _connect_toolbox_client(self):
|
|
82
|
+
try:
|
|
83
|
+
if self.toolsets is not None or self.tool_name is not None:
|
|
84
|
+
self.__core_client = ToolboxClient(
|
|
85
|
+
url=self.toolbox_url,
|
|
86
|
+
client_headers=self.headers,
|
|
87
|
+
)
|
|
88
|
+
self._core_client_initialized = True
|
|
89
|
+
|
|
90
|
+
if self.toolsets is not None:
|
|
91
|
+
# Load multiple toolsets
|
|
92
|
+
all_functions = await self.load_multiple_toolsets(toolset_names=self.toolsets)
|
|
93
|
+
# Replace functions dict with filtered subset
|
|
94
|
+
filtered_functions = {func.name: func for func in all_functions}
|
|
95
|
+
self.functions = filtered_functions
|
|
96
|
+
elif self.tool_name is not None:
|
|
97
|
+
tool = await self.load_tool(tool_name=self.tool_name)
|
|
98
|
+
# Replace functions dict with just this single tool
|
|
99
|
+
self.functions = {tool.name: tool}
|
|
100
|
+
except Exception as e:
|
|
101
|
+
raise RuntimeError(f"Failed to connect to ToolboxClient: {e}") from e
|
|
102
|
+
|
|
103
|
+
def _handle_auth_params(
|
|
104
|
+
self,
|
|
105
|
+
auth_token_getters: dict[str, Callable[[], str]] = {},
|
|
106
|
+
auth_tokens: Optional[dict[str, Callable[[], str]]] = None,
|
|
107
|
+
auth_headers: Optional[dict[str, Callable[[], str]]] = None,
|
|
108
|
+
):
|
|
109
|
+
"""handle authentication parameters for toolbox-core client"""
|
|
110
|
+
if auth_tokens:
|
|
111
|
+
if auth_token_getters:
|
|
112
|
+
warn(
|
|
113
|
+
"Both `auth_token_getters` and `auth_tokens` are provided. `auth_tokens` is deprecated, and `auth_token_getters` will be used.",
|
|
114
|
+
DeprecationWarning,
|
|
115
|
+
)
|
|
116
|
+
else:
|
|
117
|
+
warn(
|
|
118
|
+
"Argument `auth_tokens` is deprecated. Use `auth_token_getters` instead.",
|
|
119
|
+
DeprecationWarning,
|
|
120
|
+
)
|
|
121
|
+
auth_token_getters = auth_tokens
|
|
122
|
+
|
|
123
|
+
if auth_headers:
|
|
124
|
+
if auth_token_getters:
|
|
125
|
+
warn(
|
|
126
|
+
"Both `auth_token_getters` and `auth_headers` are provided. `auth_headers` is deprecated, and `auth_token_getters` will be used.",
|
|
127
|
+
DeprecationWarning,
|
|
128
|
+
)
|
|
129
|
+
else:
|
|
130
|
+
warn(
|
|
131
|
+
"Argument `auth_headers` is deprecated. Use `auth_token_getters` instead.",
|
|
132
|
+
DeprecationWarning,
|
|
133
|
+
)
|
|
134
|
+
auth_token_getters = auth_headers
|
|
135
|
+
return auth_token_getters
|
|
136
|
+
|
|
137
|
+
async def load_tool(
|
|
138
|
+
self,
|
|
139
|
+
tool_name: str,
|
|
140
|
+
auth_token_getters: dict[str, Callable[[], str]] = {},
|
|
141
|
+
auth_tokens: Optional[dict[str, Callable[[], str]]] = None,
|
|
142
|
+
auth_headers: Optional[dict[str, Callable[[], str]]] = None,
|
|
143
|
+
bound_params: dict[str, Union[Any, Callable[[], Any]]] = {},
|
|
144
|
+
) -> Function:
|
|
145
|
+
"""Loads the tool with the given tool name from the Toolbox service.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
tool_name (str): The name of the tool to load.
|
|
149
|
+
auth_token_getters (dict[str, Callable[[], str]], optional): A mapping of authentication source names to functions that retrieve ID tokens. Defaults to {}.
|
|
150
|
+
auth_tokens (Optional[dict[str, Callable[[], str]]], optional): Deprecated. Use `auth_token_getters` instead.
|
|
151
|
+
auth_headers (Optional[dict[str, Callable[[], str]]], optional): Deprecated. Use `auth_token_getters` instead.
|
|
152
|
+
bound_params (dict[str, Union[Any, Callable[[], Any]]], optional): A mapping of parameter names to their bound values. Defaults to {}.
|
|
153
|
+
|
|
154
|
+
Raises:
|
|
155
|
+
RuntimeError: If the tool is not found in the MCP functions registry.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Function: The loaded tool function.
|
|
159
|
+
"""
|
|
160
|
+
auth_token_getters = self._handle_auth_params(
|
|
161
|
+
auth_token_getters=auth_token_getters,
|
|
162
|
+
auth_tokens=auth_tokens,
|
|
163
|
+
auth_headers=auth_headers,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
core_sync_tool = await self.__core_client.load_tool(
|
|
167
|
+
name=tool_name,
|
|
168
|
+
auth_token_getters=auth_token_getters,
|
|
169
|
+
bound_params=bound_params,
|
|
170
|
+
)
|
|
171
|
+
# Return the Function object from our MCP functions registry
|
|
172
|
+
if core_sync_tool._name in self.functions:
|
|
173
|
+
return self.functions[core_sync_tool._name]
|
|
174
|
+
else:
|
|
175
|
+
raise RuntimeError(f"Tool '{tool_name}' was not found in MCP functions registry")
|
|
176
|
+
|
|
177
|
+
async def load_toolset(
|
|
178
|
+
self,
|
|
179
|
+
toolset_name: Optional[str] = None,
|
|
180
|
+
auth_token_getters: dict[str, Callable[[], str]] = {},
|
|
181
|
+
auth_tokens: Optional[dict[str, Callable[[], str]]] = None,
|
|
182
|
+
auth_headers: Optional[dict[str, Callable[[], str]]] = None,
|
|
183
|
+
bound_params: dict[str, Union[Any, Callable[[], Any]]] = {},
|
|
184
|
+
strict: bool = False,
|
|
185
|
+
) -> List[Function]:
|
|
186
|
+
"""Loads tools from the configured toolset.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
toolset_name (Optional[str], optional): The name of the toolset to load. Defaults to None.
|
|
190
|
+
auth_token_getters (dict[str, Callable[[], str]], optional): A mapping of authentication source names to functions that retrieve ID tokens. Defaults to {}.
|
|
191
|
+
auth_tokens (Optional[dict[str, Callable[[], str]]], optional): Deprecated. Use `auth_token_getters` instead.
|
|
192
|
+
auth_headers (Optional[dict[str, Callable[[], str]]], optional): Deprecated. Use `auth_token_getters` instead.
|
|
193
|
+
bound_params (dict[str, Union[Any, Callable[[], Any]]], optional): A mapping of parameter names to their bound values. Defaults to {}.
|
|
194
|
+
strict (bool, optional): If True, raises an error if *any* loaded tool instance fails
|
|
195
|
+
to utilize all of the given parameters or auth tokens. (if any
|
|
196
|
+
provided). If False (default), raises an error only if a
|
|
197
|
+
user-provided parameter or auth token cannot be applied to *any*
|
|
198
|
+
loaded tool across the set.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
List[Function]: A list of all tools loaded from the Toolbox.
|
|
202
|
+
"""
|
|
203
|
+
auth_token_getters = self._handle_auth_params(
|
|
204
|
+
auth_token_getters=auth_token_getters,
|
|
205
|
+
auth_tokens=auth_tokens,
|
|
206
|
+
auth_headers=auth_headers,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
core_sync_tools = await self.__core_client.load_toolset(
|
|
210
|
+
name=toolset_name,
|
|
211
|
+
auth_token_getters=auth_token_getters,
|
|
212
|
+
bound_params=bound_params,
|
|
213
|
+
strict=strict,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
tools = []
|
|
217
|
+
for core_sync_tool in core_sync_tools:
|
|
218
|
+
if core_sync_tool._name in self.functions:
|
|
219
|
+
tools.append(self.functions[core_sync_tool._name])
|
|
220
|
+
else:
|
|
221
|
+
logger.debug(f"Tool '{core_sync_tool._name}' from toolset '{toolset_name}' not available in MCP server")
|
|
222
|
+
return tools
|
|
223
|
+
|
|
224
|
+
async def load_multiple_toolsets(
|
|
225
|
+
self,
|
|
226
|
+
toolset_names: List[str],
|
|
227
|
+
auth_token_getters: dict[str, Callable[[], str]] = {},
|
|
228
|
+
bound_params: dict[str, Union[Any, Callable[[], Any]]] = {},
|
|
229
|
+
strict: bool = False,
|
|
230
|
+
) -> List[Function]:
|
|
231
|
+
"""Load tools from multiple toolsets.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
toolset_names (List[str]): A list of toolset names to load.
|
|
235
|
+
auth_token_getters (dict[str, Callable[[], str]], optional): A mapping of authentication source names to functions that retrieve ID tokens. Defaults to {}.
|
|
236
|
+
bound_params (dict[str, Union[Any, Callable[[], Any]]], optional): A mapping of parameter names to their bound values. Defaults to {}.
|
|
237
|
+
strict (bool, optional): If True, raises an error if *any* loaded tool instance fails to utilize all of the given parameters or auth tokens. Defaults to False.
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
List[Function]: A list of all tools loaded from the specified toolsets.
|
|
241
|
+
"""
|
|
242
|
+
all_tools = []
|
|
243
|
+
for toolset_name in toolset_names:
|
|
244
|
+
tools = await self.load_toolset(
|
|
245
|
+
toolset_name=toolset_name,
|
|
246
|
+
auth_token_getters=auth_token_getters,
|
|
247
|
+
bound_params=bound_params,
|
|
248
|
+
strict=strict,
|
|
249
|
+
)
|
|
250
|
+
all_tools.extend(tools)
|
|
251
|
+
return all_tools
|
|
252
|
+
|
|
253
|
+
async def close(self):
|
|
254
|
+
"""Close the underlying asynchronous client."""
|
|
255
|
+
if self._core_client_initialized and hasattr(self, "_MCPToolbox__core_client"):
|
|
256
|
+
await self.__core_client.close()
|
|
257
|
+
await super().close()
|
|
258
|
+
|
|
259
|
+
async def load_toolset_safe(self, toolset_name: str) -> List[str]:
|
|
260
|
+
"""Safely load a toolset and return tool names."""
|
|
261
|
+
try:
|
|
262
|
+
tools = await self.load_toolset(toolset_name)
|
|
263
|
+
return [tool.name for tool in tools]
|
|
264
|
+
except Exception as e:
|
|
265
|
+
raise RuntimeError(f"Failed to load toolset '{toolset_name}': {e}") from e
|
|
266
|
+
|
|
267
|
+
def get_client(self) -> ToolboxClient:
|
|
268
|
+
"""Get the underlying ToolboxClient."""
|
|
269
|
+
if not self._core_client_initialized:
|
|
270
|
+
raise RuntimeError("ToolboxClient not initialized. Call connect() first.")
|
|
271
|
+
return self.__core_client
|
|
272
|
+
|
|
273
|
+
async def __aenter__(self):
|
|
274
|
+
"""Initialize the direct toolbox client."""
|
|
275
|
+
await super().__aenter__()
|
|
276
|
+
await self.connect()
|
|
277
|
+
return self
|
|
278
|
+
|
|
279
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
280
|
+
"""Clean up the toolbox client."""
|
|
281
|
+
# Close ToolboxClient first, then MCP client
|
|
282
|
+
if self._core_client_initialized and hasattr(self, "_MCPToolbox__core_client"):
|
|
283
|
+
await self.__core_client.close()
|
|
284
|
+
await super().__aexit__(exc_type, exc_val, exc_tb)
|